File size: 4,284 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
"""
rules_digest.py (ex utils/rules_digest.py)
──────────────────────────────────────────
Parser de `pythonisation_rules.md` + construction de digests ciblés injectés
dans les prompts. Parsé une fois à l'import, caché en module.

API publique :
  RULES_BY_ID         dict[str, Rule]
  ALL_RULE_IDS        list[str]
  build_rules_digest(rule_ids)  → markdown compact
  AUDIT_RULES_ALWAYS  list[str]
"""

from __future__ import annotations

import re
from typing import Optional, TypedDict

from app.config import RULES_MD


class Rule(TypedDict, total=False):
    id:       str
    title:    str
    section:  str
    bad:      Optional[str]
    good:     Optional[str]
    why:      Optional[str]


_RULE_HEADING = re.compile(
    r"^### RÈGLE (?P<id>\d+\.\d+(?:\.\d+)?)\s*—\s*(?P<title>.+?)\s*$",
    re.MULTILINE,
)
_SECTION_HEADING = re.compile(
    r"^## (?P<num>\d+)\.\s*(?P<title>.+?)\s*$",
    re.MULTILINE,
)
_BAD_BLOCK = re.compile(
    r"\*\*❌\s*FAUTIF\*\*[^\n]*\n```(?:[a-zA-Z]*)\n(.+?)\n```",
    re.DOTALL,
)
_GOOD_BLOCK = re.compile(
    r"\*\*✅\s*CORRIG(?:É|EE?|É?)\*\*[^\n]*\n```(?:[a-zA-Z]*)\n(.+?)\n```",
    re.DOTALL,
)
_WHY = re.compile(
    r"\*\*POURQUOI\*\*\s*:\s*(.+?)(?=\n{2,}|\n#{2,3}\s|\Z)",
    re.DOTALL,
)


def _parse_rules(content: str) -> dict[str, Rule]:
    sections: list[tuple[int, str]] = []
    for m in _SECTION_HEADING.finditer(content):
        sections.append((m.start(), f"{m.group('num')}. {m.group('title').strip()}"))

    rule_matches = list(_RULE_HEADING.finditer(content))
    rules: dict[str, Rule] = {}

    for i, m in enumerate(rule_matches):
        rule_id = m.group("id")
        title = m.group("title").strip()
        body_start = m.end()
        body_end = rule_matches[i + 1].start() if i + 1 < len(rule_matches) else len(content)
        body = content[body_start:body_end].strip()

        section_title = ""
        for off, label in sections:
            if off < m.start():
                section_title = label

        bad_m = _BAD_BLOCK.search(body)
        good_m = _GOOD_BLOCK.search(body)
        why_m = _WHY.search(body)

        rules[rule_id] = Rule(
            id=rule_id,
            title=title,
            section=section_title,
            bad=bad_m.group(1).rstrip() if bad_m else None,
            good=good_m.group(1).rstrip() if good_m else None,
            why=why_m.group(1).strip() if why_m else None,
        )
    return rules


_RULES_CACHE: Optional[dict[str, Rule]] = None


def _load_rules() -> dict[str, Rule]:
    global _RULES_CACHE
    if _RULES_CACHE is None:
        _RULES_CACHE = _parse_rules(RULES_MD.read_text(encoding="utf-8"))
    return _RULES_CACHE


RULES_BY_ID: dict[str, Rule] = _load_rules()
ALL_RULE_IDS: list[str] = sorted(
    RULES_BY_ID.keys(),
    key=lambda s: tuple(int(p) for p in s.split(".")),
)


def build_rules_digest(rule_ids: list[str]) -> str:
    """Digest markdown compact des règles demandées (IDs inconnus ignorés)."""
    if not rule_ids:
        return ""
    parts: list[str] = []
    for rid in rule_ids:
        rule = RULES_BY_ID.get(rid)
        if rule is None:
            continue
        parts.append(f"### Règle {rule['id']} — {rule['title']}")
        if rule.get("bad"):
            parts.append("❌ FAUTIF :")
            parts.append(f"```\n{rule['bad']}\n```")
        if rule.get("good"):
            parts.append("✅ CORRECT :")
            parts.append(f"```\n{rule['good']}\n```")
        if rule.get("why"):
            parts.append(f"→ {' '.join(rule['why'].split())}")
        parts.append("")
    return "\n".join(parts).rstrip() + "\n"


# Règles vérifiables sur le texte assemblé seul (pas d'exec) — passe d'audit.
AUDIT_RULES_ALWAYS: list[str] = [
    "2.1",   # :id: vide en première ligne
    "3.1",   # un seul bloc Python principal
    "6.1",   # pas d'appels **kwargs dans {{...}}
    "6.2",   # pas de pxsl_format_number(**kwargs) dans {{...}}
    "6.3",   # variables d'affichage pré-calculées
    "7.1",   # pas de concaténation signe + valeur
    "9.1",   # paragraphes contextuels hors questionStatement
    "9.4",   # pas de doublons d'énoncés
    "11.5",  # plt.show() (pas savefig)
]