phi-complexity 0.13.1__tar.gz → 0.14.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/PKG-INFO +5 -2
  2. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/README.md +4 -1
  3. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/carte.py +20 -0
  4. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/core.py +1 -1
  5. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/croyances.py +11 -0
  6. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/dependances.py +42 -54
  7. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/indexeur.py +4 -0
  8. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/eft.py +1 -1
  9. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/lean.py +185 -4
  10. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/registry.py +16 -10
  11. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/treesitter_generic.py +19 -0
  12. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/mission.py +8 -0
  13. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/modeles.py +3 -0
  14. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/oracle.py +1 -1
  15. phi_complexity-0.14.1/phi_complexity/parseur_autonome.py +870 -0
  16. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/parseur_lean.py +77 -2
  17. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/veille.py +71 -6
  18. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/PKG-INFO +5 -2
  19. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/SOURCES.txt +6 -1
  20. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/pyproject.toml +1 -1
  21. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_ancrage.py +1 -1
  22. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_durcissement.py +13 -11
  23. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_durcissement_20261001.py +8 -2
  24. phi_complexity-0.14.1/tests/test_durcissement_extraction_20261002.py +179 -0
  25. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_lean.py +10 -4
  26. phi_complexity-0.14.1/tests/test_parseur_autonome.py +221 -0
  27. phi_complexity-0.14.1/tests/test_veille_alternatif.py +187 -0
  28. phi_complexity-0.14.1/tests/test_zero_treesitter_silencieux.py +187 -0
  29. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/LICENSE +0 -0
  30. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/__init__.py +0 -0
  31. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/__main__.py +0 -0
  32. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/analyseur.py +0 -0
  33. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/ancrage.py +0 -0
  34. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/bayes.py +0 -0
  35. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/chemins_verifiables.py +0 -0
  36. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/cli.py +0 -0
  37. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/durcissement.py +0 -0
  38. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/__init__.py +0 -0
  39. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/panneau_phi.py +0 -0
  40. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/recherche.py +0 -0
  41. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/tampon.py +0 -0
  42. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/editeur/tui.py +0 -0
  43. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/entropie.py +0 -0
  44. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/explorateur.py +0 -0
  45. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/formules/__init__.py +0 -0
  46. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/formules/nodes.py +0 -0
  47. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/formules/parser.py +0 -0
  48. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/__init__.py +0 -0
  49. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/base.py +0 -0
  50. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/langs/python_native.py +0 -0
  51. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/metriques.py +0 -0
  52. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/piste_sorry.py +0 -0
  53. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/rapport.py +0 -0
  54. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity/sondes.py +0 -0
  55. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/dependency_links.txt +0 -0
  56. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/entry_points.txt +0 -0
  57. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/requires.txt +0 -0
  58. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/phi_complexity.egg-info/top_level.txt +0 -0
  59. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/setup.cfg +0 -0
  60. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_adiabatique.py +0 -0
  61. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_analyseur.py +0 -0
  62. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_atteignabilite.py +0 -0
  63. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_bayes.py +0 -0
  64. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_carte.py +0 -0
  65. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_chemins_b6_memoire.py +0 -0
  66. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_chemins_durcissement.py +0 -0
  67. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_chemins_multilemmes.py +0 -0
  68. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_chemins_verifiables.py +0 -0
  69. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_cli.py +0 -0
  70. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_core.py +0 -0
  71. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_croyances.py +0 -0
  72. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_durcissement_sondes.py +0 -0
  73. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_editeur.py +0 -0
  74. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_eft.py +0 -0
  75. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_elagage.py +0 -0
  76. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_encerclement.py +0 -0
  77. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_entropie.py +0 -0
  78. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_explorateur.py +0 -0
  79. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_formules.py +0 -0
  80. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_mission.py +0 -0
  81. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_multilang.py +0 -0
  82. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_piste_sorry.py +0 -0
  83. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_rapport.py +0 -0
  84. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_scoreur_type.py +0 -0
  85. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_sondes.py +0 -0
  86. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_synthese_structures.py +0 -0
  87. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_veille.py +0 -0
  88. {phi_complexity-0.13.1 → phi_complexity-0.14.1}/tests/test_vitesse.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: phi-complexity
3
- Version: 0.13.1
3
+ Version: 0.14.1
4
4
  Summary: Multi-language code quality metrics based on Golden Ratio (φ) mathematical invariants and adiabatic equilibrium
5
5
  Author-email: Tomy Verreault <contact@phidelia.dev>
6
6
  License: MIT
@@ -50,7 +50,10 @@ Unlike `pylint` (cultural rules) or `radon` (McCabe metrics), `phi-complexity` a
50
50
  pip install git+https://github.com/spockoo/Phi-Complexity.git
