1111
1212import ast
1313import re
14- from functools import lru_cache
14+ from functools import cache
1515from pathlib import Path
16- from typing import Any , Dict , List , Optional
16+ from typing import Any
1717
1818from .code_auditor_agent import CodeAuditorAgent
1919from .mermaid_renderer import MermaidRenderer
3838_ARGUMENTOS_SIN_TIPO_ESPERADO = {"self" , "cls" }
3939
4040
41- @lru_cache ( maxsize = None )
42- def _parse_programa_oficial (programa_path : Path ) -> Dict [int , dict ]:
41+ @cache
42+ def _parse_programa_oficial (programa_path : Path ) -> dict [int , dict ]:
4343 """Parsea el programa de asignatura oficial en subtemas y semanas por unidad.
4444
4545 El resultado se cachea por ruta (`lru_cache`) porque `audit_all_units`
@@ -59,7 +59,7 @@ def _parse_programa_oficial(programa_path: Path) -> Dict[int, dict]:
5959 content = programa_path .read_text (encoding = "utf-8" )
6060 lines = content .split ("\n " )
6161
62- mapeo : Dict [int , dict ] = {}
62+ mapeo : dict [int , dict ] = {}
6363 unidad_actual = None
6464
6565 for line in lines :
@@ -95,8 +95,8 @@ class ContentAuditorAgent:
9595
9696 def __init__ (
9797 self ,
98- programa_path : Optional [ Path ] = None ,
99- mermaid_renderer : Optional [ MermaidRenderer ] = None ,
98+ programa_path : Path | None = None ,
99+ mermaid_renderer : MermaidRenderer | None = None ,
100100 ) -> None :
101101 self .code_auditor = CodeAuditorAgent ()
102102 self .programa_path = (
@@ -106,7 +106,7 @@ def __init__(
106106 output_dir = Path .cwd () / "notebooks" / "assets" / "diagramas"
107107 )
108108
109- def _audit_latex (self , content : str ) -> List [str ]:
109+ def _audit_latex (self , content : str ) -> list [str ]:
110110 """Detecta delimitadores LaTeX desbalanceados y comandos mal formados.
111111
112112 Args:
@@ -115,7 +115,7 @@ def _audit_latex(self, content: str) -> List[str]:
115115 Returns:
116116 Lista de descripciones de hallazgos; vacía si no hay problemas.
117117 """
118- hallazgos : List [str ] = []
118+ hallazgos : list [str ] = []
119119
120120 sin_fences = re .sub (r"```.*?```" , "" , content , flags = re .DOTALL )
121121 tokens = re .findall (r"\$\$|\$" , sin_fences )
@@ -142,7 +142,7 @@ def _audit_latex(self, content: str) -> List[str]:
142142
143143 return hallazgos
144144
145- def _audit_codigo (self , python_blocks : List [str ]) -> List [str ]:
145+ def _audit_codigo (self , python_blocks : list [str ]) -> list [str ]:
146146 """Audita bloques de código Python de ejemplo (docstrings, type hints, estilo).
147147
148148 Ignora la longitud de línea (PEP8 79 cols) porque el código de
@@ -160,7 +160,7 @@ def _audit_codigo(self, python_blocks: List[str]) -> List[str]:
160160 Returns:
161161 Lista de descripciones de hallazgos; vacía si no hay problemas.
162162 """
163- hallazgos : List [str ] = []
163+ hallazgos : list [str ] = []
164164
165165 for code in python_blocks :
166166 if _CELDA_MAGICA_IPYTHON .search (code ):
@@ -200,7 +200,7 @@ def _audit_codigo(self, python_blocks: List[str]) -> List[str]:
200200
201201 return hallazgos
202202
203- def _audit_pedagogico (self , bloques : List [tuple ], content : str ) -> List [str ]:
203+ def _audit_pedagogico (self , bloques : list [tuple ], content : str ) -> list [str ]:
204204 """Verifica coherencia pedagógica: Hilo de Oro, analogías, y sintaxis Mermaid.
205205
206206 Args:
@@ -210,7 +210,7 @@ def _audit_pedagogico(self, bloques: List[tuple], content: str) -> List[str]:
210210 Returns:
211211 Lista de descripciones de hallazgos; vacía si no hay problemas.
212212 """
213- hallazgos : List [str ] = []
213+ hallazgos : list [str ] = []
214214
215215 idiomas_presentes = {lang for _ , lang , _ in bloques }
216216 hilo_de_oro_esperado = {"pseudocodigo" , "mermaid" , "python" , "pytest" }
@@ -244,7 +244,7 @@ def _audit_pedagogico(self, bloques: List[tuple], content: str) -> List[str]:
244244
245245 return hallazgos
246246
247- def _audit_curricular (self , md_path : Path , content : str ) -> List [str ]:
247+ def _audit_curricular (self , md_path : Path , content : str ) -> list [str ]:
248248 """Verifica cobertura temática y duración contra el programa oficial.
249249
250250 Args:
@@ -264,7 +264,7 @@ def _audit_curricular(self, md_path: Path, content: str) -> List[str]:
264264 if numero_unidad not in mapeo :
265265 return []
266266
267- hallazgos : List [str ] = []
267+ hallazgos : list [str ] = []
268268 info = mapeo [numero_unidad ]
269269 content_lower = content .lower ()
270270
@@ -297,8 +297,8 @@ def _audit_curricular(self, md_path: Path, content: str) -> List[str]:
297297 _UNIT_NUMBER_PATTERN = re .compile (r"unit_number\s*=\s*(\d+)" )
298298
299299 def _verifica_unit_number (
300- self , python_blocks : List [str ], md_path : Path
301- ) -> List [str ]:
300+ self , python_blocks : list [str ], md_path : Path
301+ ) -> list [str ]:
302302 """Verifica que unit_number en la celda de auto-evaluación coincida
303303 con el número de unidad indicado por el nombre del archivo.
304304
@@ -316,7 +316,7 @@ def _verifica_unit_number(
316316 return []
317317 numero_esperado = int (unidad_match .group (1 ))
318318
319- hallazgos : List [str ] = []
319+ hallazgos : list [str ] = []
320320 for code in python_blocks :
321321 match = self ._UNIT_NUMBER_PATTERN .search (code )
322322 if match and int (match .group (1 )) != numero_esperado :
@@ -328,7 +328,7 @@ def _verifica_unit_number(
328328
329329 _WRITEFILE_LINEA = re .compile (r"^\s*%%writefile\s+(\S+)" , re .MULTILINE )
330330
331- def _extrae_writefiles_reales (self , python_blocks : List [str ]) -> List [tuple ]:
331+ def _extrae_writefiles_reales (self , python_blocks : list [str ]) -> list [tuple ]:
332332 """Extrae líneas reales de %%writefile (primera línea no en blanco
333333 de una celda), ignorando menciones en prosa o dentro de f-strings.
334334
@@ -345,7 +345,7 @@ def _extrae_writefiles_reales(self, python_blocks: List[str]) -> List[tuple]:
345345 return resultado
346346
347347 def _tiene_definicion_previa (
348- self , python_blocks : List [str ], indice_bloque : int
348+ self , python_blocks : list [str ], indice_bloque : int
349349 ) -> bool :
350350 """Verifica si algún bloque anterior a indice_bloque define una
351351 función o clase (ast.parse + ast.walk), tolerando bloques con
@@ -372,8 +372,8 @@ def _tiene_definicion_previa(
372372 return False
373373
374374 def _verifica_writefiles_con_definicion (
375- self , python_blocks : List [str ]
376- ) -> List [str ]:
375+ self , python_blocks : list [str ]
376+ ) -> list [str ]:
377377 """Verifica que todo %%writefile real tenga una definición de
378378 función/clase en algún bloque anterior del mismo MD.
379379
@@ -384,7 +384,7 @@ def _verifica_writefiles_con_definicion(
384384 Lista con un hallazgo por cada %%writefile sin definición
385385 previa; vacía si todos tienen definición o no hay ninguno.
386386 """
387- hallazgos : List [str ] = []
387+ hallazgos : list [str ] = []
388388 for nombre_archivo , indice_bloque in self ._extrae_writefiles_reales (
389389 python_blocks
390390 ):
@@ -396,7 +396,7 @@ def _verifica_writefiles_con_definicion(
396396 )
397397 return hallazgos
398398
399- def _verifica_fences_balanceados (self , content : str ) -> List [str ]:
399+ def _verifica_fences_balanceados (self , content : str ) -> list [str ]:
400400 """Detecta fences de apertura que extract_fenced_blocks() no pudo
401401 cerrar correctamente — señal indirecta: el último bloque extraído
402402 consume texto hasta el final del documento, aunque el .md real
@@ -435,14 +435,16 @@ def _verifica_fences_balanceados(self, content: str) -> List[str]:
435435 )
436436 if not cierre_encontrado :
437437 return [
438- f"Posible fence de código sin cerrar: un bloque abierto con "
439- f"'{ ultimo_fence } ' no encuentra su línea de cierre exacta en "
440- "el resto del documento — extract_fenced_blocks() pudo haber "
441- "consumido contenido no destinado a ser código."
438+ (
439+ f"Posible fence de código sin cerrar: un bloque abierto con "
440+ f"'{ ultimo_fence } ' no encuentra su línea de cierre exacta en "
441+ "el resto del documento — extract_fenced_blocks() pudo haber "
442+ "consumido contenido no destinado a ser código."
443+ )
442444 ]
443445 return []
444446
445- def _verifica_celda_setup (self , content : str ) -> List [str ]:
447+ def _verifica_celda_setup (self , content : str ) -> list [str ]:
446448 """Verifica que la celda estándar de setup (git clone + os.chdir)
447449 esté presente en el MD.
448450
@@ -454,8 +456,10 @@ def _verifica_celda_setup(self, content: str) -> List[str]:
454456 """
455457 if "git clone" not in content or "os.chdir" not in content :
456458 return [
457- "No se encontró la celda de setup estándar (git clone + "
458- "os.chdir) en esta unidad."
459+ (
460+ "No se encontró la celda de setup estándar (git clone + "
461+ "os.chdir) en esta unidad."
462+ )
459463 ]
460464 return []
461465
@@ -464,7 +468,7 @@ def _verifica_celda_setup(self, content: str) -> List[str]:
464468 r"^- La secuencia de la unidad anterior"
465469 )
466470
467- def _verifica_seccion_prerequisitos (self , content : str ) -> List [str ]:
471+ def _verifica_seccion_prerequisitos (self , content : str ) -> list [str ]:
468472 """Verifica que la sección de prerequisitos, si existe, tenga al
469473 menos una línea con el formato parseable esperado por
470474 CurriculumMapAgent.render_dag() (relación estructurada o la línea
@@ -499,16 +503,18 @@ def _verifica_seccion_prerequisitos(self, content: str) -> List[str]:
499503 return []
500504
501505 return [
502- "La sección '## 📚 Prerequisitos de esta unidad' existe pero "
503- "ninguna de sus líneas sigue el formato esperado "
504- "('- **término** (Unidad N)' o la línea alternativa de "
505- "'sin conceptos adicionales') — CurriculumMapAgent.render_dag() "
506- "no podrá extraer ninguna relación de esta unidad."
506+ (
507+ "La sección '## 📚 Prerequisitos de esta unidad' existe pero "
508+ "ninguna de sus líneas sigue el formato esperado "
509+ "('- **término** (Unidad N)' o la línea alternativa de "
510+ "'sin conceptos adicionales') — CurriculumMapAgent.render_dag() "
511+ "no podrá extraer ninguna relación de esta unidad."
512+ )
507513 ]
508514
509515 def _audit_invariantes_estructurales (
510- self , python_blocks : List [str ], content : str , md_path : Path
511- ) -> List [str ]:
516+ self , python_blocks : list [str ], content : str , md_path : Path
517+ ) -> list [str ]:
512518 """Audita invariantes estructurales: consistencia de la celda de
513519 auto-evaluación (donde ya existe), consistencia de la sección de
514520 prerequisitos (donde ya existe), e invariantes generales del
@@ -522,15 +528,15 @@ def _audit_invariantes_estructurales(
522528 Returns:
523529 Lista concatenada de hallazgos de las 5 verificaciones.
524530 """
525- hallazgos : List [str ] = []
531+ hallazgos : list [str ] = []
526532 hallazgos .extend (self ._verifica_unit_number (python_blocks , md_path ))
527533 hallazgos .extend (self ._verifica_writefiles_con_definicion (python_blocks ))
528534 hallazgos .extend (self ._verifica_fences_balanceados (content ))
529535 hallazgos .extend (self ._verifica_celda_setup (content ))
530536 hallazgos .extend (self ._verifica_seccion_prerequisitos (content ))
531537 return hallazgos
532538
533- def audit_unit (self , md_path : Path ) -> Dict [str , Any ]:
539+ def audit_unit (self , md_path : Path ) -> dict [str , Any ]:
534540 """Audita una unidad del curso contra las 5 dimensiones de calidad.
535541
536542 Args:
0 commit comments