diff --git a/scripts/notebook_tools/check_split_reading_cells.py b/scripts/notebook_tools/check_split_reading_cells.py index 1e53754540..b4df32a68a 100644 --- a/scripts/notebook_tools/check_split_reading_cells.py +++ b/scripts/notebook_tools/check_split_reading_cells.py @@ -572,18 +572,46 @@ def role(c): return role(prev), role(nxt) +_PURE_FENCE_RE = re.compile(r"```[^\n]*\n.*?```\Z", re.DOTALL) + + +def _is_pure_fence(cell: dict) -> bool: + """Vrai si la cellule markdown est reduite a UN bloc fence, sans prose. + + C'est le gabarit exact d'une conversion code->markdown (pseudo-code fige, + #18602) : ouverture ```` ``` ````, corps, fermeture -- rien avant, rien + apres. Ce n'est ni une lecture ni de la prose : la cellule represente le + code qu'elle etait. La lecture qui la suit commentait SA sortie (disparue + avec la conversion), pas celle du code au-dessus -- la remontee s'arrete + donc sur elle comme sur un exercice. Une cellule faite uniquement de + fences (une ou plusieurs conversions) l'est aussi : chaque fence fige un + code dont la sortie a disparu. Seule la cellule qui MELANGE prose et + fence n'est pas une pure fence : ses lectures restent rattachees au code + au-dessus. + """ + if cell.get("cell_type") != "markdown": + return False + src = cell_source(cell).strip() + if not src.startswith("```"): + return False + return bool(_PURE_FENCE_RE.match(src)) + + def _output_key_above(cells: list[dict], idx: int) -> str | None: """Source de la cellule de code dont ``cells[idx]`` commente la sortie. La sortie est identifiee par la **source de sa cellule de code** (#17044, decision ai-01 c.5836401913). La remontee s'arrete a la premiere cellule de code : c'est elle, et elle seule, que la lecture commente. ``None`` s'il n'y - a rien a lire -- aucun code au-dessus, code d'exercice, ou code jamais - execute. + a rien a lire -- aucun code au-dessus, code d'exercice, code jamais + execute, ou pure fence de conversion (#18602) -- la lecture sous la fence + commentait la sortie du code converti, pas celle du code au-dessus. """ for j in range(idx - 1, -1, -1): c = cells[j] if c.get("cell_type") != "code": + if _is_pure_fence(c): + return None continue if is_exercise_cell(c): return None @@ -599,12 +627,14 @@ def readings_by_output(nb: dict) -> Counter[str]: C'est ce compte, et lui seul, que le mode diff compare entre la base et la tete (#17044, decision c.5836401913) : une cellule markdown dont la source est neuve n'est pas, a elle seule, un ajout -- une fusion, une revision ou - un deplacement laissent ce compte inchange. + un deplacement laissent ce compte inchange. Une pure fence de conversion + code->markdown (#18602) n'est pas une lecture : elle ne gonfle le compte + d'aucune sortie. """ cells = nb.get("cells", []) counts: Counter[str] = Counter() for i, c in enumerate(cells): - if not is_reading_or_prose(c): + if not is_reading_or_prose(c) or _is_pure_fence(c): continue key = _output_key_above(cells, i) if key is not None: diff --git a/scripts/tests/test_split_reading_code_to_md.py b/scripts/tests/test_split_reading_code_to_md.py index a3fe355974..e46dd1637f 100644 --- a/scripts/tests/test_split_reading_code_to_md.py +++ b/scripts/tests/test_split_reading_code_to_md.py @@ -38,12 +38,16 @@ Cycle c.1001 : main (commit ``24511147e`` / PR #18604) a fixe le bug pour ``detect_added_readings`` et la topologie reelle Lean-10 c.63 (les tests 1 et 3 passent donc en regression). Le test 2 -(``readings_by_output``, helper single-notebook) reste en xfail strict : -le helper a un bug separe que le carve-out principal ne touche pas (cf -message commit ``24511147e``). A chaque XPASS sur test 2 = signal que -le helper a ete corrige -> retirer ce xfail. +(``readings_by_output``, helper single-notebook) restait en xfail strict : +le helper avait un bug separe que le carve-out principal ne touchait pas +(cf message commit ``24511147e``). + +Cycle suivant (residuel #18602, c.935) : le helper est corrige -- une +pure fence de conversion code->markdown n'est plus comptee comme lecture +et borne la remontee ``_output_key_above`` (la lecture sous la fence +commentait la sortie du code converti, disparue). Le xfail du test 2 est +retire : les trois tests sont des regressions. """ -import pytest import sys from pathlib import Path @@ -89,7 +93,6 @@ def test_code_to_md_conversion_not_second_reading(): assert mod.detect_added_readings(head_nb, base_nb) == [] -@pytest.mark.xfail(reason="REPRODUCTION c.970 du bug split-reading code->md ; le cliquet rattache a tort -- cf docstring", strict=True) def test_code_to_md_keeps_legitimate_reading_count(): """La conversion code->fence ne doit faire monter le compte de lectures d'AUCUNE sortie : la lecture legitime reste rattachee a la sortie du @@ -127,6 +130,22 @@ def test_code_to_md_keeps_legitimate_reading_count(): assert mod.readings_by_output(base_nb) == {"print(100)": 1, "print(42)": 1} +def test_prose_mixed_fence_is_not_a_boundary(): + """Controle negatif de la borne pure-fence : une cellule qui MELANGE prose + et fence n'est pas une conversion figee -- la lecture titree qui la suit + reste rattachee au code execute au-dessus (le melange ne borne pas la + remontee). La prose melangee est comptee comme lecture, comportement + preexistant d'``is_reading_or_prose`` que ce fix ne change pas.""" + nb = { + "cells": [ + code("print(100)", "c0", output="100\n"), + md("Exemple de reglage :\n```python\nprint(42)\n```\nOn voit la valeur.", "mix"), + md("### Interpretation : resultat", "m2"), + ] + } + assert mod.readings_by_output(nb) == {"print(100)": 2} + + def test_minimal_repro_pr_18440(): """Topologie Lean-10-LeanDojo c.60-c.65 mesuree sur la PR #18440 (head = 1ad120705, base = 1ad120705^) : ids reels, memes positions.