51
51
  ```
52
52
 
53
- > Note : le paquet est aussi sur PyPI (`pip install phi-complexity`, 0.12.0) — identique à cet arbre depuis le 2026-10-01.
53
+ > Note : le paquet est aussi sur PyPI (`pip install phi-complexity`, 0.14.1).
54
+ > Nouveauté 0.14.1 : parseur Lean 4 autonome (stdlib uniquement, zéro dépendance
55
+ > externe) — l'analyse Lean ne nécessite plus tree-sitter ; durcissement de
56
+ > l'extraction (la veille ne devient plus aveugle en silence).
54
57
 
55
58
  ```bash
56
59
  # Audit a file
@@ -21,7 +21,10 @@ Unlike `pylint` (cultural rules) or `radon` (McCabe metrics), `phi-complexity` a
21
21
  pip install git+https://github.com/spockoo/Phi-Complexity.git
22
22
  ```
23
23
 
24
- > Note : le paquet est aussi sur PyPI (`pip install phi-complexity`, 0.12.0) — identique à cet arbre depuis le 2026-10-01.
24
+ > Note : le paquet est aussi sur PyPI (`pip install phi-complexity`, 0.14.1).
25
+ > Nouveauté 0.14.1 : parseur Lean 4 autonome (stdlib uniquement, zéro dépendance
26
+ > externe) — l'analyse Lean ne nécessite plus tree-sitter ; durcissement de
27
+ > l'extraction (la veille ne devient plus aveugle en silence).
25
28
 
26
29
  ```bash
27
30
  # Audit a file
