Spaces:
Runtime error
Runtime error
File size: 42,458 Bytes
77a0a72 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 | # Règles de pythonisation PyxiScience — Base de connaissances
> Document destiné à être injecté dans un prompt système Claude Code modifiant le générateur d'exercices pythonisés. Chaque règle est dérivée de bugs réels rencontrés en production sur les annales Bac (Métropole J2 18/06/2025, exos 2 à 4) et de leur correction validée par l'équipe contenu.
---
## 0. Objectif
Le générateur prend en entrée :
- un énoncé APMEP (PDF/LaTeX/image)
- une version PyxiScience non pythonisée déjà validée par Chabane
Et produit en sortie une version pythonisée MyST (`.md`) prête pour soumission à la plateforme.
Les règles ci-dessous codifient les invariants à respecter. Tout livrable qui les enfreint sera rejeté par l'audit.
---
## 1. Conventions PyxiScience (rappel)
Ces conventions sont préalables et non négociables. Le générateur les respecte par défaut.
- **Variables Python** : camelCase uniquement. Les underscores (`p_a`, `coef_t`) provoquent des erreurs de subscript LaTeX dans certains contextes.
- **Bloc Python** : ouvert avec **4 backticks** (vérifié sur 222/222 exemples livrés — constante `PYTHON_FENCE_BACKTICKS` dans `app/config.py`), terminé par `globals()` pour exposer les variables au moteur de rendu MyST (accès via `{{var}}`). Enveloppe `{exercise}` = 5 backticks.
- **Bilingue** : rôles inline `` {fr}`...`{en}`...` `` UNIQUEMENT (aucun bloc `:::{fr}`/`:::{en}` dans les 222 exemples livrés — ne pas en produire).
- **Math display** : `\begin{equation*}...\end{equation*}` avec `&` pour alignement. Ne JAMAIS utiliser `\[ \]` ni `$$ $$`.
- **Espacement** : `\phantom{-}\\` pour saut vertical en math, `%` avant la première équation d'un bloc.
- **Interdits** : `\textbf{}` (utiliser `**...**`), `\begin{itemize}` (utiliser `$\bullet$` ou tirets MyST), `\bbRac` (n'existe pas), variable nommée `total` (réservée), `if pxs_variation_number` (vaut toujours 1).
- **Niveaux de difficulté** : `Advanced` = avancé, `Intermediate` = intermédiaire.
- **`pxsl_format_number` ne fait PAS d'arrondi** — c'est purement un formattage (virgule française). Toujours arrondir AVANT.
### RÈGLE 1.1 — `$` collé à un chiffre : préfixer par `${}` (casse silencieuse)
Ajoutée 2026-06-12 (cas réel : 1413 occurrences dans les lots livrés ; motif interdit du harnais).
**❌ FAUTIF** :
```latex
Calculer $4\bigl(g(\sqrt{x})\bigr)^2$. Le prix est ${{prixAff}}$ €.
```
**✅ CORRIGÉ** :
```latex
Calculer ${}4\bigl(g(\sqrt{x})\bigr)^2$. Le prix est ${}{{prixAff}}$ €.
```
**POURQUOI** : un `$` immédiatement suivi d'un chiffre est lu par le moteur comme un MONTANT en devise, pas comme une ouverture de math inline ; le `$…$` se désynchronise et toute la directive part en texte brut. Le groupe vide `{}` est invisible au rendu. Préfixer par `${}` TOUTE injection inline `${{…}}` susceptible de rendre un nombre. (L'app applique un auto-correctif déterministe dans `app/pipeline/postprocess.py::fix_dollar_digit`.)
---
## 2. Métadonnées du fichier
### RÈGLE 2.1 — `:id:` en première ligne, vide
```
`````{exercise}
:id:
:title:...
:modules: ...
...
```
**❌ FAUTIF**
```
`````{exercise}
:title:...
:id: 7dd83aee-26e1-11f1-828c-0ed8d3b012a9
```
**✅ CORRIGÉ**
```
`````{exercise}
:id:
:title:...
```
**POURQUOI** : la plateforme génère un nouvel ID à la soumission. Tout ID hardcodé sera réutilisé et provoquera un conflit.
### RÈGLE 2.2 — Toutes les autres métadonnées sont conservées strictement à l'identique
Aucun ajout, aucune modification de `:title:`, `:modules:`, `:level:`, `:involvedConcepts:`, `:originalSource:`, etc.
---
## 3. Architecture du bloc Python — UN SEUL BLOC PRINCIPAL
### RÈGLE 3.1 — Tous les paramètres aléatoires et calculs sont dans UN bloc Python en début d'exercice
**❌ FAUTIF** (cas vu dans la version pythonisée fournie de l'exo 4)
```python
# Bloc 1 (début exo)
v0 = rd.randint(10, 15)
k = round(rd.uniform(0.4, 0.8), 1)
...
# Bloc 2 (avant Q3)
k_val = round(rd.uniform(0.4, 0.8), 1) # REDÉFINIT k !
t_A = round(rd.uniform(4.0, 6.0), 1)
...
# Bloc 3 (avant Q5)
v0 = rd.randint(10, 15) # REDÉFINIT v0 !
k_val = round(rd.uniform(0.4, 0.8), 1)
...
```
**✅ CORRIGÉ**
```python
# UN SEUL bloc en début d'exercice
import random as rd
import math
import numpy as np
import matplotlib.pyplot as plt
from sympy import Rational, latex
from pyxiscience.Mes_fctions_generalistes_bis import pxs_config, pxsl_format_number
config_std = pxs_config()
# Tirage initial
v0 = rd.choice([11, 12, 13])
k_num = rd.choice([5, 6])
k_coef = Rational(k_num, 10)
# Toutes les variables dérivées calculées ici
inv_k = Rational(10, k_num)
asymptote = v0 * inv_k + inv_k**2
alpha = ...
# etc.
globals()
```
**POURQUOI** : `random.seed` n'est pas réinitialisé entre blocs Python. Redéfinir `v0`, `k_coef`, etc. dans des blocs ultérieurs fait que les valeurs **diffèrent entre questions** : Q4 dit "y' + 0,6y = e^(-0,6t)" puis Q7 dit "v(t) = (12+t)e^(-0,4t)" → exercice incohérent.
### RÈGLE 3.2 — Imports uniquement dans le premier bloc
Tous les `import numpy as np`, `import matplotlib.pyplot as plt`, etc. sont factorisés. Les blocs ultérieurs (typiquement le bloc graphique) réutilisent les imports via `globals()`.
### RÈGLE 3.3 — Le bloc graphique réutilise les variables du bloc principal
Si un bloc Python séparé est nécessaire pour le graphique (placement après l'intro de la partie A par exemple), il doit utiliser les variables déjà calculées, pas les re-tirer.
```python
# Bloc graphique (après l'énoncé de la partie A)
fig, ax = plt.subplots(figsize=(7, 5))
t_graph = np.linspace(0, 16.5, 2000)
y_graph = np.array([d_num(x) for x in t_graph]) # d_num défini dans bloc principal
ax.plot(t_graph, y_graph, color="blue")
# ...
plt.show()
```
---
## 4. Tirage des paramètres — Cohérence mathématique
### RÈGLE 4.1 — Tirages en Rational exact, pas en flottant arrondi
**❌ FAUTIF**
```python
k = round(rd.uniform(0.4, 0.8), 1)
k_coef = Rational(int(k * 10), 10) # int(7.0) peut donner 6 à cause des flottants
```
**✅ CORRIGÉ**
```python
k_num = rd.choice([5, 6, 7, 8])
k_coef = Rational(k_num, 10)
```
**POURQUOI** : `round(rd.uniform(0.4, 0.8), 1)` peut produire `0.6999999999999...`, et `int(0.6999... * 10) = 6` au lieu de 7. Bug d'arrondi flottant classique. Tirer directement les entiers numérateurs/dénominateurs.
**TOLÉRANCE** : `round(rd.uniform(...), N)` est ACCEPTABLE pour une **probabilité d'affichage** dont la précision n'est pas critique (par ex. `p = round(rd.uniform(0.05, 0.25), 3)` pour un coefficient binomial affiché à 0.001 près). À ne PAS utiliser pour des valeurs entrant dans des calculs sympy exacts, des bornes d'intervalles, ou des paramètres de récurrence.
### RÈGLE 4.2 — Les paramètres dérivés sont COHÉRENTS avec les paramètres tirés
Tout paramètre qui apparaît dans l'énoncé ET qui dépend mathématiquement d'autres paramètres ne doit pas être tiré indépendamment.
**❌ FAUTIF** (vu dans exo 4)
```python
v0 = rd.randint(10, 15)
k = round(rd.uniform(0.4, 0.8), 1)
L_min = rd.randint(20, 25) # tirage indépendant
d_15 = rd.randint(12, 18) # tirage indépendant
```
Conséquences :
- `L_min` peut être inférieur à l'asymptote `v0/k + 1/k²` → la zone de freinage est trop petite, l'énoncé est faux.
- `d_15` n'a aucun lien avec la valeur de `d(2)` à lire sur le graphique → la "lecture graphique" donne un résultat incohérent.
**✅ CORRIGÉ**
```python
v0 = rd.choice([11, 12, 13])
k_num = rd.choice([5, 6])
k_coef = Rational(k_num, 10)
# Paramètres DÉRIVÉS
asymptote = v0 * Rational(10, k_num) + Rational(10, k_num)**2
L_min = math.ceil(float(asymptote)) # garanti > asymptote
d_lecture = round(d_num(2)) # cohérent avec graphique
```
**POURQUOI** : un paramètre qui doit satisfaire une contrainte mathématique se calcule, il ne se tire pas.
### RÈGLE 4.3 — Les paramètres aléatoires PRÉSERVENT la propriété démontrée
Si l'exercice démontre une propriété (ex: `w_n ≥ n`), les paramètres tirés doivent la rendre vraie pour TOUS les seeds.
**❌ FAUTIF** (vu dans exo 3)
```python
w0 = rd.randint(-2, 3)
alpha = rd.randint(2, 4)
beta = rd.randint(-3, -1)
gamma = rd.randint(1, 5)
```
Sur 1000 seeds testés :
- 36% ont `w0 < 0` → l'initialisation `w_0 ≥ 0` est fausse.
- 34% ont `α + β ≤ 0` → la récurrence `w_n ≥ n` est mathématiquement fausse (testé : `α=2, β=-3, γ=1, w_0=1` donne `w_4 = -2 < 4`).
**✅ CORRIGÉ**
```python
# Contrainte : w_0 >= 0, alpha + beta = 1, gamma >= 1
w0 = rd.randint(0, 3)
ab_pairs = [(2, -1), (3, -2), (4, -3)] # alpha + beta = 1 garanti
alpha, beta = rd.choice(ab_pairs)
gamma = rd.randint(1, 5)
```
**POURQUOI** : pythoniser ne signifie pas randomiser sans contrainte. La propriété démontrée doit rester vraie sur 100% des variantes. Si nécessaire, restreindre la plage ou utiliser une boucle `while` pour rejeter les tirages invalides.
**EXTENSION — calculs intermédiaires dans `detailedSolution`** : si une variable aléatoire change (ex. `p ∈ {0.7, 0.8, 0.9}`), TOUS les calculs intermédiaires affichés dans les `detailedSolution` doivent être recalculés à partir de cette variable. **Aucune valeur dérivée ne peut être hardcodée en littéral.**
```
❌ FAUTIF : `= 0{,}81 + 0{,}01 = 0{,}82` (vrai uniquement pour p=0.9)
✅ CORRIGÉ : `= {{p_fidele_carre_str}} + {{p_contraire_carre_str}} = {{p_V3_str}}`
```
L'audit doit lister TOUS les nombres décimaux apparaissant dans les `detailedSolution` et vérifier qu'ils proviennent d'une variable Python, pas d'une chaîne littérale.
### RÈGLE 4.5 — Détection des invariants Vrai/Faux dépendants des paramètres
Pour les exercices "Vrai ou Faux", la réponse V/F dépend souvent de valeurs numériques précises. **Avant de tirer aléatoirement un paramètre**, vérifier algébriquement pour quelles valeurs la réponse V/F annoncée par la solution reste correcte.
Exemples typiques :
- `∫₀^a x² dx = a²` (aire d'un carré) ⟺ `a = 3`
- `f(x) = e^x + e^(cx)` solution de `y' = cy - e^x` ⟺ `c = 2`
- `A(n, k) = C(m, p)` ⟺ couples (n, k, m, p) précis
**Deux stratégies acceptables** :
1. **Fixer les paramètres** sur la valeur source (pas de variation aléatoire)
2. **Adapter dynamiquement la conclusion V/F** selon le tirage (cf. règle 8.4)
### RÈGLE 4.6 — Arrondi nul à supprimer
Quand une probabilité ou un résultat est exprimé en notation scientifique (valeur `< 10⁻³`), ne pas afficher l'arrondi `≈ 0,000` qui n'apporte aucune information.
**✅ PATTERN**
```python
if round(float(proba), 3) == 0:
proba_final = proba_exact_str # ex: "8,1 × 10⁻⁵"
else:
proba_final = f"{proba_exact_str} \\approx {proba_str}"
```
### RÈGLE 4.7 — Cohérence des formules développées avec les paramètres
Si une solution développe une formule pas à pas (ex. `n × (n-1) × (n-2)` pour un arrangement à `k=3`), le paramètre `k` DOIT être fixé OU le développement DOIT être généré dynamiquement.
**✅ PATTERN — helpers à inclure dans le bloc `{python}`**
```python
def develop_chain(start, length):
"""start × (start-1) × ... (length facteurs)"""
return " \\times ".join(str(start - i) for i in range(length))
def develop_chain_to(start, end_inclusive):
"""start × (start-1) × ... × end_inclusive"""
return " \\times ".join(str(i) for i in range(start, end_inclusive - 1, -1))
```
### RÈGLE 4.8 — Pas d'assertion sur les distances numériques
Quand on tire une variable "presque égale" à une valeur cible (typiquement pour créer un piège dans un Vrai/Faux), NE PAS imposer un seuil de distance strict — l'inégalité mathématique structurelle (ex: rationnel vs irrationnel) suffit.
```
❌ FAUTIF : assert abs(float(frac) - J_approx) > 1e-4
# plante pour 7/11 (différence ≈ 7×10⁻⁵) qui est pourtant
# le piège le plus crédible
✅ CORRIGÉ : pas d'assertion sur la distance — la distinction structurelle
(rationnel vs irrationnel) garantit l'inégalité.
```
### RÈGLE 4.4 — Identifier les contraintes mathématiques avant de pythoniser
Avant de transformer une constante en variable aléatoire, vérifier qu'elle peut varier sans casser :
- l'initialisation d'une récurrence
- les conditions d'une inégalité
- les hypothèses d'un théorème invoqué dans la solution
- le sens d'une asymptote / limite
Si une contrainte existe, soit borner le tirage, soit ne pas pythoniser ce paramètre.
---
## 5. Calculs — Rational exact, pas de double approximation
### RÈGLE 5.1 — Tous les calculs intermédiaires en Rational/Fraction exacte
**❌ FAUTIF**
```python
p_A = round(rd.uniform(0.4, 0.8) / 0.05) * 0.05 # produit 0.35000000000000003
```
**✅ CORRIGÉ**
```python
num_pA = rd.randint(8, 16) # 0,40 à 0,80 par pas de 0,05
p_A = Fraction(num_pA, 20)
```
### RÈGLE 5.2 — Conversion `float()` UNIQUEMENT au moment de l'affichage
```python
# Calculs : tout en Rational
asymptote = v0 * inv_k + inv_k**2
# Affichage : convertir avec arrondi explicite
asymptote_disp = pxsl_format_number(round(float(asymptote), 2), **config_std)
```
### RÈGLE 5.3 — Pas de fractions reconstruites par double `limit_denominator`
**❌ FAUTIF**
```python
frac = Fraction(p_float).limit_denominator(1000) / Fraction(q_float).limit_denominator(1000)
frac = frac.limit_denominator(100)
```
**✅ CORRIGÉ**
```python
# Calculer la fraction exacte directement à partir des Rational initiaux
frac_exact = p_inter_q / p_total
```
### RÈGLE 5.4 — Centième inférieur via `math.floor`, pas via formule arithmétique
**❌ FAUTIF**
```python
seuil_arrondi = round(seuil * 100 - 5) / 100 # donne 0,73 au lieu de 0,77
```
**✅ CORRIGÉ**
```python
seuil_arrondi = math.floor(seuil * 100) / 100
```
### RÈGLE 5.5 — Vérifier les opérations sur les coefficients
Si la solution simplifie `αn + βn`, le coefficient résultant est `α + β`, pas `α - β`.
**❌ FAUTIF** (vu dans exo 3)
```python
# Dans la solution :
{{alpha}} w_n {{beta}} n + {{gamma}} &\geqslant {{alpha - beta}} n + {{gamma}}
```
Avec `α=3, β=-2`, le code calcule `α - β = 5` au lieu de `α + β = 1`. Bug critique de simplification.
**✅ CORRIGÉ**
```python
{{alpha}} w_n {{beta_n_str}} + {{gamma}} &\geqslant n + {{gamma}} # car alpha + beta = 1
```
---
## 6. Interpolation MyST `{{...}}` — Pièges critiques
### RÈGLE 6.1 — INTERDIT : appels de fonction avec `**kwargs` dans `{{...}}`
**❌ FAUTIF**
```
La suite converge vers ${{latex(limite_proposee, **config_standard)}}$.
```
**Le rendu produit `latex(limite_proposee, **config_standard)` en clair**, pas la valeur. Cause : MyST parse `**config_standard` comme du gras markdown, ce qui casse l'expression.
**✅ CORRIGÉ**
```python
# Dans le bloc Python
limite_proposee_tex = latex(limite_proposee, **config_standard)
```
```
La suite converge vers ${{limite_proposee_tex}}$.
```
**POURQUOI** : tout appel `f(x, **kwargs)` dans `{{...}}` est interprété par MyST comme markdown avant interpolation Python. Toujours stocker le résultat dans une variable string en amont.
### RÈGLE 6.2 — `pxsl_format_number(...)` dans `{{...}}` : créer la string en amont
**❌ FAUTIF**
```
Le coefficient vaut ${{pxsl_format_number(round(float(coef), 2), **config_standard)}}$.
```
**✅ CORRIGÉ**
```python
coef_disp = pxsl_format_number(round(float(coef), 2), **config_std)
```
```
Le coefficient vaut ${{coef_disp}}$.
```
### RÈGLE 6.3 — Pré-calculer toutes les variables d'affichage dans le bloc Python
À la fin du bloc Python principal, créer un bloc dédié au formattage. Seul `sympy.latex(...)` accepte `**config_standard` — les helpers PyxiScience (`pxsl_format_number`, `pxsl_res_num`, etc.) ont leurs propres kwargs spécifiques (cf. règle 6.4).
```python
# === Variables d'affichage prêtes ===
k_disp = pxsl_format_number(float(k_coef)) # PAS de **config_std
coef_const_disp = pxsl_format_number(float(coef_const))
inv_k_tex = latex(inv_k, **config_standard) # OK ici
asymptote_tex = latex(asymptote, **config_standard) # OK ici
alpha_disp = pxsl_format_number(alpha)
# etc.
globals()
```
Le markdown ne contient alors plus que `{{var}}` simples, sans appel de fonction.
### RÈGLE 6.4 — `**config_standard` réservé à `sympy.latex`
Le résultat de `pxs_config()` contient des clés (`ln_notation`, `mat_str`, etc.) destinées **uniquement à `sympy.latex()`**. Les helpers PyxiScience (`pxsl_format_number`, `pxsl_res_num`, `pxsl_matrix`, etc.) ont leurs propres kwargs et **plantent** si on leur passe `**config_standard`.
```
❌ FAUTIF : pxsl_format_number(aire_carre, **config_standard)
→ TypeError: got an unexpected keyword argument 'ln_notation'
✅ CORRECT : pxsl_format_number(aire_carre) ← pas de splat
✅ CORRECT : pxsl_res_num(proba, dec=4, egal=False) ← kwargs explicites
✅ CORRECT : latex(p_fidele, **config_standard) ← seul sympy.latex
```
Liste explicite : **NE PAS** splat `**config_standard` sur `pxsl_format_number`, `pxsl_res_num`, `pxsl_matrix`, `pxsl_pow`, `pxsl_latex_coefficient` (alias `lc`), `pxsl_par`, `pxsl_mult`, `pxsl_choose_udv`, `pxs_Interval(...).print()`. **TOUJOURS** splat sur `sympy.latex(...)`.
### RÈGLE 6.5 — `{{ }}` ne contient QU'UN nom de variable nu (camelCase, suffixe `Aff`)
Ajoutée 2026-06-12 (convention vérifiée sur 222/222 exemples livrés : 0 appel de fonction, 0 underscore, 2563 injections `…Aff` ; contrôle STATIQUE du harnais).
**❌ FAUTIF** :
```latex
$T(x) = {{latex(T)}}$, coefficient {{lc(a, sign=True)}}, sur {{dom.print()}}, p = {{p_str}}
```
**✅ CORRIGÉ** :
```python
# Dans le bloc Python :
tTex = latex(T, **config_standard)
coefAAff = lc(a, sign=True)
domAff = dom.print()
pStr = str(p)
```
```latex
$T(x) = {{tTex}}$, coefficient {{coefAAff}}, sur {{domAff}}, p = {{pStr}}
```
**POURQUOI** : le runtime de la plateforme ne substitue de façon fiable que des identifiants nus ; un appel/calcul dans `{{ }}` peut fuir tel quel dans le rendu, et un underscore crée des indices LaTeX parasites. Tout s'évalue dans le bloc Python, dans des variables d'affichage camelCase suffixées `Aff`. (Auto-lift déterministe : `app/pipeline/postprocess.py::auto_lift_injections` + `rename_underscore_injections`.)
---
## 7. Affichage des coefficients signés — Anti-concaténation
### RÈGLE 7.1 — NE JAMAIS concaténer un signe à une valeur dans le markdown
**❌ FAUTIF**
```
w_{n+1} = {{alpha}} w_n {{beta}} n + {{gamma}}
```
Avec `β = -1`, le rendu donne `2 w_n -1 n + 3` (moche : `-1n` au lieu de `-n`).
**✅ CORRIGÉ**
```python
# Dans le bloc Python
if beta == -1:
beta_n_str = "- n"
elif beta < 0:
beta_n_str = f"- {-beta}n"
else:
beta_n_str = f"+ {beta}n"
```
```
w_{n+1} = {{alpha}} w_n {{beta_n_str}} + {{gamma}}
```
### RÈGLE 7.2 — Pour tout coefficient pouvant être ±, générer une string complète avec signe intégré
```python
# Pour v'(t) = (coef_const + coef_t * t) e^(-kt)
# coef_const < 0 toujours → latex(coef_const) commence par "-"
# coef_t < 0 toujours → on construit explicitement
coef_t_signed = f"- {pxsl_format_number(float(-coef_t), **config_std)}"
# Affichage : ({{coef_const_disp}} {{coef_t_signed}} t)
# → "(-6,2 - 0,6 t)"
```
### RÈGLE 7.3 — Cas typiques de coefficients à traiter explicitement
- Coefficient devant `t`, `n`, `x` (terme variable d'un polynôme)
- Constante d'intégration ajoutée
- Reste d'une factorisation
- Termes dans une somme avec signes alternés
### RÈGLE 7.4 — Format des décimales à 3 chiffres fixes
Pour les arrondis à 10⁻³ près (consigne fréquente en Bac), utiliser un format à **3 décimales fixes** plutôt que `str(round(...))` qui tronque les zéros.
```
❌ FAUTIF : str(round(0.6561, 3)) → '0.656' (OK)
str(round(0.6, 3)) → '0.6' (manque "00" final attendu)
✅ CORRECT : f"{x:.3f}".replace('.', '{,}')
# 0.6561 → '0{,}656'
# 0.6 → '0{,}600'
```
---
## 8. Solutions validées — Directive Chabane
### RÈGLE 8.1 — INTERDICTION ABSOLUE de modifier les solutions validées
Le générateur ne doit pas :
- ajouter des listes à puces qui n'existaient pas
- ajouter des préambules redondants en début de solution
- ajouter des étapes intermédiaires de calcul non validées
- reformuler des phrases (changer "factoriser" en "diviser", etc.)
- ajouter de la pédagogie ("Les quatre conditions de la loi binomiale sont vérifiées...")
**❌ FAUTIF** (vu dans exo 3)
```
{fr}`On divise le numérateur et le dénominateur par {{b1}}^n :`
```
Alors que la version validée disait :
```
{fr}`On factorise le numérateur par 5^n et le dénominateur par 3^n :`
```
(La manipulation est en réalité une **factorisation séparée** au numérateur et au dénominateur, pas une division commune.)
**✅ CORRIGÉ**
```
{fr}`On factorise le numérateur par {{b1}}^n et le dénominateur par {{d1}}^n :`
```
### RÈGLE 8.2 — En cas de doute, copier-coller la solution validée
Le générateur ne doit jamais "améliorer" ou "compléter" une solution validée. Si la pythonisation impose une adaptation (variable au lieu de constante), substituer mécaniquement et rien d'autre.
### RÈGLE 8.3 — Si la pythonisation a touché à une solution validée, signaler avant correction
L'audit doit demander confirmation explicite avant de restaurer. Le générateur ne doit pas le faire silencieusement.
### RÈGLE 8.4 — Conclusions Vrai/Faux adaptatives
Pour les exos "Vrai ou Faux" où on veut faire varier des paramètres qui peuvent changer la réponse, préparer la conclusion en Python comme variable conditionnelle :
```python
if condition_vraie:
conclusion_fr = "... L'affirmation est vraie."
conclusion_en = "... Statement is true."
else:
conclusion_fr = f"... (détail spécifique). L'affirmation est fausse."
conclusion_en = f"... (specific detail). Statement is false."
```
Puis injecter via `{{conclusion_fr}}` / `{{conclusion_en}}` dans la `detailedSolution`. La structure du raisonnement reste identique, seul le verdict final s'adapte au tirage.
---
## 9. Structure MyST — Paragraphes contextuels
### RÈGLE 9.1 — Les paragraphes contextuels (intro de partie, intro de question groupée) sont HORS `questionStatement`
**❌ FAUTIF**
```
:::::{question}
:questionType: STQ
::::{questionStatement}
$\underline{\textbf{Partie A}}$
{fr}`Le centre propose aux personnes...`{en}`...`
{fr}`Question 1...`{en}`...`
::::
```
**✅ CORRIGÉ**
```
$\underline{\textbf{Partie A}}$
{fr}`Le centre propose aux personnes...`{en}`...`
(exo1-q1)=
:::::{question}
:questionType: STQ
::::{questionStatement}
{fr}`Question 1...`{en}`...`
::::
```
**POURQUOI** : `questionStatement` doit contenir UNIQUEMENT la consigne de la question. Tout contexte commun à plusieurs questions est un paragraphe libre entre les blocs `:::::{question}`.
### RÈGLE 9.2 — Les paragraphes contextuels qui utilisent des variables `{{var}}` viennent APRÈS le bloc Python qui les définit
```
```{python}
v0 = ...
k_coef = ...
globals()
```
{fr}`On considère la fonction $v$ avec $v(0) = {{v0}}$.`{en}`...`
```
### RÈGLE 9.3 — Phrase "Dans cette question, on étudie..." (regroupement APMEP)
L'APMEP utilise parfois "Dans cette question, on étudie X" pour introduire un groupe de sous-questions (2.a, 2.b, 2.c, 2.d). En PyxiScience, les sous-questions sont éclatées en questions distinctes.
**RÈGLE** : reformuler en précisant le périmètre exact pour ne pas inclure les questions hors groupe.
```
{fr}`Dans les quatre questions qui suivent, on étudie la fonction $v$ sur $[0~;~+\infty[$.`
```
Le **nombre est explicite** (`les quatre questions`) pour éviter d'englober la question suivante (numérotée séparément en APMEP).
### RÈGLE 9.4 — Pas de doublons d'énoncés
L'énoncé de chaque question apparaît UNE SEULE FOIS, dans son `questionStatement`. Ne pas le répéter en paragraphe libre avant le bloc question.
**❌ FAUTIF** (vu dans exo 4)
```
{fr}`Que vaut $d'(t_A)$ ? Interpréter ce résultat.`
:::::{question}
::::{questionStatement}
{fr}`Que vaut $d'(t_A)$ ? Interpréter ce résultat.` # DOUBLON
::::
```
### RÈGLE 9.5 — Variables Python dans les blocs matplotlib
Les substitutions `{{...}}` ne fonctionnent que dans le **texte MyST**, **PAS** à l'intérieur des blocs ```` ```{python} ```` . Dans un bloc Python, utiliser les vraies variables Python (f-string ou concaténation).
```
❌ FAUTIF : probs = r"${{p_fidele_str}}$"
# matplotlib reçoit littéralement "${{p_fidele_str}}$"
# mathtext rend "p_fidele_str" en italique math (incompréhensible)
✅ CORRECT : probs = f"${p_fidele_str}$"
# Python interpole d'abord → matplotlib reçoit "$0{,}9$"
```
Pareil pour les `ax.set_title(...)`, `ax.text(...)`, `ax.legend(...)` : utiliser des f-strings Python, jamais `{{...}}`.
---
## 10. Bilinguisme FR/EN — Symétrie
### RÈGLE 10.1 — FR et EN doivent être équivalents en niveau de détail
**❌ FAUTIF** (vu dans exo 2 Q6, exo 4 Q9)
- FR détaille le changement de variable `X = 0,6t` puis applique les croissances comparées (5 lignes)
- EN saute directement à "by comparison of growth rates" puis le boxed (2 lignes)
**✅ CORRIGÉ** : les deux versions ont le même squelette de raisonnement, mêmes étapes, mêmes formules.
### RÈGLE 10.2 — Notation math identique en FR et EN
**❌ FAUTIF**
```
{fr}`...l'intervalle $[0\,;\,1]$.`{en}`...the interval [0\,;\,1].`
^^^^^^^^^^^ pas en mode math
```
**✅ CORRIGÉ**
```
{fr}`...l'intervalle $[0\,;\,1]$.`{en}`...the interval $[0\,;\,1]$.`
```
### RÈGLE 10.3 — Si l'utilisateur a modifié l'énoncé, effacer la version EN
Si la version pythonisée modifie le contenu de l'énoncé (pas seulement réagencement structurel), le générateur efface tous les blocs `{en}` pour permettre passage en retraduction par l'app dédiée. Le réagencement (séparation a/b/c en questions distinctes) n'est PAS une modification.
### RÈGLE 10.4 — Format auto fraction / décimal selon la nature
Pour les valeurs qui peuvent être rationnelles non-décimales (ex. `0,5 / 0,6 = 5/6`), créer un helper qui choisit automatiquement entre forme décimale finie et fraction LaTeX :
```python
def _frac_to_latex(f):
"""Fraction Python → décimal LaTeX si fini, sinon \\frac{a}{b}."""
if f.denominator == 1:
return str(f.numerator)
d = f.denominator
while d % 2 == 0: d //= 2
while d % 5 == 0: d //= 5
if d == 1:
return str(float(f)).replace('.', '{,}')
return f"\\frac{{{f.numerator}}}{{{f.denominator}}}"
```
Exemples :
- `_frac_to_latex(Fraction(5, 8))` → `'0{,}625'` (décimal fini)
- `_frac_to_latex(Fraction(5, 6))` → `'\\frac{5}{6}'` (décimal infini)
- `_frac_to_latex(Fraction(5, 4))` → `'1{,}25'` (décimal fini)
### RÈGLE 10.5 — Notation scientifique française pour valeurs très petites
Pour les valeurs `< 10⁻⁴`, basculer en notation scientifique LaTeX plutôt qu'afficher `0{,}000081`.
```python
import math
def _frac_to_str_smart(f, sci_threshold=1e-4):
"""Décimal pour valeurs raisonnables, sinon notation scientifique."""
fl = float(f)
if 0 < abs(fl) < sci_threshold:
exp = int(math.floor(math.log10(abs(fl))))
mant = fl / (10 ** exp)
mant_str = f"{mant:.2f}".rstrip('0').rstrip('.').replace('.', '{,}') or "1"
return f"{mant_str} \\times 10^{{{exp}}}"
return _frac_to_latex(f)
```
---
## 11. Rendu graphique matplotlib
### RÈGLE 11.1 — Toute variable utilisée dans l'énoncé DOIT être utilisée dans le tracé
**❌ FAUTIF** (vu dans exo 3)
```python
x_tangente_horiz = rd.randint(1, 3) # tirée
# Énoncé : "tangente horizontale en x = {{x_tangente_horiz}}"
# Mais le code matplotlib trace une fonction f(x) = K * (x^c - 1) * ln(x)
# qui a SA tangente horizontale FIGÉE en x=1 (par construction f(1)=0, f'(1)=0).
```
Le graphique montre la tangente horizontale en x=1 alors que l'énoncé annonce x=2 ou x=3. Incohérent dans 66% des seeds.
**✅ CORRIGÉ**
- Soit fixer `x_tangente_horiz = 1` (constante), soit modifier la fonction tracée pour que sa tangente horizontale soit effectivement en `x_tangente_horiz`.
### RÈGLE 11.2 — La tangente tracée DOIT être la vraie tangente analytique
**❌ FAUTIF**
```python
slope = f(x_A) / (x_A + 0.3) # heuristique arbitraire
```
**✅ CORRIGÉ**
```python
def f_prime(x):
c = coeff_puissance
return facteur * (c * x**(c-1) * np.log(x) + (x**c - 1)/x)
slope = f_prime(x_A)
b_intercept = f(x_A) - slope * x_A
```
### RÈGLE 11.3 — Les labels (Cf, T, Δ, A) doivent rester DANS la fenêtre du graphique
**Symptôme** : si un label est positionné à `(x, y)` avec `y > ylim_max`, matplotlib étend automatiquement la zone d'affichage pour l'inclure, ce qui **compresse** le graphique.
**❌ FAUTIF**
```python
ax.set_ylim(-1.2, 8)
ax.text(11, f(11) - 0.4, r"$\mathcal{C}_f$", ...) # f(11) peut être > 8
ax.text(11, slope*11 + b_intercept + 0.15, "T", ...) # idem
```
**✅ CORRIGÉ**
```python
# Borner les paramètres pour que f(x_max_visible) reste sous le plafond
y_label_target = 6.5
denom_y_max = (x_max**coeff_puissance - 1) * np.log(x_max)
facteur_max = min(70, int(y_label_target / denom_y_max))
facteur_echelle = rd.randint(30, max(30, facteur_max))
# Placer les labels dans une zone garantie sûre
x_label = max(x_A + 1, x_max - 1.5)
x_label = min(x_label, x_max)
y_lab_cf = f(x_label) - 0.3
y_lab_t = min(slope * x_label + b_intercept + 0.5, ylim_max - 0.5)
```
### RÈGLE 11.4 — Pas de mélange Rational sympy + numpy array
**❌ FAUTIF**
```python
d_vals = Rational(5, 3) * t_graph + asymptote # Rational * np.array casse
```
**✅ CORRIGÉ**
```python
d_vals = float(Rational(5, 3)) * t_graph + float(asymptote)
# ou
inv_k_float = 1.0 / float(k_coef)
d_vals = -(v0 + t_graph) * inv_k_float * np.exp(-float(k_coef) * t_graph) + ...
```
### RÈGLE 11.5 — `plt.show()`, jamais `plt.savefig`
```python
# ❌ Le chemin local n'est pas résolu côté plateforme
plt.savefig('arbre.png')
# puis dans le markdown : 
# ✅
plt.show()
```
### RÈGLE 11.6 — Pas d'éléments parasites dans le graphique
- Pas de `ax.text(8.5, 22.2, "Fig. 2", ...)` à l'intérieur du graphique. La référence "Fig. 2" est une légende externe APMEP, pas un élément du tracé.
- Pas de doubles labels "1" sur les axes si les ticks par défaut sont déjà annotés.
- Pas de titre auto matplotlib.
---
## 12. Fidélité APMEP
### RÈGLE 12.1 — L'énoncé pythonisé est strictement conforme à l'APMEP, modulo paramètres
- Toutes les phrases-clés de l'énoncé original sont présentes textuellement.
- Toutes les valeurs numériques sont identiques (modulo pythonisation).
- L'ordre des questions est respecté.
### RÈGLE 12.2 — Sous-questions APMEP a/b/c → questions PyxiScience distinctes
Convention PyxiScience : chaque sous-question APMEP devient une question distincte (`q1`, `q2`, ...) avec son propre `questionStatement` / `questionHint` / `detailedSolution`. Ce réagencement n'est PAS une modification d'énoncé au sens de la directive 2.
### RÈGLE 12.3 — Géométrie : pythonisation autorisée à condition de préserver le niveau
La pythonisation des exercices de géométrie dans l'espace (vecteurs, droites, plans, projections, orthocentres) est **autorisée** (cf. exos validés Amérique du Sud 13 novembre 2025, Amérique du Nord 22 mai 2025). Convention observée :
- **Représenter les points/vecteurs en `sympy.Matrix([x, y, z])`** pour calculs symboliques.
- **Vérifier l'orthogonalité via `.dot()` = 0** au lieu de calculer par projection.
- **Pour une projection orthogonale**, paramétrer la droite (`K = A + lam*(B-A)`), construire `CK.dot(AB) = 0`, résoudre via `solve(...)`.
- **Distances via `sqrt(vec.dot(vec))`** (norme).
⚠️ **NE PAS changer le niveau pédagogique** : si l'APMEP demande une démonstration géométrique (collinéarité, coplanarité, équation cartésienne d'un plan), la version pythonisée doit conserver cette difficulté — on randomise les coefficients, pas la nature du raisonnement.
**❌ FAUTIF** (mauvaise pythonisation)
```python
# Donner les coordonnées du point projeté directement
A = Matrix([1, 0, 0]); B = Matrix([0, 1, 0]); C = Matrix([0, 0, 1])
K = Matrix([0.5, 0.5, 0]) # hardcodé — élève n'a plus à calculer
```
**✅ CORRIGÉ**
```python
# Le tirage des sommets randomise les valeurs, mais l'élève DOIT calculer la projection
alpha, beta, gamma = rd.sample(range(1, 4), 3)
A = Matrix([alpha, 0, 0]); B = Matrix([0, beta, 0]); C = Matrix([0, 0, gamma])
lam = Symbol('lam', real=True)
K = A + lam*(B - A)
sol = solve((K - C).dot(B - A), lam)[0]
# K_proj = A + sol*(B - A) # ← l'élève le démontre dans sa solution
```
### RÈGLE 12.4 — Coquilles APMEP corrigées silencieusement
Si l'APMEP contient une coquille (ex: "au activités" sans "x"), la version PyxiScience peut la corriger silencieusement. Ce n'est pas une modification d'énoncé.
---
## 13. Pièges spécifiques par type d'exercice
### 13.1 — Probabilités
- Tirages de probabilités : `rd.randint(a, b) / 20` + `Fraction`, jamais `round(uniform/0.05)*0.05`.
- Variables conditionnelles : ne pas écraser `p_non_B_sachant_non_A` (P_{Ā}(B̄)) par `p_non_A_sachant_non_B` (P_{B̄}(Ā)). Ce sont des conditionnelles inversées (Bayes), pas la même quantité.
- Loi binomiale : `scipy.stats.binom.cdf(k, n, p)` pour `P(X ≤ k)`.
- Bienaymé-Tchebychev : `math.floor(seuil*100)/100` pour le centième inférieur.
### 13.2 — Suites
- Variation `\case1`/`\case2` : à mettre INSIDE `\right{}`/`\wrong{}`, jamais autour.
- `pxs_variation_number` toujours = 1 dans le code Python — calculer toutes les variations en parallèle.
- Récurrence : voir RÈGLE 4.3 (préserver la propriété par contraintes sur les paramètres).
### 13.3 — Analyse / Intégration
- Constante d'intégration : ajouter `\py{C_new}` ou demander explicitement à l'utilisateur.
- Pas de concaténation `\py{sg(x)}` : créer une variable complète `signe_coeff_x = "+ val"` ou `"- val"` (RÈGLE 7.1).
### 13.4 — Tracés matplotlib
- Voir section 11 complète.
---
## 16bis. Patterns récurrents observés en production
(Section issue de l'analyse des exos validés de mai/novembre 2025. À ne pas confondre avec la section 16 (Anti-patterns), qui liste les BUGS — ici on liste des CONVENTIONS positives à reproduire.)
### RÈGLE 16.1 — Tirage de paramètres avec contraintes : préférer la list comprehension
Quand plusieurs paramètres doivent satisfaire une contrainte couplée (ex: `p_vv > p_nv` et `denominator(1/(1-(p_vv-p_nv))) <= 6`), ne PAS faire de `for _ in range(100): ... if cond: break` qui peut échouer silencieusement. Préférer une **list comprehension qui énumère toutes les combinaisons valides**, puis `rd.choice()`.
**❌ FAUTIF**
```python
for _ in range(100):
p_vv = rd.choice(valeurs)
p_nv = rd.choice(valeurs)
if p_vv > p_nv and Rational(1, 1-(p_vv-p_nv)).denominator <= 6:
break # peut sortir sans break, valeurs invalides en silence
```
**✅ CORRIGÉ**
```python
couples_valides = [
(p_vv, p_nv) for p_vv in valeurs for p_nv in valeurs
if p_vv > p_nv and Rational(1, 1 - (p_vv - p_nv)).denominator <= 6
]
p_vv, p_nv = rd.choice(couples_valides) # garanti valide
```
**POURQUOI** : la list comprehension échoue de manière visible (IndexError si liste vide), au lieu de laisser passer des paramètres invalides. Pattern observé dans les exos validés Amérique du Sud 13 novembre 2025.
### RÈGLE 16.2 — Loi binomiale : utiliser scipy.stats.binom (sf / cdf / pmf)
Pour les calculs `P(X ≥ k)`, `P(X ≤ k)`, `P(X = k)` d'une binomiale, **toujours** utiliser `scipy.stats.binom`.
**❌ FAUTIF** (calcul à la main, lent et fragile)
```python
prob_inf = sum(binomial(n, i) * p**i * (1-p)**(n-i) for i in range(k+1))
```
**✅ CORRIGÉ**
```python
from scipy.stats import binom
prob_inf = binom.cdf(k, n, p) # P(X ≤ k)
prob_sup_strict = binom.sf(k - 1, n, p) # P(X ≥ k) (sf = 1 - cdf(k-1))
prob_egal = binom.pmf(k, n, p) # P(X = k)
```
**Astuce conditionnelle** : si `(X ≥ nb) ⊂ (X ≥ ne)` (donc `nb ≥ ne`), alors `P(X ≥ nb | X ≥ ne) = binom.sf(nb-1, n, p) / binom.sf(ne-1, n, p)`. Garantir `ne < nb` au tirage pour que le quotient soit ≤ 1.
**POURQUOI** : `binom.sf` (survival function) évite les sommes manuelles de coefficients binomiaux qui sont coûteuses en sympy et peuvent dériver numériquement. Pattern dans Exo 2 Amérique du Sud, exo binomiale randomisée.
### RÈGLE 16.3 — Matplotlib : axes "zero spines" pour la convention math française
Pour les graphes de fonction (style scolaire FR), positionner les axes sur l'origine et masquer les spines droite/haute. C'est la convention attendue par les enseignants.
**✅ PATTERN STANDARD**
```python
fig, ax = plt.subplots(figsize=(6, 5))
# ... ax.plot(...) ...
ax.spines['left'].set_position('zero')
ax.spines['bottom'].set_position('zero')
ax.spines['right'].set_color('none')
ax.spines['top'].set_color('none')
ax.xaxis.set_label_coords(1.02, -0.05) # label "x" en bout d'axe
ax.yaxis.set_label_coords(-0.05, 1.02) # label "y" en haut d'axe
plt.show()
```
À utiliser pour **toute étude de fonction** (Bac, terminale). Ne PAS utiliser pour les graphes de distribution / histogrammes (cadre classique avec spines complètes mieux adapté). Pattern observé dans Exos 3 + 4 Amérique du Sud.
---
## 14. Checklist pre-flight avant livraison
Le générateur n'envoie pas le fichier tant que les points suivants ne sont pas cochés :
### Métadonnées
- [ ] `:id:` est en première ligne et vide
- [ ] Toutes les autres métadonnées sont identiques à la version non pythonisée
### Architecture Python
- [ ] UN SEUL bloc Python principal en début d'exercice
- [ ] Imports uniquement dans le premier bloc
- [ ] Tous les blocs Python se terminent par `globals()`
- [ ] Aucune redéfinition de variable entre blocs
- [ ] Bloc graphique (s'il existe) réutilise les variables du bloc principal
### Tirages aléatoires
- [ ] Calculs en `Rational` exact, conversion `float()` uniquement à l'affichage
- [ ] Pas de `int(x * 10)` sur des flottants
- [ ] Tous les paramètres dérivés sont **calculés**, pas tirés indépendamment
- [ ] La propriété démontrée par l'exercice est vraie sur 100% des seeds (testée sur ≥ 200 seeds)
- [ ] Pas de valeurs négatives quand l'énoncé exige des entiers naturels (ex: `w_0 ≥ 0`)
### Affichage / interpolation
- [ ] Aucun `{{f(x, **kwargs)}}` dans le markdown — toutes les strings d'affichage pré-calculées dans le bloc Python
- [ ] Coefficients signés générés explicitement (pas de `{{a}}{{b}}t` avec `b` négatif)
- [ ] `pxsl_format_number` reçoit toujours des valeurs déjà arrondies (`round(float(x), n)` en amont)
### Solutions
- [ ] Toutes les solutions validées Chabane sont conformes textuellement (modulo paramètres)
- [ ] Aucun ajout pédagogique non sollicité (listes à puces, préambules, étapes intermédiaires)
- [ ] Pas de reformulation ("factoriser" reste "factoriser", pas "diviser")
### Structure MyST
- [ ] Paragraphes contextuels HORS `questionStatement`
- [ ] Pas de doublons d'énoncés (chaque énoncé apparaît une seule fois)
- [ ] Phrases "Dans cette question, on étudie..." reformulées avec périmètre explicite
- [ ] Ancres `(exoN-qK)=` présentes avant chaque bloc question
### Bilinguisme
- [ ] FR et EN équivalents en niveau de détail
- [ ] Notation math identique (`$[0\,;\,1]$` dans les deux versions)
- [ ] Si l'énoncé est modifié → blocs `{en}` effacés (sinon conservés intacts)
### Rendu graphique
- [ ] Toutes les variables aléatoires utilisées dans l'énoncé sont effectivement utilisées dans le tracé
- [ ] Tangentes calculées analytiquement (`f'(x_A)`), pas par heuristique
- [ ] Tous les labels (Cf, T, Δ, A) restent dans la fenêtre `[xlim, ylim]`
- [ ] Paramètres bornés pour que les courbes ne sortent pas du cadre
- [ ] `plt.show()` (pas `plt.savefig`)
- [ ] Pas de Rational sympy mélangé à des array numpy
### Tests automatiques
- [ ] Simulation sur ≥ 100 seeds : aucune erreur de rendu
- [ ] Simulation sur ≥ 100 seeds : aucun artefact flottant (`0.35000000000000003`, `0.44999999999999996`)
- [ ] Simulation sur ≥ 100 seeds : toutes les contraintes mathématiques de l'exercice sont respectées
- [ ] Aucune valeur affichée hors plage attendue (probabilité > 1, distance négative, etc.)
---
## 15. Convention de nommage des fichiers
- Version non pythonisée corrigée : `exoN_corrige.md`
- Version pythonisée corrigée : `exoN_pythonise_corrige.md`
- Version FR-only (après modif d'énoncé) : `*_FR_only.md`
- Toujours dans `/mnt/user-data/outputs/`
- Pas de longues chaînes descriptives dans le nom
---
## 16. Anti-patterns récurrents — résumé
Les 10 erreurs les plus fréquentes vues sur les pythonisations livrées :
1. **`{{latex(x, **config_standard)}}`** dans le markdown → casse à cause des `**`. Stocker en variable.
2. **Variables redéfinies entre blocs Python** → valeurs incohérentes entre questions.
3. **Tirages indépendants de paramètres mathématiquement liés** → énoncé faux (ex: `L_min < asymptote`).
4. **Paramètres aléatoires qui invalident la propriété démontrée** → solution dit "vrai" alors que c'est faux.
5. **`int(x * 10)` sur flottant** → bugs d'arrondi (ex: `int(4.7 * 10) = 46`).
6. **Heuristique de tangente** au lieu de `f'(x_A)` analytique.
7. **`x_paramètre` aléatoire mais ignoré dans le tracé matplotlib** → graphique incohérent avec l'énoncé.
8. **Labels matplotlib qui sortent de la fenêtre** → graphique compressé.
9. **Solutions validées modifiées** (listes à puces ajoutées, reformulations, étapes ajoutées) → directive Chabane violée.
10. **Doublons d'énoncés** (même phrase en paragraphe libre + dans `questionStatement`).
|