diff --git a/scripts/notebook_tools/check_split_reading_cells.py b/scripts/notebook_tools/check_split_reading_cells.py index 9672704f7c..b6b36c2a34 100644 --- a/scripts/notebook_tools/check_split_reading_cells.py +++ b/scripts/notebook_tools/check_split_reading_cells.py @@ -41,8 +41,11 @@ est un **compte par sortie**, pas un jugement par cellule : voir la decision #17044 ci-dessous ; - ``READING_BEFORE_CODE`` : cellule markdown ajoutee directement devant une - cellule de code AVEC sortie (la lecture doit - suivre la sortie, pas la preceder) ; + cellule de code AVEC sortie ET sans code execute + au-dessus d'elle (la lecture doit suivre la + sortie, pas la preceder ; sous un code execute, + le compte par sortie prend le relais -- #17044, + decision c.5877090210) ; - ``EXERCISE_READING`` : cellule markdown ajoutee juste apres une cellule d'exercice (stub sans sortie) -- l'etudiant ne verra pas la lecture tant qu'il n'a pas complete. @@ -620,7 +623,8 @@ def detect_added_readings(head_nb: dict, base_nb: dict | None) -> list[dict]: 4. Pour chaque cellule ajoutee qui est markdown, classifier via le **contexte HEAD** : - ``EXERCISE_READING`` prev_role == "exercise" - - ``READING_BEFORE_CODE`` next_role == "code_with_output" + - ``READING_BEFORE_CODE`` next_role == "code_with_output" ET + prev_role != "code_with_output" - ``SECOND_READING`` la sortie que la cellule commente porte **plus de lectures en tete qu'en base** @@ -803,9 +807,16 @@ def _bucket_for(prev_role: str, next_role: str) -> str | None: if prev_role == "exercise": return "EXERCISE_READING_CANDIDATE" if next_role in ("code_with_output", "exercise"): - # Une lecture ajoutee devant un exercice OU un code deja execute - # precede le resultat qu'elle est censee commenter -- dans les deux - # cas la place canonique est APRES, pas avant. + # Une lecture ajoutee sous un code execute est rattachee par + # ``_output_key_above`` a la sortie du dessus : elle releve du compte + # par sortie (SECOND_READING si le compte monte, rien sinon) -- la + # dire en meme temps « avant son resultat » serait l'incoherence + # mesuree sur #17044 (c.5877090210). READING_BEFORE_CODE ne survit + # que sans code execute directement au-dessus (prev_role md ou + # BOUNDARY) : la lecture y precede alors le resultat qu'elle est + # censee commenter, et la place canonique est APRES, pas avant. + if prev_role == "code_with_output": + return "SECOND_READING" return "READING_BEFORE_CODE" if prev_role in ("md", "code_with_output"): return "SECOND_READING" diff --git a/scripts/tests/test_check_split_reading_cells.py b/scripts/tests/test_check_split_reading_cells.py index 2a9d1b7276..46657293c4 100644 --- a/scripts/tests/test_check_split_reading_cells.py +++ b/scripts/tests/test_check_split_reading_cells.py @@ -735,6 +735,67 @@ def test_diff_reading_before_code_devant_sortie(): assert f["next_role"] == "code_with_output" +def test_diff_lecture_sous_code_devant_code_sans_exces_non_flaggee(): + """Decision #17044 (c.5877090210) : une lecture ajoutee directement SOUS un + code execute est rattachee par ``_output_key_above`` a la sortie du dessus + -- elle releve du compte par sortie, pas de la topologie. Devant le code + SUIVANT mais sans exces de lecture sur la sortie du dessus : rien a + signaler. Classe mesuree : cellule 8 d'Infer-08b (#18087), faux positif. + """ + base = nb( + code("print(1)"), + code("print(2)"), + ) + head = nb( + code("print(1)"), + md("### Analyse du resultat\nLa convergence est nette."), + code("print(2)"), + ) + assert detect_added_readings(head, base) == [] + + +def test_diff_lecture_sous_code_avec_exces_rest_second_reading(): + """Meme topologie (prev = code execute, next = code), mais la sortie du + dessus portait deja une lecture : l'ajout fait monter le compte -> + SECOND_READING par le compte par sortie. Le bucket topologique ne dit plus + READING_BEFORE_CODE pour une cellule qu'il rattache par ailleurs a la + sortie du dessus (incoherence mesuree sur #17044). + """ + base = nb( + code("print(1)"), + md("### Lecture\nPremiere lecture legitime."), + code("print(2)"), + ) + head = nb( + code("print(1)"), + md("### Lecture\nPremiere lecture legitime."), + md("### Lecture chiffree\nDeuxieme lecture sur la meme sortie."), + code("print(2)"), + ) + findings = detect_added_readings(head, base) + assert [f["type"] for f in findings] == ["SECOND_READING"] + + +def test_diff_lecture_sous_md_devant_code_rest_reading_before_code(): + """Controle negatif de la decision : sans code execute directement + au-dessus (prev_role md), la lecture ajoutee devant un code execute RESTE + READING_BEFORE_CODE -- la place canonique d'une lecture introductive + n'a pas change. + """ + base = nb( + md("## 1. Contexte du banc"), + code("print(1)"), + ) + head = nb( + md("## 1. Contexte du banc"), + md("### Lecture introductive\nOn annonce le resultat avant le code."), + code("print(1)"), + ) + findings = detect_added_readings(head, base) + assert len(findings) == 1 + assert findings[0]["type"] == "READING_BEFORE_CODE" + + def test_diff_section_header_devant_code_non_flagge(): """Carve-out #17777 etendu a READING_BEFORE_CODE : un en-tete de section (titre d'organisation, meme suivi de prose) qui introduit le code qui