""" scripts/validate.py Valide le fichier de motifs livre (tex/hyph-mg.tex) contre : 1. l'integrite des clusters proteges (source de verite = le fichier .tex) 2. la synchronisation avec le corpus source + les overrides manuels 3. les contradictions entre overrides et motifs regeneres 4. la couverture du corpus (avertissement, pas bloquant) 5. la justification des overrides (avertissement) """ from __future__ import annotations import argparse import re import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from source.generate_patterns import ( AnnotatedWord, MalagasyPhonotactics, PatternExtractor, find_override_contradictions, load_annotated_words, load_manual_overrides, ) class ValidationReport: def __init__(self) -> None: self.errors: list[str] = [] self.warnings: list[str] = [] def error(self, msg: str) -> None: self.errors.append(msg) def warn(self, msg: str) -> None: self.warnings.append(msg) def ok(self) -> bool: return not self.errors def render_text(self) -> str: lines = [f"[AVERTISSEMENT] {w}" for w in self.warnings] lines += [f"[ERREUR] {e}" for e in self.errors] status = "OK" if self.ok() else "ECHEC" lines.append( f"\nValidation : {status} " f"({len(self.errors)} erreur(s), {len(self.warnings)} avertissement(s))" ) return "\n".join(lines) def render_markdown(self) -> str: status = "OK" if self.ok() else "ECHEC" lines = [f"# Rapport de validation -- hyph-malagasy\n", f"**Statut : {status}**\n"] if self.errors: lines.append("## Erreurs\n") lines += [f"- {e}" for e in self.errors] if self.warnings: lines.append("\n## Avertissements\n") lines += [f"- {w}" for w in self.warnings] return "\n".join(lines) def parse_tex_patterns(tex_path: Path) -> list[str]: text = tex_path.read_text(encoding="utf-8") match = re.search(r"\\patterns\{%?\s*(.*?)\}", text, re.DOTALL) if not match: raise ValueError(f"Aucun bloc \\patterns{{...}} trouve dans {tex_path}") body = match.group(1) return [line.strip() for line in body.splitlines() if line.strip() and line.strip() != "%"] def check_cluster_integrity(patterns: list[str], phonotactics: MalagasyPhonotactics, report: ValidationReport) -> None: for pattern in patterns: for v in phonotactics.find_violations(pattern): report.error(f"tex/hyph-mg.tex : motif '{v.pattern}' scinde le cluster protege '{v.cluster}'") def check_sync_with_corpus( tex_patterns: list[str], regenerated_patterns: list[str], manual_overrides: list[str], report: ValidationReport ) -> None: tex_set = set(tex_patterns) expected_set = set(regenerated_patterns) | set(manual_overrides) missing_from_tex = expected_set - tex_set unexplained_in_tex = tex_set - expected_set if missing_from_tex: report.error( f"{len(missing_from_tex)} motif(s) attendu(s) absent(s) de tex/hyph-mg.tex " f"(regeneration requise) : {sorted(missing_from_tex)[:5]}..." ) if unexplained_in_tex: report.error( f"{len(unexplained_in_tex)} motif(s) dans tex/hyph-mg.tex ne proviennent ni du " f"corpus ni de source/manual-overrides.txt : {sorted(unexplained_in_tex)[:5]}..." ) def check_override_contradictions(manual_overrides: list[str], regenerated_patterns: list[str], report: ValidationReport) -> None: for c in find_override_contradictions(manual_overrides, regenerated_patterns): report.error( f"contradiction : l'override '{c.override_pattern}' indique une coupure de " f"parite opposee au motif du corpus '{c.corpus_pattern}' en position {c.position} " f"({c.override_value} vs {c.corpus_value})" ) def check_corpus_coverage(words: list[AnnotatedWord], kept_rules, report: ValidationReport) -> None: covered = {w for rule in kept_rules for w in rule.source_words} for word in words: if word.raw not in covered: report.warn(f"aucun motif retenu pour '{word.raw}' -- verifier min_len/max_len") def check_overrides_have_justification(overrides_path: Path, report: ValidationReport) -> None: if not overrides_path.exists(): return for line in overrides_path.read_text(encoding="utf-8").splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#"): continue pattern_part, _, comment_part = stripped.partition("#") if not comment_part.strip(): report.warn(f"override '{pattern_part.strip()}' sans reference de ticket/issue explicite") def validate(corpus_path: Path, tex_path: Path, overrides_path: Path) -> ValidationReport: report = ValidationReport() words = load_annotated_words(corpus_path) extractor = PatternExtractor(min_len=2, max_len=6) for w in words: extractor.add_word(w) phonotactics = MalagasyPhonotactics() all_rules = extractor.patterns() kept_rules = phonotactics.force_cluster_integrity(all_rules) regenerated_patterns = [r.pattern for r in kept_rules] manual_overrides = load_manual_overrides(overrides_path) try: tex_patterns = parse_tex_patterns(tex_path) except (FileNotFoundError, ValueError) as e: report.error(str(e)) return report check_cluster_integrity(tex_patterns, phonotactics, report) check_sync_with_corpus(tex_patterns, regenerated_patterns, manual_overrides, report) check_override_contradictions(manual_overrides, regenerated_patterns, report) check_corpus_coverage(words, kept_rules, report) check_overrides_have_justification(overrides_path, report) return report def main() -> int: parser = argparse.ArgumentParser(description="Valide les motifs de cesure malgache") parser.add_argument("--corpus", type=Path, default=Path("source/malagasy-hyphenation.txt")) parser.add_argument("--tex", type=Path, default=Path("tex/hyph-mg.tex")) parser.add_argument("--overrides", type=Path, default=Path("source/manual-overrides.txt")) parser.add_argument("--format", choices=["text", "markdown"], default="text") args = parser.parse_args() report = validate(args.corpus, args.tex, args.overrides) print(report.render_markdown() if args.format == "markdown" else report.render_text()) return 0 if report.ok() else 1 if __name__ == "__main__": sys.exit(main())