@@ -142,6 +142,26 @@ def carte_projet(dossier: str, lang: Optional[str] = None,
142
142
  non_supportes = fichiers_non_supportes(dossier, exclusions=exclusions,
143
143
  langage=lang)
144
144
  avertissements = _avertissements_backend()
145
+ # Verrouillage « zéro tree-sitter silencieux » (2026-10-03) : si des
146
+ # symboles n'existent que grâce au repli robuste (tree-sitter les a
147
+ # avalés), on le dit explicitement dans les avertissements — la clé
148
+ # existe déjà, aucun changement de schéma.
149
+ par_fichier_repli = {}
150
+ for chemin, symboles in index.items():
151
+ n = sum(1 for s in symboles
152
+ if getattr(s, "extraction", "analyseur") == "robuste_repli")
153
+ if n:
154
+ par_fichier_repli[os.path.basename(chemin)] = n
155
+ if par_fichier_repli:
156
+ total_repli = sum(par_fichier_repli.values())
157
+ avertissements.append(
158
+ f"⚠️ EXTRACTION DÉGRADÉE — repli robuste : {total_repli} "
159
+ f"symbole(s) récupéré(s) dans {len(par_fichier_repli)} "
160
+ f"fichier(s) (invisibles à tree-sitter, métriques = proxy "
161
+ f"lignes) : " + ", ".join(
162
+ f"{f} ({n})" for f, n in sorted(par_fichier_repli.items())))
163
+ # (docstring : en mode complet, les clés sont EXACTEMENT celles de
164
+ # v0.7.0 — on n'ajoute AUCUNE clé, on remplit `avertissements`.)
145
165
 
146
166
  fichiers = []
147
167
  radiances = []
@@ -48,7 +48,7 @@ ENTROPIE_CIBLE_NORMALISEE = PHI_INV
48
48
  BANDE_TOLERANCE_ADIABATIQUE = ETA_GOLDEN
49
49
  """Bande passante de tolérance pour l'entropie relative : η_golden = 1 - φ⁻¹ ≈ 0.381966."""
50
50
 
51
- VERSION = "0.12.0"
51
+ VERSION = "0.14.1"
52
52
  AUTEUR = "Tomy Verreault"
53
53
  FRAMEWORK = "Morphic Phi Framework (φ-Meta)"
54
54
 
@@ -577,10 +577,17 @@ def chemins_croyants(dossier: str, lang: Optional[str] = None,
577
577
  sorry_modes = set()
578
578
 
579
579
  croyances: List[CroyanceSymbole] = []
580
+ # Durcissement 2026-10-02 : par fichier, combien de symboles viennent
581
+ # du repli robuste (tree-sitter aveugle). Jamais silencieux.
582
+ extraction_repli: Dict[str, int] = {}
580
583
  for f in fichiers:
581
584
  c = canon[f]
582
585
  rel = os.path.relpath(c, dossier_abs)
583
586
  symboles = sorted(index[f], key=lambda s: s.ligne)
587
+ n_repli = sum(1 for s in symboles
588
+ if getattr(s, "extraction", "analyseur") == "robuste_repli")
589
+ if n_repli:
590
+ extraction_repli[rel] = n_repli
584
591
  sorry_map, mode = _sorry_par_symbole(f, symboles)
585
592
  sorry_modes.add(mode)
586
593
  if mode == "grep_repli":
@@ -676,6 +683,10 @@ def chemins_croyants(dossier: str, lang: Optional[str] = None,
676
683
  "imports_non_resolus": non_resolus[:20],
677
684
  "nb_imports_non_resolus": len(non_resolus),
678
685
  "limites": LIMITE_HONNETETE,
686
+ # Durcissement 2026-10-02 : fichiers où le repli robuste a récupéré
687
+ # des symboles invisibles à tree-sitter ({fichier_rel: n}).
688
+ # Absent/vide = extraction nominale partout.
689
+ "extraction_repli": extraction_repli,
679
690
  }
680
691
  if mode_exact:
681
692
  # Clé EFT : uniquement en mode exact (JSON flottant inchangé).
@@ -1,11 +1,12 @@
1
1
  """Extraction des dépendances entre définitions Lean.
2
2
 
3
- Utilise le parseur tree-sitter (parseur_lean.py) pour :
4
- 1. Extraire toutes les déclarations d'un fichier.
3
+ Utilise le parseur autonome (parseur_autonome.py, 2026-10-03, autonomie
4
+ stricte) pour :
5
+ 1. Extraire toutes les déclarations d’un fichier (spans exacts).
5
6
  2. Pour chaque déclaration, trouver les identifiants référencés dans son corps.
6
7
  3. Construire le graphe de dépendances.
7
8
 
8
- Limites honnêtes :
9
+ Zéro dépendance externe. Limites honnêtes :
9
10
  - Syntaxique, pas sémantique : on extrait les identifiants textuels,
10
11
  sans résolution de noms complète (pas de distinction entre
11
12
  variable liée et référence globale sans analyse de portée).
@@ -16,10 +17,10 @@ Limites honnêtes :
16
17
 
17
18
  from __future__ import annotations
18
19
 
20
+ import re
19
21
  from dataclasses import dataclass, field
20
22
  from typing import Dict, List, Set
21
23
 
22
- from .parseur_lean import parse, extraire_declarations, extraire_declarations_robuste
23
24
 
24
25
 
25
26
  # Mots-clés Lean à ignorer (non des dépendances)
@@ -88,24 +89,39 @@ class GrapheDependances:
88
89
 
89
90
 
90
91
  def extraire_identifiants(code: str) -> Set[str]:
91
- """Extrait tous les identifiants d'un fragment de code Lean."""
92
- try:
93
- racine = parse(code)
94
- except Exception:
95
- return set()
96
- identifiants = set()
97
- for noeud in racine.trouver_tous("identifier"):
98
- texte = noeud.texte.strip()
99
- # Filtrer les mots-clés et les identifiants vides
100
- if texte and texte not in MOTS_CLES and not texte.startswith("_"):
101
- # Ignorer les projections de champ simples (ex: x.val -> on garde x)
102
- # Pour l'instant, on garde le texte brut
103
- identifiants.add(texte)
104
- return identifiants
92
+ """Extrait tous les identifiants d'un fragment de code Lean.
93
+
94
+ Source unique : le lexer du parseur autonome (2026-10-03). Les
95
+ commentaires et chaînes sont exclus par construction (tokens).
96
+ Ne rend jamais un ensemble vide silencieusement : si le code ne
97
+ contient vraiment aucun identifiant, c'est le cas — sinon le
98
+ lexer les voit tous (aucune grammaire externe).
99
+ """
100
+ from .parseur_autonome import tokenize, identifiants
101
+ toks, _avs, _nc = tokenize(code)
102
+ return {nom for nom, _ligne in identifiants(toks)
103
+ if nom and nom not in MOTS_CLES and not nom.startswith("_")}
104
+
105
+
106
+ _RE_IDENT = re.compile(r"[A-Za-z_][A-Za-z0-9_']*")
107
+
108
+
109
+ def _identifiants_regex(code: str) -> Set[str]:
110
+ """Repli regex pour `extraire_identifiants` quand tree-sitter est
111
+ indisponible (jamais de silence : ensemble vide seulement si le
112
+ code n'a vraiment aucun identifiant)."""
113
+ return {m.group(0) for m in _RE_IDENT.finditer(code)
114
+ if m.group(0) not in MOTS_CLES
115
+ and not m.group(0).startswith("_")}
105
116
 
106
117
 
107
118
  def analyser_fichier(chemin: str) -> GrapheDependances:
108
- """Analyse un fichier Lean et construit le graphe de dépendances."""
119
+ """Analyse un fichier Lean et construit le graphe de dépendances.
120
+
121
+ Source unique : parseur autonome — déclarations avec spans de corps
122
+ exacts (pas de découpage par lignes approximatif).
123
+ """
124
+ from .parseur_autonome import parse_declarations
109
125
  graphe = GrapheDependances()
110
126
  try:
111
127
  with open(chemin, 'r', encoding='utf-8') as f:
@@ -113,49 +129,21 @@ def analyser_fichier(chemin: str) -> GrapheDependances:
113
129
  except Exception:
114
130
  return graphe
115
131
 
116
- # Extraire les déclarations (version robuste, pas de limite de taille)
117
- declarations = extraire_declarations_robuste(code)
118
- noms_declares = {d.nom for d in declarations if d.nom}
119
-
120
- # Pour chaque déclaration, extraire les identifiants de son corps
121
- # On utilise une approche simple : découper le code par déclaration
122
- # (une approche plus précise utiliserait les positions AST)
123
- for decl in declarations:
132
+ for decl in parse_declarations(code):
124
133
  if not decl.nom:
125
134
  continue
126
- # Trouver le corps : on prend le texte après ":=" jusqu'à la prochaine déclaration
127
- # Simplification : on analyse tout le fichier et on filtre
128
- # (pour une version précise, il faudrait les offsets AST)
129
- pass
130
-
131
- # Approche précise : extraire le corps entre deux déclarations
132
- # On trie les déclarations par ligne, puis pour chacune,
133
- # le corps va de sa ligne jusqu'à la ligne de la suivante.
134
- lignes = code.split('\n')
135
- decls_triees = sorted(
136
- [d for d in declarations if d.nom and d.ligne],
137
- key=lambda d: d.ligne
138
- )
139
- for idx, decl in enumerate(decls_triees):
140
- # Début : ligne de la déclaration (0-indexed)
141
- debut = decl.ligne - 1
142
- # Fin : ligne de la prochaine déclaration (exclusive),
143
- # ou fin du fichier si c'est la dernière
144
- if idx + 1 < len(decls_triees):
145
- fin = decls_triees[idx + 1].ligne - 1
146
- else:
147
- fin = len(lignes)
148
- # Extraire le bloc exact
149
- bloc = '\n'.join(lignes[debut:fin])
150
- # Extraire les identifiants du bloc
151
- refs = extraire_identifiants(bloc)
152
- # Retirer l'auto-référence
135
+ refs = extraire_identifiants(decl.corps)
153
136
  refs.discard(decl.nom)
137
+ # Retirer les noms liés par les binders (paramètres, pas dépendances)
138
+ for bnom, _impl in decl.binders:
139
+ refs.discard(bnom)
154
140
  graphe.ajouter(decl.nom, refs, chemin, decl.ligne)
141
+ return graphe
155
142
 
156
143
  return graphe
157
144
 
158
145
 
146
+
159
147
  def dependances_pour(nom: str, graphe: GrapheDependances) -> List[str]:
160
148
  """Retourne la liste triée des dépendances transitives d'un nom,
161
149
  en ordre topologique (dépendances d'abord)."""
@@ -71,6 +71,9 @@ class Symbole:
71
71
  complexite: int
72
72
  langage: str
73
73
  fichier: str
74
+ extraction: str = "analyseur" # "analyseur" ou "robuste_repli"
75
+ # (durcissement 2026-10-02 : repli robuste Lean quand tree-sitter
76
+ # a avalé la déclaration — métriques = proxy lignes)
74
77
 
75
78
 
76
79
  # Cache d'invalidation : (chemin, complet) -> mtime de la dernière
@@ -103,6 +106,7 @@ def _symboles_du_fichier(chemin: str, langage: Optional[str] = None,
103
106
  complexite=int(f.complexite),
104
107
  langage=resultat.langage,
105
108
  fichier=chemin,
109
+ extraction=getattr(f, "extraction", "analyseur"),
106
110
  )
107
111
  )
108
112
  except Exception:
@@ -1,5 +1,5 @@
1
1
  """
2
- eft.py — Error-Free Transformations pour phi-complexity (v0.12.0, opt-in).
2
+ eft.py — Error-Free Transformations pour phi-complexity (v0.14.1, opt-in).
3
3
 
4
4
  Références mathématiques :
5
5
  - T. Dekker, « A floating-point technique for extending the available
@@ -41,6 +41,14 @@ from .base import AnalyseurBase
41
41
  # Types de nœuds mot-clé (tels que la grammaire les normalise).
42
42
  _TYPES_DECLARATION = {"theorem", "def", "abbrev", "instance", "structure", "inductive"}
43
43
 
44
+ # Kinds récupérables par le repli robuste : exactement ceux que
45
+ # l'analyseur compte (mots-clés SOURCE, avant normalisation —
46
+ # `lemma`→theorem et `class`→structure sont normalisés par la grammaire).
47
+ # `example` et `opaque` en sont exclus : le repli répare un angle mort,
48
+ # il n'étend pas le périmètre.
49
+ _KINDS_COMPTES = {"theorem", "lemma", "def", "abbrev", "instance",
50
+ "structure", "class", "inductive"}
51
+
44
52
  # Enfants de `declaration` à sauter avant le mot-clé.
45
53
  _PREFIXES_IGNORES = {"decl_modifiers", "attributes"}
46
54
 
@@ -76,17 +84,33 @@ class AnalyseurLean(AnalyseurBase):
76
84
  """
77
85
  Analyseur fractal dédié à Lean 4 : déclarations de tête uniquement,
78
86
  métriques calibrées pour le code de preuve.
87
+
88
+ Moteur d'extraction (2026-10-03, autonomie stricte) :
89
+ - "autonome" (défaut) : parseur_autonome (stdlib uniquement, zéro
90
+ dépendance externe) — source unique sur le chemin Lean ;
91
+ - "tree_sitter" : ancien chemin tree-sitter, conservé pour vérification
92
+ croisée optionnelle (désactivé par défaut).
79
93
  """
80
94
 
81
- def __init__(self, fichier: str):
95
+ def __init__(self, fichier: str, moteur: str = "autonome"):
82
96
  super().__init__(fichier)
97
+ self.moteur = moteur
83
98
  self.tree = None
84
99
  self.source: bytes = b""
85
100
  self.lignes: List[str] = []
86
101
  self.resultat = ResultatAnalyse(fichier=fichier, langage="lean")
87
102
 
88
103
  def charger(self) -> "AnalyseurLean":
89
- """Charge et parse le fichier. Lève ImportError explicite si la grammaire manque."""
104
+ """Charge le fichier. Le moteur autonome ne nécessite rien
105
+ d'externe ; le moteur tree_sitter lève ImportError explicite si
106
+ la grammaire manque."""
107
+ if self.moteur == "autonome":
108
+ with open(self.fichier, "r", encoding="utf-8", errors="replace") as f:
109
+ contenu = f.read()
110
+ self.lignes = contenu.splitlines()
111
+ self.source = contenu.encode("utf-8", errors="replace")
112
+ self._contenu = contenu
113
+ return self
90
114
  try:
91
115
  from tree_sitter_language_pack import get_parser
92
116
  except ImportError as exc:
@@ -113,6 +137,9 @@ class AnalyseurLean(AnalyseurBase):
113
137
  """
114
138
  Analyse : déclarations de tête, métriques, règles, oudjat.
115
139
 
140
+ Moteur "autonome" (défaut) : parseur proprietaire sans dépendance
141
+ externe. Moteur "tree_sitter" : ancien chemin (vérification croisée).
142
+
116
143
  `complet=True` : comportement historique. `complet=False`
117
144
  (phase 1) : noms, lignes, `complexite = nb_lignes` (PROXY explicite,
118
145
  pas le proxy lignes+tactiques du mode complet) ; ni comptage de
@@ -120,6 +147,8 @@ class AnalyseurLean(AnalyseurBase):
120
147
  (sentinelles : `profondeur_max=0`, `distance_fib=0.0`,
121
148
  `phi_ratio=1.0`). L'oudjat est la déclaration la plus longue.
122
149
  """
150
+ if self.moteur == "autonome":
151
+ return self._analyser_autonome(complet=complet)
123
152
  if self.tree is None:
124
153
  self.charger()
125
154
  racine = self.tree.root_node
@@ -134,13 +163,88 @@ class AnalyseurLean(AnalyseurBase):
134
163
  self.resultat.nb_classes += 1
135
164
  elif enfant.type in _COMMENTAIRES:
136
165
  self.resultat.nb_commentaires += 1
166
+ # Durcissement 2026-10-02 : repli robuste — récupère les
167
+ # déclarations avalées par la récupération d'erreur tree-sitter
168
+ # (cause racine : `|expr|` en position de type, voir _repli_robuste).
169
+ self._repli_robuste()
137
170
  if complet:
138
171
  self._regle_fibonacci()
139
172
  self._identifier_oudjat(complet=complet)
140
173
  return self.resultat
141
174
 
142
175
  # ────────────────────────────────────────────────────────
143
- # EXTRACTION DES DÉCLARATIONS
176
+ # MOTEUR AUTONOME (2026-10-03, autonomie stricte)
177
+ # ────────────────────────────────────────────────────────
178
+
179
+ def _analyser_autonome(self, complet: bool = True) -> ResultatAnalyse:
180
+ """Analyse via le parseur autonome (stdlib uniquement).
181
+
182
+ Même contrat de sortie que le chemin tree-sitter : symboles
183
+ comptés = KINDS_COMPTES, complexite = nb_lignes + nb_tactiques
184
+ (mode complet) ou nb_lignes (phase 1, PROXY explicite).
185
+ Les avertissements du parseur deviennent des annotations WARNING
186
+ — jamais silencieux.
187
+ """
188
+ from ..parseur_autonome import parse as parse_autonome, KINDS_COMPTES
189
+ if not hasattr(self, "_contenu"):
190
+ self.charger()
191
+ contenu = self._contenu
192
+ self.resultat.nb_lignes_total = len(self.lignes)
193
+ res = parse_autonome(contenu)
194
+ self.resultat.nb_commentaires = res.nb_commentaires
195
+ for d in res.declarations:
196
+ if d.kind not in KINDS_COMPTES:
197
+ continue
198
+ if d.nom.startswith("_"):
199
+ continue # définitions anonymes, comme avant
200
+ nb_lignes = d.ligne_fin - d.ligne_debut + 1
201
+ if not complet:
202
+ mesure = MetriqueFonction(
203
+ nom=d.nom,
204
+ ligne=d.ligne,
205
+ complexite=nb_lignes, # PROXY phase 1 (voir docstring analyser)
206
+ nb_args=0, # sentinelle : non calculé
207
+ nb_lignes=nb_lignes,
208
+ profondeur_max=0, # sentinelle : non calculée
209
+ distance_fib=0.0, # sentinelle : non calculée
210
+ phi_ratio=1.0, # sentinelle : non calculé
211
+ extraction="autonome",
212
+ )
213
+ else:
214
+ mesure = MetriqueFonction(
215
+ nom=d.nom,
216
+ ligne=d.ligne,
217
+ complexite=nb_lignes + d.nb_tactiques,
218
+ nb_args=d.nb_args,
219
+ nb_lignes=nb_lignes,
220
+ profondeur_max=d.profondeur_by,
221
+ distance_fib=distance_fibonacci(nb_lignes),
222
+ phi_ratio=1.0, # Calculé après, quand la moyenne est connue
223
+ extraction="autonome",
224
+ )
225
+ self.resultat.fonctions.append(mesure)
226
+ if d.kind in ("structure", "class", "inductive"):
227
+ self.resultat.nb_classes += 1
228
+ # Avertissements du parseur → annotations bruyantes.
229
+ for av in res.avertissements:
230
+ extrait = self.lignes[av.ligne - 1].strip() \
231
+ if 0 < av.ligne <= len(self.lignes) else ""
232
+ self.resultat.annotations.append(
233
+ Annotation(
234
+ ligne=av.ligne,
235
+ message=f"PARSEUR AUTONOME : {av.message}",
236
+ niveau="WARNING",
237
+ extrait=extrait,
238
+ categorie="PARSEUR",
239
+ )
240
+ )
241
+ if complet:
242
+ self._regle_fibonacci()
243
+ self._identifier_oudjat(complet=complet)
244
+ return self.resultat
245
+
246
+ # ────────────────────────────────────────────────────────
247
+ # EXTRACTION DES DÉCLARATIONS (chemin tree_sitter)
144
248
  # ────────────────────────────────────────────────────────
145
249
 
146
250
  def _noeud_mot_cle(self, declaration):
@@ -219,9 +323,86 @@ class AnalyseurLean(AnalyseurBase):
219
323
  )
220
324
 
221
325
  # ────────────────────────────────────────────────────────
222
- # MÉTRIQUES SPÉCIFIQUES LEAN
326
+ # REPLI ROBUSTE (durcissement 2026-10-02)
223
327
  # ────────────────────────────────────────────────────────
224
328
 
329
+ def _repli_robuste(self) -> int:
330
+ """Récupère les déclarations avalées par la récupération d'erreur.
331
+
332
+ Cause racine : la grammaire tree-sitter-lean confond les barres
333
+ `|expr|` (valeur absolue / norme) en position de type de retour
334
+ avec une alternative de filtrage `|` ; le nœud ERROR produit
335
+ avale ensuite les déclarations suivantes, qui deviennent
336
+ INVISIBLES à l'analyseur — silencieusement (constaté :
337
+ 169 déclarations vues sur 261 dans scratch_65c_global.lean,
338
+ 412 déclarations manquées sur 60/176 fichiers du dépôt Lean).
339
+
340
+ L'extracteur robuste (regex) est la source de vérité pour
341
+ l'EXISTENCE des symboles ; tree-sitter garde la main sur les
342
+ métriques fines quand il voit la déclaration. Les symboles
343
+ récupérés portent extraction="robuste_repli" et des métriques
344
+ PROXY honnêtes (complexite = nb_lignes du span, comme la
345
+ phase 1) — jamais de métriques inventées, jamais de silence.
346
+
347
+ Seuls les kinds déjà comptés par l'analyseur sont récupérés
348
+ (pas de `example`, pas de `opaque`) : le repli ne crée aucun
349
+ nouveau périmètre, il répare un angle mort.
350
+
351
+ Retourne le nombre de symboles récupérés. Le repli ne lève
352
+ jamais : en cas d'échec il ne fait rien (l'analyse principale
353
+ reste intacte).
354
+ """
355
+ try:
356
+ from ..parseur_lean import extraire_declarations_robuste
357
+ code = self.source.decode("utf-8", errors="replace")
358
+ robustes = sorted(extraire_declarations_robuste(code),
359
+ key=lambda d: d.ligne)
360
+ # Référence de couverture : ce que la passe tree-sitter a
361
+ # VRAIMENT extrait (self.resultat.fonctions) — et non
362
+ # extraire_declarations(), qui ignore structure/inductive
363
+ # alors que l'analyseur les compte (doublons sinon).
364
+ vus = [(f.nom, f.ligne) for f in self.resultat.fonctions]
365
+
366
+ def couvert(d):
367
+ return any(n == d.nom and abs(l - d.ligne) <= 2
368
+ for (n, l) in vus)
369
+
370
+ manquants = [
371
+ d for d in robustes
372
+ if d.kind in _KINDS_COMPTES and not d.nom.startswith("_")
373
+ and not couvert(d)
374
+ ]
375
+ if not manquants:
376
+ return 0
377
+ # Spans : chaque déclaration robuste va jusqu'à la suivante
378
+ # (tous kinds, comme le découpage par grep de _spans_symboles).
379
+ lignes_fin = {}
380
+ for i, d in enumerate(robustes):
381
+ fin = (robustes[i + 1].ligne - 1
382
+ if i + 1 < len(robustes) else len(self.lignes))
383
+ lignes_fin[(d.nom, d.ligne)] = max(d.ligne, fin)
384
+ recuperes = 0
385
+ for d in manquants:
386
+ fin = lignes_fin.get((d.nom, d.ligne), len(self.lignes))
387
+ span = max(1, fin - d.ligne + 1)
388
+ self.resultat.fonctions.append(MetriqueFonction(
389
+ nom=d.nom,
390
+ ligne=d.ligne,
391
+ complexite=span, # PROXY phase 1, explicite
392
+ nb_args=0, # sentinelle : non calculé
393
+ nb_lignes=span,
394
+ profondeur_max=0, # sentinelle : non calculée
395
+ distance_fib=distance_fibonacci(span),
396
+ phi_ratio=1.0, # sentinelle : non calculé
397
+ extraction="robuste_repli",
398
+ ))
399
+ if d.kind in ("structure", "class", "inductive"):
400
+ self.resultat.nb_classes += 1
401
+ recuperes += 1
402
+ return recuperes
403
+ except Exception:
404
+ return 0
405
+
225
406
  def _compter_tactiques(self, declaration) -> int:
226
407
  """Nombre de pas de tactique = enfants nommés directs des blocs `by`."""
227
408
  total = 0
@@ -121,8 +121,12 @@ def analyseur_disponible(chemin: str, langage: Optional[str] = None) -> bool:
121
121
  return False
122
122
  if lang == "python":
123
123
  return True
124
+ if lang == "lean":
125
+ # Parseur autonome (stdlib uniquement, 2026-10-03) : Lean est
126
+ # toujours analysable, sans aucune dépendance externe.
127
+ return True
124
128
  ext = os.path.splitext(chemin)[1].lower()
125
- if ext in EXTENSIONS_ANALYSEUR_DEDIE or lang == "lean":
129
+ if ext in EXTENSIONS_ANALYSEUR_DEDIE:
126
130
  # Analyseur dédié : le pack seul ne suffit pas, il faut la grammaire.
127
131
  return treesitter_disponible() and _grammaire_lean_ok()
128
132
  return treesitter_disponible()
@@ -149,14 +153,16 @@ def obtenir_analyseur(fichier: str, langage: Optional[str] = None) -> AnalyseurB
149
153
  return AnalyseurPython(fichier)
150
154
 
151
155
  ext = os.path.splitext(fichier)[1].lower()
152
- if ext in EXTENSIONS_ANALYSEUR_DEDIE or lang == "lean":
153
- if not treesitter_disponible() or not _grammaire_lean_ok():
154
- raise ImportError(
155
- "Analyse Lean non supportée : grammaire lean indisponible "
156
- "(installez `pip install phi-complexity[multilang]` et vérifiez "
157
- "l'accès réseau pour le téléchargement de la grammaire)."
158
- )
159
- return _analyseur_dedie(ext)(fichier)
160
-
156
+ if ext in EXTENSIONS_ANALYSEUR_DEDIE or lang == "lean":
157
+ # Lean : parseur autonome par défaut (zéro dépendance externe).
158
+ # Le moteur tree_sitter reste disponible en vérification croisée
159
+ # optionnelle : AnalyseurLean(fichier, moteur="tree_sitter").
160
+ return _analyseur_dedie(ext)(fichier)
161
+
162
+ if not treesitter_disponible():
163
+ raise ImportError(
164
+ "Analyse non supportée : `tree-sitter-language-pack` indisponible "
165
+ "(installez `pip install phi-complexity[multilang]`)."
166
+ )
161
167
  from .treesitter_generic import AnalyseurTreeSitter
162
168
  return AnalyseurTreeSitter(fichier, ts_langage=lang)
@@ -151,6 +151,25 @@ class AnalyseurTreeSitter(AnalyseurBase):
151
151
  self.charger()
152
152
  racine = self.tree.root_node
153
153
  self.resultat.nb_lignes_total = len(self.lignes)
154
+ # ────────────────────────────────────────────────────────
155
+ # VERROUILLAGE « zéro tree-sitter silencieux » (2026-10-03) :
156
+ # un arbre avec nœuds ERROR signifie récupération d'erreur —
157
+ # des symboles peuvent manquer SANS aucun signal sinon.
158
+ # On l'annonce en annotation CRITICAL explicite (jamais de
159
+ # silence) ; on ne tente pas de réparer la grammaire externe.
160
+ # ────────────────────────────────────────────────────────
161
+ try:
162
+ if racine.has_error:
163
+ self._annoter(
164
+ 1,
165
+ "⚠️ TREE-SITTER DÉGRADÉ : l'arbre syntaxique contient "
166
+ "des nœuds d'erreur — des symboles peuvent manquer "
167
+ "silencieusement dans cette analyse.",
168
+ "CRITICAL",
169
+ "SOUVERAINETE",
170
+ )
171
+ except Exception:
172
+ pass # la garde ne doit jamais casser l'analyse
154
173
  if complet:
155
174
  self._compter_elements_globaux(racine)
156
175
  self._analyser_fonctions(racine)
@@ -143,6 +143,14 @@ JUSTIFICATIONS = {
143
143
  "croyance montre sa chaîne, pas de boîte noire.",
144
144
  "sert": "instrument",
145
145
  },
146
+ "parseur_autonome": {
147
+ "but": "Parseur Lean 4 proprietaire de phi-complexity (stdlib "
148
+ "uniquement, zero dependance externe) : lexer + descente "
149
+ "recursive sur les en-tetes + delimitation exacte des corps. "
150
+ "Remplace tree-sitter sur tout le chemin Lean depuis "
151
+ "l'autonomie stricte du 2026-10-03.",
152
+ "sert": "instrument",
153
+ },
146
154
  "piste_sorry": {
147
155
  "but": "Piste d'un sorry à travers Lean 4 : inventaire rigoureux "
148
156
  "(commentaires et chaînes exclus), graphe d'imports, chantiers "
@@ -17,6 +17,9 @@ class MetriqueFonction:
17
17
  profondeur_max: int # Imbrication maximale
18
18
  distance_fib: float # Éloignement de la séquence naturelle
19
19
  phi_ratio: float # Rapport complexité/moyenne (idéal: φ)
20
+ extraction: str = "analyseur" # "analyseur" ou "robuste_repli"
21
+ # (durcissement 2026-10-02 : symboles récupérés par l'extracteur
22
+ # robuste quand tree-sitter les a avalés — métriques = proxy lignes)
20
23
 
21
24
 
22
25
  @dataclass
@@ -1,5 +1,5 @@
1
1
  """
2
- oracle.py — Traces d'oracle pour phi-complexity (v0.12.0, opt-in EFT).
2
+ oracle.py — Traces d'oracle pour phi-complexity (v0.14.1, opt-in EFT).
3
3
 
4
4
  Un oracle n'est pas un devin : c'est une chaîne de raisonnement EXHIBÉE.
5
5
  Chaque mise à jour de croyance enregistre :