pythonise-exercice / app /knowledge /rules_digest.py
wilenPxs
Deploiement app pythonisation (cache FAISS reconstruit au boot)
77a0a72
Raw
History Blame Contribute Delete
4.28 kB
"""
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)
]