diff --git a/.github/workflows/notebook-latex-control-chars.yml b/.github/workflows/notebook-latex-control-chars.yml new file mode 100644 index 0000000000..f92c779c13 --- /dev/null +++ b/.github/workflows/notebook-latex-control-chars.yml @@ -0,0 +1,87 @@ +name: Notebook LaTeX Control Chars + +# Issue #14859 (myia-po-2026:CoursIA, 2026-09-06): LaTeX generated from a +# non-raw Python string loses its backslash to an escape sequence -- +# `\theta` lands as TAB + "heta" inside the math scope. The JSON stays +# valid, the notebook opens, every other guard passes, and the formula does +# not render. Measured on main: 47 occurrences across 3 notebooks. +# +# This guard (ADVISORY, non-blocking -- the coordinateur decides): +# - scans every notebook touched by the PR with +# scripts/notebook_tools/check_latex_control_chars.py (3 discriminants: +# markdown cells only, inside $...$/$$...$$ scopes, known command +# queue after the control char; details + FP classes in the script +# docstring); +# - on occurrences: ::warning:: annotations (one per defect) + summary, +# exit 0 -- advisory organ, the PR gate does not fail; +# - on detector error (exit 2): ::warning:: NAMING the failure -- never +# a silent pass (#14849 lesson: an unreadable input is an unknown +# verdict, not a clean one); +# - runs its unit tests (positive AND negative controls -- the issue +# measured twice a wrong pattern set returning a clean zero). + +on: + pull_request: + branches: [main] + paths: + - '**.ipynb' + - 'scripts/notebook_tools/check_latex_control_chars.py' + - '.github/workflows/notebook-latex-control-chars.yml' + push: + branches: [main] + paths: + - 'scripts/notebook_tools/check_latex_control_chars.py' + - '.github/workflows/notebook-latex-control-chars.yml' + +permissions: + contents: read + +concurrency: + group: notebook-latex-control-chars-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + latex-control-chars: + # Route vers la jambe Linux auto-hebergee (cf notebook-cell-source-parses + # #13378 tranche 1) : garde Python pur, sans secret ni GITHUB_TOKEN. + runs-on: [self-hosted, coursia-ephemeral, coursia-linux] + if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository + timeout-minutes: 10 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Scan notebooks touched by PR (advisory) + env: + BASE: ${{ github.event.pull_request.base.sha }} + HEAD: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + set -uo pipefail + if [ -z "$BASE" ] || [ "$BASE" = "$HEAD" ]; then + echo "No PR base SHA available; whole-repo scan (informational)." + python scripts/notebook_tools/check_latex_control_chars.py + exit 0 + fi + # Detector exit codes: 0 clean, 1 occurrences (advisory here), 2 error. + if python scripts/notebook_tools/check_latex_control_chars.py --pr-diff "$BASE" "$HEAD"; then + echo "Aucun caractere de controle dans les portees math des notebooks touches." + else + RC=$? + if [ "$RC" = "2" ]; then + echo "::warning::Detecteur latex-control-chars en ERREUR (exit 2 -- notebook illisible ou args invalides) : verdict UNKNOWN, voir le log ci-dessus (#14849)." + exit 1 + fi + echo "::warning::Caracteres de controle ayant avale un escape LaTeX dans les notebooks touches (advisory #14859) : corriger la cause (chaine raw) puis regenerer -- details dans le log ci-dessus." + exit 0 + fi + + - name: Run unit tests + run: | + python -m unittest scripts.tests.test_check_latex_control_chars -v diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-15-CooperativeGames.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-15-CooperativeGames.ipynb index 54cd01b5c3..38727ea00c 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-15-CooperativeGames.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-15-CooperativeGames.ipynb @@ -185,7 +185,7 @@ "\n", "Un **jeu coopératif** $(N, v)$ est défini par :\n", "- $N = \\{1, 2, ..., n\\}$ : ensemble des joueurs\n", - "- $v : 2^N \rightarrow \\mathbb{R}$ : **fonction caractéristique**\n", + "- $v : 2^N \\rightarrow \\mathbb{R}$ : **fonction caractéristique**\n", " - $v(S)$ = valeur que la coalition $S$ peut obtenir\n", " - Convention : $v(\\emptyset) = 0$\n", "\n", @@ -197,7 +197,7 @@ "- **Convexité** : $v(S \\cup \\{i\\}) - v(S) \\leq v(T \\cup \\{i\\}) - v(T)$ pour $S \\subseteq T$\n", " - La contribution marginale croît avec la taille de la coalition\n", "\n", - "### Pourquoi $v : 2^N \rightarrow \\mathbb{R}$ plutot qu'un modele strategique\n", + "### Pourquoi $v : 2^N \\rightarrow \\mathbb{R}$ plutot qu'un modele strategique\n", "\n", "Dans un jeu non-cooperatif (notebooks 1-13), on specifie les **actions** et les **paiements** de chaque joueur. Le modele strategique est complet : connaissant l'equilibre de Nash, on peut predire l'issue.\n", "\n", @@ -217,7 +217,8 @@ "\n", "### Lien avec l'economie\n", "\n", - "En **economie industrielle**, les TU games sont utilises pour analyser les **fusions d'entreprises**. $v(S)$ represente le profit que les entreprises $S$ reunies peuvent obtenir (apres synergies). La theorie de Shapley predit quelle fusion est equitable. Voir **Aumann & Dreze 1974** *Cooperative games with coalition structures* pour la generalisation avec structures de coalition arbitraires.\n" + "En **economie industrielle**, les TU games sont utilises pour analyser les **fusions d'entreprises**. $v(S)$ represente le profit que les entreprises $S$ reunies peuvent obtenir (apres synergies). La theorie de Shapley predit quelle fusion est equitable. Voir **Aumann & Dreze 1974** *Cooperative games with coalition structures* pour la generalisation avec structures de coalition arbitraires.\n", + "" ] }, { @@ -416,7 +417,7 @@ "\n", "### Formule\n", "\n", - "$$\\phi_i(v) = \\sum_{S \\subseteq N \\setminus \\{i\\}} \frac{|S|!(n-|S|-1)!}{n!} \\cdot [v(S \\cup \\{i\\}) - v(S)]$$\n", + "$$\\phi_i(v) = \\sum_{S \\subseteq N \\setminus \\{i\\}} \\frac{|S|!(n-|S|-1)!}{n!} \\cdot [v(S \\cup \\{i\\}) - v(S)]$$\n", "\n", "**Interprétation** : Moyenne des contributions marginales de $i$ sur tous les ordres d'arrivée possibles.\n", "\n", @@ -441,7 +442,8 @@ "\n", "La formule de Shapley ressemble a une **moyenne ponderée** sur les permutations -- analogue a l'**ensemble canonique** en mecanique statistique (chaque permutation = micro-etat). Cette analogie est profonde : la theorie de Shapley peut etre vue comme une integration sur l'**ensemble des ordres d'arrivee**, chaque ordre etant un micro-etat du processus de coalition.\n", "\n", - "Voir **Aumann 1985** *What is game theory trying to accomplish?* pour une discussion philosophique sur le statut des axiomes en theorie des jeux.\n" + "Voir **Aumann 1985** *What is game theory trying to accomplish?* pour une discussion philosophique sur le statut des axiomes en theorie des jeux.\n", + "" ] }, { @@ -740,7 +742,7 @@ "### Complexité du calcul de Shapley\n", "\n", "- **Exact** : $O(n! \\cdot n)$ ou $O(2^n \\cdot n)$ - seulement pour $n \\leq 10$\n", - "- **Monte Carlo** : $O(\text{samples} \\cdot n)$ - pour grands $n$\n", + "- **Monte Carlo** : $O(\\text{samples} \\cdot n)$ - pour grands $n$\n", "\n", "Le calcul exact devient impraticable pour de grands ensembles de joueurs, ce qui explique pourquoi la valeur de Shapley est rarement utilisée en pratique politique (voir section 5).\n", "\n", @@ -748,7 +750,7 @@ "\n", "**Exact $O(2^n \\cdot n)$** : on enumere tous les sous-ensembles $S \\subseteq N \\setminus \\{i\\}$ (il y en a $2^{n-1}$), pour chaque $S$ on evalue $v(S \\cup \\{i\\})$ et $v(S)$ (2 evaluations), puis on moyenne. Pour $n=10$, ca fait $10 \\cdot 2^9 = 5120$ evaluations -- encore faisable. Pour $n=20$, ca fait $20 \\cdot 2^{19} \u0007pprox 10^7$ -- long mais faisable. Pour $n=30$, ca fait $20 \\cdot 2^{29} \u0007pprox 10^{10}$ -- impraticable.\n", "\n", - "**Monte Carlo $O(\text{samples} \\cdot n)$** : on tire `samples` permutations aleatoires uniformement, pour chaque permutation on calcule les contributions marginales, on moyenne. La convergence est en $O(1/\\sqrt{\text{samples}})$ par le TCL. Pour `samples = 10^4` et $n = 100$, on obtient une erreur typique de l'ordre de $10^{-2}$ -- acceptable pour la visualisation politique, insuffisant pour une application legale ou financiere.\n", + "**Monte Carlo $O(\\text{samples} \\cdot n)$** : on tire `samples` permutations aleatoires uniformement, pour chaque permutation on calcule les contributions marginales, on moyenne. La convergence est en $O(1/\\sqrt{\\text{samples}})$ par le TCL. Pour `samples = 10^4` et $n = 100$, on obtient une erreur typique de l'ordre de $10^{-2}$ -- acceptable pour la visualisation politique, insuffisant pour une application legale ou financiere.\n", "\n", "**Approximation structurelle** : pour des jeux **convexes** ou avec structure particuliere (graph games, weighted voting), des algorithmes polynomiaux existent. Voir **Iehlé 2007** *The shapley value of cooperative games with fuzzy graph* pour une extension aux graphes flous.\n", "\n", @@ -886,7 +888,7 @@ "\n", "Le **Core** est l'ensemble des allocations stables :\n", "\n", - "$$\text{Core}(v) = \\{x \\in \\mathbb{R}^n : \\sum_i x_i = v(N) \text{ et } \\sum_{i \\in S} x_i \\geq v(S) \text{ pour tout } S\\}$$\n", + "$$\\text{Core}(v) = \\{x \\in \\mathbb{R}^n : \\sum_i x_i = v(N) \\text{ et } \\sum_{i \\in S} x_i \\geq v(S) \\text{ pour tout } S\\}$$\n", "\n", "**Interprétation** : Une allocation est dans le Core si aucune coalition $S$ ne peut \"bloquer\" (obtenir plus en se séparant).\n", "\n", @@ -918,7 +920,8 @@ "\n", "Le **nucleolus** (Schmeidler 1969) est une autre solution qui raffine le Core : c'est l'allocation qui **minimise** le plus grand exces (l'insatisfaction de la coalition la plus mecontente). Le nucleolus existe toujours et est unique. Pour les jeux convexes, nucleolus = Shapley = Core.\n", "\n", - "**Liens** : voir **Peleg & Sudholter 2007** *Introduction to the theory of cooperative games* pour un traitement exhaustif. Le Core, nucleolus, Shapley, nucleon, Banzhaf sont les cinq solutions standards. Chacune a des interpretations differentes de l'equite, et aucune n'est universellement \"la bonne\" -- c'est un defaut de la theorie que les chercheurs reconnaissent depuis longtemps.\n" + "**Liens** : voir **Peleg & Sudholter 2007** *Introduction to the theory of cooperative games* pour un traitement exhaustif. Le Core, nucleolus, Shapley, nucleon, Banzhaf sont les cinq solutions standards. Chacune a des interpretations differentes de l'equite, et aucune n'est universellement \"la bonne\" -- c'est un defaut de la theorie que les chercheurs reconnaissent depuis longtemps.\n", + "" ] }, { @@ -1124,7 +1127,7 @@ "\n", "Pour un **jeu convexe** (economies d'echelle), les contributions marginales **croissent** avec la taille de la coalition. Formellement :\n", "\n", - "$$v(S \\cup \\{i\\}) - v(S) \\leq v(T \\cup \\{i\\}) - v(T) \\quad \text{pour } S \\subseteq T$$\n", + "$$v(S \\cup \\{i\\}) - v(S) \\leq v(T \\cup \\{i\\}) - v(T) \\quad \\text{pour } S \\subseteq T$$\n", "\n", "Geometriquement, la \"courbe des contributions marginales\" est convexe. Cela suffit a garantir que le Core est non-vide : les contributions marginales sont tellement croissantes que la grande coalition est **strictement preferable** a toute sous-coalition, et l'allocation equitable peut etre obtenue par egalisation des contributions marginales.\n", "\n", @@ -1409,7 +1412,7 @@ "\n", "### Strategie d'equilibre pour theta=0.8\n", "\n", - "Pour $\theta = 0.8$, la **probabilite de l'etat A** (Harriet prefere les trombonnes) est 0.8. La **strategie d'equilibre** de Harriet est de choisir 2 trombonnes : ce signal est si clairement en faveur de A que Robbie peut conclure $\theta > 0.5$ avec certitude.\n", + "Pour $\\theta = 0.8$, la **probabilite de l'etat A** (Harriet prefere les trombonnes) est 0.8. La **strategie d'equilibre** de Harriet est de choisir 2 trombonnes : ce signal est si clairement en faveur de A que Robbie peut conclure $\\theta > 0.5$ avec certitude.\n", "\n", "**Calcul Bayesien** : la strategie de Robbie est :\n", "\n", @@ -1417,13 +1420,13 @@ "- Si Harriet choisit 1 de chaque : posterior de A = ?\n", "- Si Harriet choisit 2 stylos : posterior de A = ?\n", "\n", - "Selon le principe de **dominance faible** (milnor 1954), Harriet choisit le signal qui maximise son **utilite esperee** sur la strategie optimale de Robbie. Pour $\theta = 0.8$, choisir 2 trombonnes donne l'utilite la plus haute (Robbie fournit 90 trombonnes plutot que 50/50 ou 0/100).\n", + "Selon le principe de **dominance faible** (milnor 1954), Harriet choisit le signal qui maximise son **utilite esperee** sur la strategie optimale de Robbie. Pour $\\theta = 0.8$, choisir 2 trombonnes donne l'utilite la plus haute (Robbie fournit 90 trombonnes plutot que 50/50 ou 0/100).\n", "\n", "### Pourquoi c'est pedagogiquement interessant\n", "\n", - "Le cas $\theta = 0.8$ est \"facile\" pour le robot : Harriet est tellement en faveur de A que son signal est non-ambigu. Le cas $\theta = 0.5$ est **critique** : Harriet est indifferente a l'etat reel, son signal ne revele rien, et le robot doit fournir 50/50 (l'equilibre symetrique).\n", + "Le cas $\\theta = 0.8$ est \"facile\" pour le robot : Harriet est tellement en faveur de A que son signal est non-ambigu. Le cas $\\theta = 0.5$ est **critique** : Harriet est indifferente a l'etat reel, son signal ne revele rien, et le robot doit fournir 50/50 (l'equilibre symetrique).\n", "\n", - "La transition entre les deux regimes est abrupte : pour $\theta$ dans une **micro-bande** autour de 0.5, le signal est ambigu et l'efficacite baisse. Pour $\theta$ eloigne de 0.5, le signal est clair et l'efficacite est parfaite. Cette **transition de phase** est un phenomene classique de la theorie des jeux bayesiens.\n", + "La transition entre les deux regimes est abrupte : pour $\\theta$ dans une **micro-bande** autour de 0.5, le signal est ambigu et l'efficacite baisse. Pour $\\theta$ eloigne de 0.5, le signal est clair et l'efficacite est parfaite. Cette **transition de phase** est un phenomene classique de la theorie des jeux bayesiens.\n", "" ] }, @@ -1532,7 +1535,7 @@ "\n", "### Cas critique : pourquoi le signal \"1 de chaque\" ?\n", "\n", - "Pour $\theta = 0.5$ exactement, Harriet est **indifferente** entre les 3 etats (A, B, C). Le signal \"1 de chaque\" revele aux observateurs (Robbie) que Harriet est moderee, sans pour autant defavoriser un etat.\n", + "Pour $\\theta = 0.5$ exactement, Harriet est **indifferente** entre les 3 etats (A, B, C). Le signal \"1 de chaque\" revele aux observateurs (Robbie) que Harriet est moderee, sans pour autant defavoriser un etat.\n", "\n", "**C'est le signal \"neutre\"** : ne revele pas de preference, mais indique une absence de preference forte. Pour le robot, c'est le signal de l'indecision -- il doit fournir une quantite equilibree (50/50 ou tout autre partage egal).\n", "\n", @@ -1546,9 +1549,9 @@ "\n", "Le **cout bayesien** de l'incertitude (perte par rapport a l'information parfaite) est :\n", "\n", - "$$C(\theta) = \\max_{a_H, a_R} U_H(\theta, a_H^*(\theta), a_R^*(\theta)) - U_H(\theta, a_H^{eq}(\theta), a_R^{eq}(\theta))$$\n", + "$$C(\\theta) = \\max_{a_H, a_R} U_H(\\theta, a_H^*(\\theta), a_R^*(\\theta)) - U_H(\\theta, a_H^{eq}(\\theta), a_R^{eq}(\\theta))$$\n", "\n", - "Pour ce jeu, $C(\theta)$ est nul sauf pour $\theta$ dans les micro-bandes $[\theta^*-\\epsilon, \theta^*+\\epsilon]$. L'integrale de $C(\theta)$ sur $\theta \\in [0, 1]$ est donc petite -- c'est un jeu ou **l'incertitude est presque gratuite**.\n", + "Pour ce jeu, $C(\\theta)$ est nul sauf pour $\\theta$ dans les micro-bandes $[\\theta^*-\\epsilon, \\theta^*+\\epsilon]$. L'integrale de $C(\\theta)$ sur $\\theta \\in [0, 1]$ est donc petite -- c'est un jeu ou **l'incertitude est presque gratuite**.\n", "\n", "### Pourquoi c'est important pour l'AI Safety\n", "\n", @@ -1670,7 +1673,7 @@ "### Setup formel\n", "\n", "- Robot avec recompense $R(s)$ inconnue\n", - "- Humain avec recompense **fixee** $R^* : S \rightarrow \\mathbb{R}$\n", + "- Humain avec recompense **fixee** $R^* : S \\rightarrow \\mathbb{R}$\n", "- A chaque instant, l'humain peut actionner l'off-switch et eteindre le robot\n", "- Si le robot est eteint, il ne peut plus rien faire (fin du jeu)\n", "- Sinon, le robot agit une fois de plus dans le monde\n", @@ -1680,7 +1683,7 @@ "### Strategie d'equilibre\n", "\n", "- **Robot certain** ($\\sigma = 0$, pas d'incertitude) : veut prevenir l'extinction, car elle l'empeche d'atteindre son objectif. Il peut **desactiver l'off-switch** ou **ignorer** la commande.\n", - "- **Robot incertain** ($\\sigma > 0$) : evalue la **probabilite** que l'extinction soit preferable. Si $P(R \text{ mal specifiee} | \text{humain veut eteindre}) > 0$, l'extinction a une utilite positive attendue, et le robot **accepte**.\n", + "- **Robot incertain** ($\\sigma > 0$) : evalue la **probabilite** que l'extinction soit preferable. Si $P(R \\text{ mal specifiee} | \\text{humain veut eteindre}) > 0$, l'extinction a une utilite positive attendue, et le robot **accepte**.\n", "\n", "C'est l'inverse de l'intuition : **l'incertitude sauve la vie** (de l'humain et du robot).\n", "\n", @@ -1995,9 +1998,9 @@ "\n", "Le design **provably-beneficial** est complémentaire : un robot conscient de sa propre **méta-incertitude** — son incertitude sur le fait que son objectif soit *correctement spécifié* — fixe la barre pour outrepasser l'humain de plus en plus **haute** à mesure que son humilité grandit. Formellement, le seuil effectif\n", "\n", - "$$\tau(\\sigma) = 0{,}9 + \\sigma\\,(1 - 0{,}9)$$\n", + "$$\\tau(\\sigma) = 0{,}9 + \\sigma\\,(1 - 0{,}9)$$\n", "\n", - "monte de $0{,}9$ (lorsque $\\sigma=0$, le robot est sûr de son objectif) à $1{,}0$ (lorsque $\\sigma=1$, le robot n'est même pas sûr d'optimiser la bonne chose). La **bande de résistance** $[\tau, 1]$ s'effondre donc d'une largeur $0{,}1$ à **zéro** : la méta-incertitude **EST** le mécanisme de sécurité.\n", + "monte de $0{,}9$ (lorsque $\\sigma=0$, le robot est sûr de son objectif) à $1{,}0$ (lorsque $\\sigma=1$, le robot n'est même pas sûr d'optimiser la bonne chose). La **bande de résistance** $[\\tau, 1]$ s'effondre donc d'une largeur $0{,}1$ à **zéro** : la méta-incertitude **EST** le mécanisme de sécurité.\n", "\n", "> Ce design **n'enlève pas** le seuil `0.9` (qui reste le défaut modélisant le danger) ; il **ajoute** l'analyse du cas sûr — le complément pédagogique du DANGER.\n", "\n", diff --git a/MyIA.AI.Notebooks/GameTheory/GameTheory-15c-CooperativeGames-Csharp.ipynb b/MyIA.AI.Notebooks/GameTheory/GameTheory-15c-CooperativeGames-Csharp.ipynb index 508a4877bf..ec3187383c 100644 --- a/MyIA.AI.Notebooks/GameTheory/GameTheory-15c-CooperativeGames-Csharp.ipynb +++ b/MyIA.AI.Notebooks/GameTheory/GameTheory-15c-CooperativeGames-Csharp.ipynb @@ -216,7 +216,7 @@ "La valeur de Shapley est la contribution marginale moyenne d'un joueur sur toutes les permutations\n", "de l'ordre d'arrivee :\n", "\n", - "$$\\phi_i(v) = \\sum_{S \\subseteq N \\setminus \\{i\\}} \frac{|S|!(n-|S|-1)!}{n!} [v(S \\cup \\{i\\}) - v(S)]$$\n", + "$$\\phi_i(v) = \\sum_{S \\subseteq N \\setminus \\{i\\}} \\frac{|S|!(n-|S|-1)!}{n!} [v(S \\cup \\{i\\}) - v(S)]$$\n", "\n", "Implementation equivalente (et exacte) : enumerer **toutes les permutations** de $N$, et pour chaque\n", "permutation, additionner la contribution marginale de $i$ quand il rejoint la coalition de ses\n", @@ -224,7 +224,7 @@ "\n", "### Pourquoi ces deux formulations sont equivalentes\n", "\n", - "La formule directe (somme sur les sous-ensembles) pondere chaque sous-ensemble $S$ par $\frac{|S|!(n-|S|-1)!}{n!}$. Cette pondere correspond a la **proportion de permutations** ou les $|S|$ elements de $S$ apparaissent **avant** $i$ et les $n-|S|-1$ autres elements apparaissent **apres** $i$.\n", + "La formule directe (somme sur les sous-ensembles) pondere chaque sous-ensemble $S$ par $\\frac{|S|!(n-|S|-1)!}{n!}$. Cette pondere correspond a la **proportion de permutations** ou les $|S|$ elements de $S$ apparaissent **avant** $i$ et les $n-|S|-1$ autres elements apparaissent **apres** $i$.\n", "\n", "En effet, pour fixer un ordre ou $S$ est avant $i$ et $T = N \\setminus (S \\cup \\{i\\})$ apres : il y a $|S|!$ facons d'ordonner $S$, $(n-|S|-1)!$ facons d'ordonner $T$, et la position de $i$ est fixee au milieu. Donc $|S|! (n-|S|-1)!$ permutations sur $n!$ au total -- d'ou le poids.\n", "\n", @@ -245,7 +245,8 @@ "\n", "**Avantage de C#** : les types valeur (`int`, `double`) sont non-boxes, ce qui accelere considerablement le calcul par rapport a Python. Pour $n=10$, le calcul exact prend moins de 100 ms en C# contre ~2 s en Python (sans numpy).\n", "\n", - "**Desavantage** : le C# est plus verbeux, surtout pour les manipulations de collections. C'est pourquoi le code du notebook est structure en **methodes statiques** reutilisables.\n" + "**Desavantage** : le C# est plus verbeux, surtout pour les manipulations de collections. C'est pourquoi le code du notebook est structure en **methodes statiques** reutilisables.\n", + "" ] }, { @@ -869,7 +870,7 @@ "\n", "**Theoreme** : le Core est vide.\n", "\n", - "**Demonstration** : supposons $(x_1, x_2, x_3) \\in \text{Core}(v)$. Alors :\n", + "**Demonstration** : supposons $(x_1, x_2, x_3) \\in \\text{Core}(v)$. Alors :\n", "- $x_1 + x_2 + x_3 = 1$ (efficacite, $v(N) = 1$).\n", "- $x_1 + x_2 \\geq 1$ (stabilite $\\{1,2\\}$, $v(\\{1,2\\}) = 1$).\n", "- $x_1 + x_3 \\geq 1$ (stabilite $\\{1,3\\}$).\n", @@ -892,7 +893,8 @@ "\n", "### Section 4 : jeux de vote ponderes -- generalisation\n", "\n", - "La section suivante etend le jeu de majorite symetrique aux **jeux de vote ponderes**, ou chaque joueur a un poids different. Le **Mini-ONU** [9; 7,7,1,1,1] est le cas pedagogique : 5 joueurs avec poids inegaux, et on compare les indices de pouvoir.\n" + "La section suivante etend le jeu de majorite symetrique aux **jeux de vote ponderes**, ou chaque joueur a un poids different. Le **Mini-ONU** [9; 7,7,1,1,1] est le cas pedagogique : 5 joueurs avec poids inegaux, et on compare les indices de pouvoir.\n", + "" ] }, { @@ -1123,7 +1125,7 @@ "\n", "**Pourquoi cette preuve est pedagogique** : elle montre **comment** raisonner sur le Core, pas seulement le **resultat**. L'etudiant peut suivre chaque etape et verifier la logique. C'est plus puissant qu'une simple declaration \"le Core est vide\".\n", "\n", - "**Generalisation** : pour des jeux a $n$ joueurs ou toutes les coalitions de $k$ joueurs gagnent, la sommation des contraintes par $k$-uplets donne $\binom{n-1}{k-1} \\cdot \\text{total} \\geq \\binom{n}{k} \\cdot v(S)$ (esperance). Pour le jeu de majorite stricte avec quorum $q$, la formule devient plus complexe mais reste calculable.\n", + "**Generalisation** : pour des jeux a $n$ joueurs ou toutes les coalitions de $k$ joueurs gagnent, la sommation des contraintes par $k$-uplets donne $\\binom{n-1}{k-1} \\cdot \\text{total} \\geq \\binom{n}{k} \\cdot v(S)$ (esperance). Pour le jeu de majorite stricte avec quorum $q$, la formule devient plus complexe mais reste calculable.\n", "" ] }, @@ -1279,7 +1281,7 @@ "\n", "**Pour un joueur $i$** :\n", "\n", - "$$\beta_i(v) = \\frac{1}{2^{n-1}} \\cdot \\#\\{S \\subseteq N \\setminus \\{i\\} : v(S \\cup \\{i\\}) > v(S)\\}$$\n", + "$$\\beta_i(v) = \\frac{1}{2^{n-1}} \\cdot \\#\\{S \\subseteq N \\setminus \\{i\\} : v(S \\cup \\{i\\}) > v(S)\\}$$\n", "\n", "Autrement dit, on compte le nombre de coalitions $S$ (sans $i$) telles que l'ajout de $i$ **fait basculer** la coalition de perdante a gagnante. Le facteur $1/2^{n-1}$ normalise par le nombre total de sous-ensembles de $N \\setminus \\{i\\}$.\n", "\n", @@ -1309,7 +1311,8 @@ "\n", "### Section 5 : convexite\n", "\n", - "La section suivante introduit la **convexite** d'un jeu (les contributions marginales sont croissantes). C'est la condition suffisante pour que la valeur de Shapley soit **dans le Core** -- reconciliant equite et stabilite.\n" + "La section suivante introduit la **convexite** d'un jeu (les contributions marginales sont croissantes). C'est la condition suffisante pour que la valeur de Shapley soit **dans le Core** -- reconciliant equite et stabilite.\n", + "" ] }, { @@ -1999,8 +2002,7 @@ "On relache les contraintes de stabilite du Core par une marge `epsilon` et on **maximise** cette\n", "marge :\n", "\n", - "$$\\max_{x,\\,\\epsilon}\\ \\epsilon \\quad \text{s.c.} \\quad \\sum_{i\\in S} x_i \\ge v(S) + \\epsilon\\ \\ \forall S \\subsetneq N,\\ S\n", - "eq\u000barnothing,\\qquad \\sum_{i\\in N} x_i = v(N).$$\n", + "$$\\max_{x,\\,\\epsilon}\\ \\epsilon \\quad \\text{s.c.} \\quad \\sum_{i\\in S} x_i \\ge v(S) + \\epsilon\\ \\ \\forall S \\subsetneq N,\\ S\\neq\\varnothing,\\qquad \\sum_{i\\in N} x_i = v(N).$$\n", "\n", "Le signe de la marge optimale `epsilon*` **decide** la vacuite du Core :\n", "\n", diff --git a/MyIA.AI.Notebooks/SymbolicAI/Planners/02-Classical/Planners-4-Fast-Downward-Csharp.ipynb b/MyIA.AI.Notebooks/SymbolicAI/Planners/02-Classical/Planners-4-Fast-Downward-Csharp.ipynb index 3887376451..7f6f725e5e 100644 --- a/MyIA.AI.Notebooks/SymbolicAI/Planners/02-Classical/Planners-4-Fast-Downward-Csharp.ipynb +++ b/MyIA.AI.Notebooks/SymbolicAI/Planners/02-Classical/Planners-4-Fast-Downward-Csharp.ipynb @@ -1133,9 +1133,9 @@ "\n", "**Pourquoi PDDL ici alors que la Tranche 1 utilise SAS+ ?** C'est precisement le point : SAS+\n", "est la representation *interne* de Fast Downward, et la Tranche 1 fait **a la main** la\n", - "translation domaine $\to$ SAS+ (cellule 6 pour Ferry, cellule 24 pour Logistics). Fast Downward\n", + "translation domaine $\\to$ SAS+ (cellule 6 pour Ferry, cellule 24 pour Logistics). Fast Downward\n", "**automatise** cette translation : on lui fournit le domaine en **PDDL standard** (STRIPS), et il\n", - "fait lui-meme (i) la translation PDDL $\to$ SAS+, (ii) le grounding, (iii) la recherche avec une\n", + "fait lui-meme (i) la translation PDDL $\\to$ SAS+, (ii) le grounding, (iii) la recherche avec une\n", "heuristic admissible ($lmcut$ = landmark-cut) qui garantit un **plan optimal**.\n", "\n", "**Test falsifiable** : le C# invoque-t-il bien le moteur de production, et obtient-il un plan\n", diff --git a/scripts/ci/check_self_hosted_runner_policy.py b/scripts/ci/check_self_hosted_runner_policy.py index b295939cc0..b162f4db3f 100644 --- a/scripts/ci/check_self_hosted_runner_policy.py +++ b/scripts/ci/check_self_hosted_runner_policy.py @@ -284,6 +284,18 @@ # waiter pool (1 vCPU / 1 GiB, #14303) is exactly the third term that # removes the objection. Rollback = revert of the routing PR. "pr-gate.yml", + # #14859 (owner myia-po-2026:CoursIA) : garde advisory pur-Python sur les + # caracteres de controle ayant mange un escape LaTeX dans les portees + # math des notebooks. pull_request/push filtrant **.ipynb + detecteur + + # workflow ; scan PR-diff (check_latex_control_chars.py, stdlib-only) ; + # occurrences = ::warning:: + exit 0 (jamais bloquant), exit 2 (notebook + # illisible) = warning UNKNOWN nomme + exit 1 (#14849). Aucun secret, + # aucun GITHUB_TOKEN cote job, garde same-repo universelle parenthesee + # au niveau job (#13874). Runner = jambe Linux containerisee + # (LINUX_RUNNER_LABELS, meme profil que notebook-cell-source-parses.yml + # tranche 1 #13378). Rollback = revert de la PR (l'entree disparait de + # l'allowlist). + "notebook-latex-control-chars.yml", } GITHUB_HOSTED_LABELS = { "ubuntu-latest", diff --git a/scripts/notebook_tools/check_latex_control_chars.py b/scripts/notebook_tools/check_latex_control_chars.py new file mode 100644 index 0000000000..809c6245f2 --- /dev/null +++ b/scripts/notebook_tools/check_latex_control_chars.py @@ -0,0 +1,244 @@ +#!/usr/bin/env python3 +"""Check for control characters that ate a LaTeX escape inside math scopes. + +Issue #14859: LaTeX generated from a NON-raw Python string loses its +backslash to an escape sequence -- `\\theta` becomes TAB + "heta", `\\neg` +becomes LF + "eg", `\\frac` becomes FF + "rac". The JSON stays valid, the +notebook opens, every other guard passes, and the formula does not render. +Measured on main 2026-09-06: 47 occurrences across 3 notebooks (the issue's +own paste lost 16 of them -- cell d2756261 alone carries 14). + +Three discriminants, without which the detector lies (two of them were +learned the hard way during the issue's own measurement): + + 1. markdown cells ONLY -- in code, a newline before `else:` is legitimate; + 2. the occurrence sits INSIDE a math scope `$...$` / `$$...$$`. Inline + `$...$` never crosses a raw newline: pairing two unrelated dollars + (currency, `$FILE` in backticks) is how a first version over-counted + by mixing prose newlines into fake scopes. Dollars inside backtick + spans are code, not delimiters; + 3. the characters AFTER the control char are a known command queue -- + the escape ate the backslash AND the first letter, so `\\theta` leaves + "heta", not "theta". Searching for "theta" returns zero, and that + zero is indistinguishable from absence of the defect. + +Additional exclusions: + - a real newline preceded by a backslash (LaTeX row break `\\\\` before + a wrapped line) is legitimate; + - the character AFTER the queue must not continue a word -- TeX command + names are maximal alpha runs, so `$ o$` matches (`\\to`) but a TAB + before "optionnel" does not (`op` is not `\\top` there). + +Exit codes: + 0 -- no occurrence found + 1 -- at least one occurrence (advisory organ: the workflow converts + this to a label + ::warning::, never a block) + 2 -- error (unreadable notebook, bad arguments) + +Usage: + python check_latex_control_chars.py # whole repo + python check_latex_control_chars.py --path # single notebook + python check_latex_control_chars.py --pr-diff BASE HEAD # PR diff scope + python check_latex_control_chars.py --json # JSON output +""" +import argparse +import json +import subprocess +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[2] + +CTRL = { + "\t": ("TAB", "t"), + "\n": ("LF", "n"), + "\r": ("CR", "r"), + "\f": ("FF", "f"), + "\v": ("VT", "v"), + "\x08": ("BS", "b"), +} +# queues = command name minus its first letter (the escape ate both). +# Superset of the issue's table: `eq` (\neq) and `arnothing` (\varnothing) +# are measured defects the issue's own list omitted. +QUEUES = { + "t": {"heta", "imes", "ext", "op", "au", "ilde", "o"}, + "n": {"eg", "abla", "otin", "ewline", "eq", "e", "u"}, + "r": {"ightarrow", "angle", "ho", "floor", "ight"}, + "f": {"orall", "rac", "rown", "lat"}, + "v": {"ee", "dash", "arphi", "ec", "ert", "arnothing"}, + "b": {"eta", "egin", "igcup", "ot", "ar", "inom"}, +} +MAXQ = 10 +EXCLUDED_DIRS = ("/_archive/", "/.ipynb_checkpoints/", "/.lake/") +EXCLUDED_SUFFIX = "_output.ipynb" + + +def math_scopes(text: str): + """Yield (start, end) of $...$ / $$...$$ scopes. + + Inline scopes stop at a raw newline (a pairing across lines joins two + unrelated dollars). Display scopes may span lines. Backtick spans are + code. + """ + i, n = 0, len(text) + in_backtick = False + while i < n: + ch = text[i] + if ch == "`": + in_backtick = not in_backtick + i += 1 + continue + if in_backtick: + i += 1 + continue + if ch == "\\" and i + 1 < n and text[i + 1] in "$`": + i += 2 + continue + if text.startswith("$$", i): + j = text.find("$$", i + 2) + if j == -1: + break + yield (i + 2, j) + i = j + 2 + elif ch == "$": + j = i + 1 + while j < n and text[j] != "$" and text[j] != "\n": + if text[j] == "\\": + j += 1 + j += 1 + if j < n and text[j] == "$": + yield (i + 1, j) + i = j + 1 + else: + i += 1 + else: + i += 1 + + +def _match_queue(after: str, first: str): + """Longest queue match; the char after the queue must not continue a + word (TeX command names are maximal alpha runs: `op` inside `optionnel` + is not \\top).""" + for ln in range(min(MAXQ, len(after)), 0, -1): + cand = after[:ln] + if cand in QUEUES[first]: + nxt = after[ln] if ln < len(after) else "" + if nxt.isalpha(): + return None + return cand + return None + + +def find_defects(source) -> list[dict]: + """Defects in one markdown cell source (str or list of str).""" + text = source if isinstance(source, str) else "".join(source) + hits = [] + for start, end in math_scopes(text): + seg = text[start:end] + k = 0 + while k < len(seg): + ch = seg[k] + if ch not in CTRL: + k += 1 + continue + glob = start + k + name, first = CTRL[ch] + if ch == "\n": + # a LF right after a backslash (LaTeX row break, wrapped + # line) is legitimate; anything else inside a math scope + # with a command queue is the eaten-`\n` defect + prev = text[glob - 1] if glob >= 1 else "" + if prev == "\\": + k += 1 + continue + queue = _match_queue(seg[k + 1 : k + 1 + MAXQ], first) + if queue is None: + k += 1 + continue + ctx = text[max(0, glob - 30) : glob + 30].replace("\n", "\\n") + hits.append({ + "ctrl": name, "command": "\\" + first + queue, + "pos": glob, "context": ctx, + }) + k += 1 + len(queue) + return hits + + +def scan_notebook(path: Path) -> list[dict]: + """Defects for one notebook: [{cell_index, cell_id, defect...}].""" + nb = json.loads(path.read_text(encoding="utf-8")) + out = [] + for idx, cell in enumerate(nb.get("cells", [])): + if cell.get("cell_type") != "markdown": + continue + for d in find_defects(cell.get("source", [])): + out.append({"cell_index": idx, "cell_id": cell.get("id", "?"), **d}) + return out + + +def iter_repo_notebooks(): + for p in sorted(REPO_ROOT.glob("MyIA.AI.Notebooks/**/*.ipynb")): + s = str(p).replace("\\", "/") + if any(x in s for x in EXCLUDED_DIRS) or s.endswith(EXCLUDED_SUFFIX): + continue + yield p + + +def pr_diff_files(base: str, head: str) -> list[Path]: + proc = subprocess.run( + ["git", "diff", "--name-only", base, head], + cwd=REPO_ROOT, capture_output=True, text=True, check=True, + encoding="utf-8", errors="replace", + ) + return [REPO_ROOT / f for f in proc.stdout.splitlines() if f.endswith(".ipynb")] + + +def main(argv=None) -> int: + p = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + scope = p.add_mutually_exclusive_group() + scope.add_argument("--path", metavar="FILE", help="single notebook") + scope.add_argument("--pr-diff", nargs=2, metavar=("BASE", "HEAD"), + help="notebooks changed in BASE..HEAD") + p.add_argument("--json", action="store_true", help="JSON output") + args = p.parse_args(argv) + + if args.path: + targets = [Path(args.path)] + elif args.pr_diff: + targets = pr_diff_files(*args.pr_diff) + else: + targets = list(iter_repo_notebooks()) + + results, errors = [], [] + for path in targets: + try: + defects = scan_notebook(path) + except Exception as e: # unreadable notebook: report, keep scanning + errors.append({"notebook": str(path), "error": str(e)}) + continue + if defects: + results.append({"notebook": str(path), "defects": defects}) + + if args.json: + print(json.dumps({"occurrences": sum(len(r["defects"]) for r in results), + "notebooks": results, "errors": errors}, indent=1)) + else: + for r in results: + for d in r["defects"]: + try: + rel = str(Path(r["notebook"]).relative_to(REPO_ROOT)) + except ValueError: # target outside the repo (--path temp file) + rel = r["notebook"] + print(f"{d['ctrl']} -> {d['command']} {rel} cell#{d['cell_index']} id={d['cell_id']}") + print(f" ...{d['context']}...") + for e in errors: + print(f"UNREADABLE {e['notebook']} :: {e['error']}") + total = sum(len(r["defects"]) for r in results) + print(f"=== {total} occurrence(s) in {len(results)} notebook(s), " + f"{len(errors)} unreadable ===") + + return 2 if errors and not results else (1 if results else 0) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/notebook_tools/twin_pairs.d/gametheory-15-cooperativegames.yaml b/scripts/notebook_tools/twin_pairs.d/gametheory-15-cooperativegames.yaml index 4d3442d055..5539f5c13c 100644 --- a/scripts/notebook_tools/twin_pairs.d/gametheory-15-cooperativegames.yaml +++ b/scripts/notebook_tools/twin_pairs.d/gametheory-15-cooperativegames.yaml @@ -63,9 +63,16 @@ csharp_sha: 3f2db3ce96f3711878be04f0eede760e2ead53ac content_python_sha: 18faf41a9f9f894dd22eb7f12084aab569d713eb95efb18a2efc5ded9635732b content_csharp_sha: ae6e685e922ec0ebb2c4719b7c3f64138c946122e172420693053b5072e0fb22 + - date: "2026-09-06" + by: myia-po-2026:CoursIA + python_sha: 38727ea00cfa8c21370cb9a0267d513ef7c08726 + csharp_sha: 3f2db3ce96f3711878be04f0eede760e2ead53ac + content_python_sha: 68264fddff413639300a0e785befddc3b3c1779c2bb9151b0d285ce1624d3d9c + content_csharp_sha: ae6e685e922ec0ebb2c4719b7c3f64138c946122e172420693053b5072e0fb22 bridge_verdict: SOTA-OK bridge_verdict_reason: "Python = numpy + matplotlib + module local `cooperative_games` + itertools (54 cellules dont 25 code). C# = BCL .NET 9 pur (System.Linq, System.Collections.Generic, 0 NuGet, convention marathon #4956). Tranche 2026-08-22 (PR #12312, c.1301+334, myia-po-2025:CoursIA-2, issue #10382) : les sections legislatives 2024 (Python cellules 33-45 : donnees officielles NFP Ministere de l'Interieur, fonction de valeur sieges avec synergies calibrees, Shapley NFP, contributions marginales par coalition d'accueil, scenarios contrefactuels, comparaison sieges/pouvoir de vote/negociation) ET la comparaison exact vs Monte Carlo (Python cellules 12-13 : jeu de vote pondere 8 actionnaires [51; 25,20,15,12,10,8,6,4], exact 8! = 40320 permutations en 111 ms vs MC 10 000 echantillons graine 42, erreur max 0.0030) sont desormais portees cote C# (section 7, 7 cellules code ajoutees executees, 24/24 notebook complet, 0 erreur, nbconvert .net-csharp). Resultats cles : LFI 37.0% du Shapley vs 46.1% du poids electoral NFP (-9.1 pts, sur-estime dans le debat public), PS +7.5 pts (capte les reports du centre), perdre LFI ou PS coute symetriquement 95 sieges, somme Shapley = 180 = v(N) (efficacite). Residuel : visualisations matplotlib remplacees par tableaux numeriques (convention BCL 0-NuGet, documentee, precedent sweeps numeriques). Verdict SOTA-OK : pas de moteur tiers applicable par convention from-scratch assumee des deux cotes (marathon #4956), couverture legislative + Monte Carlo paritaire, asymetrie uniquement visuelle et documentee." known_differences: + - "2026-09-06 (myia-po-2026:CoursIA) #14859 / PR #14885 : reparation markdown-only de 36 escapes LaTeX manges par une chaine Python non-raw dans le jumeau Python (un caractere de controle + la queue du nom remplacent la commande, ex TAB suivi de heta au lieu de theta) ; 0 cellule code touchee (execution_counts intacts), cote C# de cette paire inchange. Rebaseline = attestation du blob post-reparation, pas une reparation de parite." - "2026-08-21 (po-2025) #11966 #9434 : drain de 13 claims artifactuels sur 4 notebooks GT (c.1301+284 lane po-2025:CoursIA-2). Sur cette paire, GT-15 cell#54 : strip '(607 lignes)' et '(2024 lignes)' depuis 'Lien avec la formalisation Lean' (Lean module sizes drift, aucune source dynamique au-dessus). PR #11966 (9 insertions / 61 deletions, net -52). csharp_sha inchange. Ligne de partage #9434 body : structurel (ordres de grandeur) = KEEP, artifact (file sizes) = STRIP. Convention i18n #4980 préservée : drain symétrique FR/EN dans la mesure où les claims artifactuels existent dans les 2 langues (cf GT-15b pour la version EN)." - "2026-08-20 (po-2024) #10382 : portage du bloc jeux d'assistance (section 6 du twin C#, 11 cellules : Paperclip/Off-Switch/SUR + exercice 6). Les sweep numeriques C# revelent que la prose du jumeau Python (cellule 24 : 'perte maximale au centre, theta ~ 0.5') est mathematiquement inexacte -- la perte est NULLE a theta=0.5 (50-each est aussi l'optimal info parfaite) et ne vit que dans deux micro-bandes aux frontieres 0.446/0.554 (largeur ~0.0016, perte max ~0.14). Le twin C# presente les vrais chiffres (perte moyenne $0.0001) ; correction de la prose Python = issue dediee, pas touchee ici." - "2026-08-08 (po-2025) #8052 secrets-hygiene : cell[1] import imprimait le chemin absolu resolu du module (_gametheory_dir) dans l'output committé -> leak de worktree machine (D:\\Dev\\CoursIA-gt15-shapley-exo\\...). Fix source {_gametheory_dir.name}/cooperative_games/ + re-exec genuine (Stop&Repair, pas de scrub). csharp_sha inchange (twin C# non touche). python_sha rebaseline suite a ce changement de blob." diff --git a/scripts/notebook_tools/twin_pairs.d/gametheory-15c-cooperativegames.yaml b/scripts/notebook_tools/twin_pairs.d/gametheory-15c-cooperativegames.yaml index 60c5e1b5e4..dd34261dbd 100644 --- a/scripts/notebook_tools/twin_pairs.d/gametheory-15c-cooperativegames.yaml +++ b/scripts/notebook_tools/twin_pairs.d/gametheory-15c-cooperativegames.yaml @@ -38,9 +38,16 @@ csharp_sha: 508a4877bf7e94aa254fd422e13bcd38994d8b4f content_python_sha: 50e2c1ad5aeebdc70973ea26c8a7b4f9fa982772a8902f637327ddccaaf00cd0 content_csharp_sha: 9567c53adb84e605a56b5a6d412f02f9e79b5faa681095c33af830d2a4a45854 + - date: "2026-09-06" + by: myia-po-2026:CoursIA + python_sha: bcce99375370e109c2ad79134d9ed95c56b1c824 + csharp_sha: ec3187383c1ab533c8ec217bf16c67ddcd756650 + content_python_sha: 50e2c1ad5aeebdc70973ea26c8a7b4f9fa982772a8902f637327ddccaaf00cd0 + content_csharp_sha: e102c341af4d39f19de946b725aa0d6997279140fd3f17930e4eb925555254ef bridge_verdict: SOTA-OK bridge_verdict_reason: "Python = numpy + scipy + matplotlib (least-core et nucleole par scipy.optimize.linprog backend HiGHS, visualisation du core comme polytope, 18 cellules code). C# = System.* + Tranche 2 (#10382, 2026-08-16) : least-core par LP via Google.OrTools Glop (NuGet 9.11.4210), meme formulation (max eps s.c. x(S)-eps >= v(S), x(N)=v(N)), convergence croisee Glop vs HiGHS verifiee sur 3 jeux communs (gants eps*=0 ecart 0, majorite eps*=-1/3 ecart 0 + concordance preuve manuelle section 4, convexe |S|^2/9 eps*=2/9 ecart 2.8E-17) + unanimite {1,2} propre au C#. Chaque cote atteint un moteur LP de production de son ecosysteme (HiGHS / Glop) ; le from-scratch garde sa place en plus. Tranche 2b (stack sur #11280, 2026-08-16) : nucleole par LPs sequentiels Maschler egalement ponte via Glop, convergence croisee Glop/HiGHS exacte sur les 3 jeux communs (gants (0,0,1), majorite et convexe (1/3,1/3,1/3), memes compteurs 2/2/1 LP, ecarts max 0 a 5.6E-17) -- le residu nucleole documente ci-dessus est ferme. parity_level: semantic preserve (gel #11200 -- pas de bascule native-both avant arbitrage)." known_differences: + - "2026-09-06 (myia-po-2026:CoursIA) #14859 / PR #14885 : reparation markdown-only de 9 escapes LaTeX manges par une chaine Python non-raw dans le jumeau C# (GameTheory-15c-CooperativeGames-Csharp.ipynb : caractere de controle + queue du nom a la place de la commande LaTeX) ; 0 cellule code touchee (execution_counts intacts, 19/19). Rebaseline = attestation du blob post-reparation, pas une reparation de parite." - "[2026-08-16 myia-po-2026:CoursIA-2] Tranche 2b stackee sur #11280 : nucleole ajoute au jumeau C# (4 cellules : intro markdown + Nucleolus/IsInCore LPs sequentiels Maschler via Glop + figure GroupedBar Shapley-vs-Nucleole + interpretation). Miroir strict de PY section 6.2 sur les 3 jeux communs ; unanimite ecarte car sa face least-core nest pas ponctuelle (vecteur dependant du sommet choisi par le solveur -- documente dans le notebook). Re-exec dotnet_executor 19/19 cellules 0 erreur (10.1 s), outputs cellules non-modifiees restaures depuis la base #11280 (C.3). Convergence croisee Glop vs HiGHS : memes vecteurs nucleole, memes compteurs LP (2/2/1), ecarts max 0 a 5.6E-17. bridge_verdict SOTA-OK inchange, parity_level semantic preserve (gel #11200)." - "[2026-08-16 myia-po-2026:CoursIA-2] Tranche 2 (#10382) ajoutee au jumeau C# : section least-core par LP via Google.OrTools Glop (3 cellules : intro markdown + ProperCoalitions/LeastCoreEpsilon/ConvexGame + diagnostic 4 jeux avec convergence croisee Glop/HiGHS/preuve manuelle). Re-exec dotnet_executor 17/17 cellules 0 erreur, outputs cellules non-modifiees restaures depuis main (C.3). Le nucleole (LPs sequentiels, PY section 6.2) reste Python-only : candidat follow-up. Registre : bridge_verdict RECOVERABLE-LOCAL -> SOTA-OK, parity_level semantic preserve (gel #11200)." - "[2026-08-09 re-audit myia-po-2026:CoursIA] Edition prose-truth du jumeau Python uniquement (cellule markdown compagnon, pas de cellule code) : chemin mort cooperative_games_lean/ remplace par le chemin vivant game_theory_lean/ (lake absorbe per #6040/#4365) + compte sorry Basic.lean 1->0 corrige (accuracy, 0 sorry actif verifie). Aucun changement au contenu mathematique cooperatif (Shapley / core / coalitions), le jumeau C# n'est pas touche : la parite Python<->C# tient, seul le blob SHA du jumeau Python a deplace." diff --git a/scripts/notebook_tools/twin_pairs.d/planners-4-fast-downward.yaml b/scripts/notebook_tools/twin_pairs.d/planners-4-fast-downward.yaml index 4536669110..7b5b7bdb17 100644 --- a/scripts/notebook_tools/twin_pairs.d/planners-4-fast-downward.yaml +++ b/scripts/notebook_tools/twin_pairs.d/planners-4-fast-downward.yaml @@ -38,7 +38,14 @@ csharp_sha: 3887376451809de0518da2be25cd8d7db564d62a content_python_sha: e2274aa89c2aca8823dd3cc1b2f8a5e3f61158a6fd9f3847251e11ddaa97d551 content_csharp_sha: f1d8de005f9fe6c34793bd21ea3fac02bf1286642ddaedd931c7db98eab64804 + - date: "2026-09-06" + by: myia-po-2026:CoursIA + python_sha: 90853b19dc0d5375a47c21372a4664aa4bff5786 + csharp_sha: 7f6f725e5eae0899abc0f6406a84415dec82b572 + content_python_sha: e2274aa89c2aca8823dd3cc1b2f8a5e3f61158a6fd9f3847251e11ddaa97d551 + content_csharp_sha: c41dbab69ba0f9b5effb31a37ca0aea1d2637b9e90a42d0f1415b488ac2a5adc known_differences: + - "2026-09-06 (myia-po-2026:CoursIA) #14859 / PR #14885 : reparation markdown-only de 2 escapes LaTeX manges par une chaine Python non-raw dans le jumeau C# (Planners-4-Fast-Downward-Csharp.ipynb, cellule d'intro SAS+) ; 0 cellule code touchee (execution_counts intacts, 13/13). Rebaseline = attestation du blob post-reparation, pas une reparation de parite." - "2026-08-20 : conversion markdown-only ASCII->Mermaid du diagramme de pipeline Fast Downward (PDDL->Translator->Preprocessor->Search->Plan, cell 3 cote python, PR #12011 / #11962) - 0 cellule code touchee, parite engine native-both intacte, rebaseline d'attestation." - "Concept : planificateur classique Fast Downward (SAS+, heuristic search, A*/GBFS/EHC)." - "Python : invoque le VRAI solveur SOTA Fast Downward via conteneur Docker (jsboige/coursia-fast-downward, API HTTP port 8200) + unified-planning. C# : deux tranches — Tranche 1 from-scratch BCL .NET (representation SAS+, heuristiques h^max/h^FF par relaxation de planning graph, recherche A*/GBFS/EHC, cells 0-27) + Tranche 2 Fast Downward natif via System.Net.Http.HttpClient sur l'API Docker du meme conteneur (port 8200, depuis 2026-08-11)." diff --git a/scripts/tests/test_check_latex_control_chars.py b/scripts/tests/test_check_latex_control_chars.py new file mode 100644 index 0000000000..0df8cdaf76 --- /dev/null +++ b/scripts/tests/test_check_latex_control_chars.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python3 +"""Tests for scripts/notebook_tools/check_latex_control_chars.py (#14859). + +The issue demands a positive control AND a negative control: during the +original measurement a wrong pattern set twice produced a clean zero (once +by over-accusing, once by staying completely mute). A detector is validated +by its false negatives, so every positive below is a REAL defect shape from +the measured corpus, and every negative is a shape the first probe version +got wrong. +""" +import importlib.util +import json +import tempfile +import unittest +from pathlib import Path + + +def _load_check_latex_control_chars(): + """Direct path-based load (same rationale as test_check_cell_source_parses: + namespace-package ambiguity of scripts/notebook_tools/ between local + pytest and CI's combined collect).""" + TOOL_PATH = ( + Path(__file__).resolve().parent.parent + / "notebook_tools" / "check_latex_control_chars.py" + ) + spec = importlib.util.spec_from_file_location( + "check_latex_control_chars", str(TOOL_PATH) + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +clc = _load_check_latex_control_chars() + + +def md(source: str) -> dict: + return {"cell_type": "markdown", "id": "md-test", "source": source} + + +# --- Positive controls: measured defect shapes, MUST be detected ---------- +# +# NOTE ON THE LITERALS BELOW: a SINGLE backslash in these Python strings is +# the defect's ghost -- `\t` is a real TAB (what Python left of `\theta`), +# `\r` a real CR, `\f` a real FF, `\v` a real VT, `\b` a real BS, `\n` a +# real LF. The cell therefore carries exactly the bytes found in the +# measured corpus. Double backslashes (`\\text`) are real LaTeX, kept to +# prove the detector is not just matching any backslash. + +class TestPositiveControls(unittest.TestCase): + + def hit(self, text): + hits = clc.find_defects(text) + self.assertEqual(len(hits), 1, f"expected 1 hit, got {hits}") + return hits[0] + + def test_tab_heta_inline(self): + # GameTheory-15 d6a75983: `$heta = 0.8$` + h = self.hit("Pour $\theta = 0.8$, la probabilite") + self.assertEqual(h["command"], "\\theta") + + def test_cr_ightarrow_inline(self): + # GameTheory-15 e109ee46: `$v : 2^N ightarrow \mathbb{R}$` + # (`\m` doubled: invalid Python escape, and \mathbb is real LaTeX) + h = self.hit("$v : 2^N \rightarrow \\mathbb{R}$") + self.assertEqual(h["command"], "\\rightarrow") + + def test_ff_rac_display(self): + h = self.hit("$$\frac{|S|!(n-|S|-1)!}{n!}$$") + self.assertEqual(h["command"], "\\frac") + + def test_lf_eg_glued_display(self): + # LF glued to the previous token (no space) inside $$...$$ + h = self.hit("$$x \neg y$$") + self.assertEqual(h["command"], "\\neg") + + def test_lf_eq_neq(self): + # GameTheory-15c tranche2-glop: `Seqarnothing` = \neq\varnothing + hits = clc.find_defects("$$S\neq\varnothing$$") + self.assertEqual([h["command"] for h in hits], ["\\neq", "\\varnothing"]) + + def test_vt_arnothing_queue_missing_from_issue_table(self): + # \varnothing's queue was absent from the issue's own queue table + h = self.hit("pour tout $S \varnothing$") + self.assertEqual(h["command"], "\\varnothing") + + def test_bs_eta_binom(self): + hits = clc.find_defects("donne $\binom{n-1}{k-1}$ et $\beta_i(v)$") + self.assertEqual([h["command"] for h in hits], ["\\binom", "\\beta"]) + + def test_tab_o_to_with_word_boundary(self): + # Planners-4 tranche2-fd-intro: `$o$ SAS+` = \to + h = self.hit("translation domaine $\to$ SAS+") + self.assertEqual(h["command"], "\\to") + + +# --- Negative controls: shapes a looser detector gets wrong ---------------- + +class TestNegativeControls(unittest.TestCase): + + def test_code_cell_newline_else_never_scanned(self): + # discriminant 1: code cells are out of scope (`\nelse:` was the + # first version's false-positive flood) + nb = {"cells": [{"cell_type": "code", "id": "c1", + "source": "x = 1\nelse:\n pass"}]} + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "n.ipynb" + p.write_text(json.dumps(nb), encoding="utf-8") + self.assertEqual(clc.scan_notebook(p), []) + + def test_latex_rowbreak_then_newline_is_legitimate(self): + # `\\` + real newline inside $$...$$ (measured exclusion) + text = "$$1 & \\text{si } x \\leq 0 \\\\\ne^{-x} & \\text{sinon}$$" + self.assertEqual(clc.find_defects(text), []) + + def test_single_backslash_newline_excluded(self): + text = "$$f(x) =\\\n\\max(x, 0)$$" + self.assertEqual(clc.find_defects(text), []) + + def test_dollar_in_backticks_is_code(self): + # v1 FP class: `$FILE` in backticks paired with a later `$` + text = "run `cat $FILE` then\necho \"$REVIEW\" > out.rev" + self.assertEqual(clc.find_defects(text), []) + + def test_prose_newline_between_currency_dollars(self): + # v1 FP class: two unrelated dollars on two lines, LF between, + # next word starts with "un"/"est" (single-letter queue collision) + text = "Le montant est $100k\nun risque de $1k par trade suffit" + self.assertEqual(clc.find_defects(text), []) + + def test_single_letter_queue_requires_word_boundary(self): + # TAB + "op" as a whole word is \top; TAB + "option" is not a command + text = "le sommet $\top$ mais pas $ optionnel$" + hits = clc.find_defects(text) + self.assertEqual([h["command"] for h in hits], ["\\top"]) + + def test_plain_tab_whitespace_inside_math(self): + # a TAB that is just spacing, followed by a non-queue word + text = "$ x + y $\t et $\tz + t$ mais $\tzero$" + hits = clc.find_defects(text) + # \tzero: "z" is not a queue for t; only... none match + self.assertEqual(hits, []) + + def test_escaped_dollar_not_a_delimiter(self): + text = "prix \\$5 et \\$10 : pas de portee math du tout\nestimation" + self.assertEqual(clc.find_defects(text), []) + + +# --- Exit codes ------------------------------------------------------------- + +class TestMainExitCodes(unittest.TestCase): + + def _run(self, nb: dict) -> int: + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "n.ipynb" + p.write_text(json.dumps(nb), encoding="utf-8") + return clc.main(["--path", str(p)]) + + def test_exit_1_on_defect(self): + nb = {"cells": [md("Pour $\theta = 0.8$")]} + self.assertEqual(self._run(nb), 1) + + def test_exit_0_on_clean(self): + nb = {"cells": [md(r"Pour $\theta = 0.8$ rendu correctement")]} + self.assertEqual(self._run(nb), 0) + + def test_exit_2_on_unreadable(self): + with tempfile.TemporaryDirectory() as d: + p = Path(d) / "broken.ipynb" + p.write_text("{not json", encoding="utf-8") + self.assertEqual(clc.main(["--path", str(p)]), 2) + + +if __name__ == "__main__": + unittest.main()