evolutia 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

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.
@@ -1,160 +1,186 @@
1
- """
2
- Utilidades para parsear archivos Markdown/MyST y extraer ejercicios y soluciones.
3
- """
4
- import re
5
- import yaml
6
- from pathlib import Path
7
- from typing import Dict, List, Optional, Tuple
8
-
9
-
10
- def extract_frontmatter(content: str) -> Tuple[Dict, str]:
11
- """
12
- Extrae el frontmatter YAML del contenido Markdown.
13
-
14
- Args:
15
- content: Contenido completo del archivo
16
-
17
- Returns:
18
- Tupla (frontmatter_dict, contenido_sin_frontmatter)
19
- """
20
- frontmatter_pattern = r'^---\s*\n(.*?)\n---\s*\n'
21
- match = re.match(frontmatter_pattern, content, re.DOTALL)
22
-
23
- if match:
24
- frontmatter_str = match.group(1)
25
- try:
26
- frontmatter = yaml.safe_load(frontmatter_str) or {}
27
- content_without_frontmatter = content[match.end():]
28
- return frontmatter, content_without_frontmatter
29
- except yaml.YAMLError:
30
- return {}, content
31
- return {}, content
32
-
33
-
34
- def extract_exercise_blocks(content: str) -> List[Dict]:
35
- """
36
- Extrae bloques de ejercicio del formato MyST.
37
-
38
- Busca bloques del tipo:
39
- ```{exercise} N
40
- :label: exN-XX
41
- ...
42
- ```
43
-
44
- Args:
45
- content: Contenido Markdown
46
-
47
- Returns:
48
- Lista de diccionarios con información de cada ejercicio
49
- """
50
- exercises = []
51
-
52
- # Patrón para bloques de ejercicio MyST
53
- # Captura delimitador (grupo 1), label (grupo 2) y contenido (grupo 3)
54
- # Usa backreference \1 para coincidir con la longitud exacta del delimitador de cierre
55
- exercise_pattern = r'(`{3,4})\{exercise\}(?:\s+\d+)?\s*\n:label:\s+(\S+)\s*\n(.*?)(?=\1)'
56
-
57
- matches = re.finditer(exercise_pattern, content, re.DOTALL)
58
-
59
- for match in matches:
60
- # group(1) es el delimitador
61
- label = match.group(2)
62
- exercise_content = match.group(3).strip()
63
-
64
- # Buscar si hay un include dentro
65
- include_match = re.search(r'```\{include\}\s+(.+?)\s*```', exercise_content, re.DOTALL)
66
- if include_match:
67
- include_path = include_match.group(1).strip()
68
- exercises.append({
69
- 'label': label,
70
- 'content': exercise_content,
71
- 'include_path': include_path,
72
- 'type': 'include'
73
- })
74
- else:
75
- exercises.append({
76
- 'label': label,
77
- 'content': exercise_content,
78
- 'include_path': None,
79
- 'type': 'inline'
80
- })
81
-
82
- return exercises
83
-
84
-
85
- def extract_solution_blocks(content: str) -> List[Dict]:
86
- """
87
- Extrae bloques de solución del formato MyST.
88
-
89
- Busca bloques del tipo:
90
- ````{solution} exN-XX
91
- :label: solution-exN-XX
92
- ...
93
- ````
94
-
95
- Args:
96
- content: Contenido Markdown
97
-
98
- Returns:
99
- Lista de diccionarios con información de cada solución
100
- """
101
- solutions = []
102
-
103
- # Patrón para bloques de solución MyST
104
- # Captura delimitador (grupo 1), exercise_label (grupo 2), solution_label (grupo 3), contenido (grupo 4)
105
- solution_pattern = r'(`{3,4})\{solution\}\s+(\S+)\s*\n:label:\s+(\S+)\s*\n(.*?)(?=\1)'
106
-
107
- matches = re.finditer(solution_pattern, content, re.DOTALL)
108
-
109
- for match in matches:
110
- # group(1) es delimitador
111
- exercise_label = match.group(2)
112
- solution_label = match.group(3)
113
- solution_content = match.group(4).strip()
114
-
115
- # Buscar includes dentro de la solución
116
- include_matches = re.finditer(r'```\{include\}\s+(.+?)\s*```', solution_content, re.DOTALL)
117
- include_paths = [m.group(1).strip() for m in include_matches]
118
-
119
- solutions.append({
120
- 'exercise_label': exercise_label,
121
- 'label': solution_label,
122
- 'content': solution_content,
123
- 'include_paths': include_paths
124
- })
125
-
126
- return solutions
127
-
128
-
129
- def read_markdown_file(file_path: Path) -> str:
130
- """
131
- Lee un archivo Markdown y retorna su contenido.
132
-
133
- Args:
134
- file_path: Ruta al archivo
135
-
136
- Returns:
137
- Contenido del archivo
138
- """
139
- try:
140
- with open(file_path, 'r', encoding='utf-8') as f:
141
- return f.read()
142
- except Exception as e:
143
- raise IOError(f"Error leyendo archivo {file_path}: {e}")
144
-
145
-
146
- def resolve_include_path(include_path: str, base_dir: Path) -> Path:
147
- """
148
- Resuelve una ruta de include relativa a un directorio base.
149
-
150
- Args:
151
- include_path: Ruta relativa del include
152
- base_dir: Directorio base
153
-
154
- Returns:
155
- Ruta absoluta resuelta
156
- """
157
- # Limpiar la ruta (puede tener ./ o espacios)
158
- clean_path = include_path.strip().lstrip('./')
159
- return (base_dir / clean_path).resolve()
1
+ """
2
+ Utilidades para parsear archivos Markdown/MyST y extraer ejercicios y soluciones.
3
+ """
4
+ import re
5
+ import yaml
6
+ import logging
7
+ from pathlib import Path
8
+ from typing import Dict, List, Optional, Tuple, Union
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def extract_frontmatter(content: str) -> Tuple[Dict[str, str], str]:
14
+ """
15
+ Extrae el frontmatter YAML del contenido Markdown.
16
+
17
+ Args:
18
+ content: Contenido completo del archivo
19
+
20
+ Returns:
21
+ Tupla (frontmatter_dict, contenido_sin_frontmatter)
22
+ """
23
+ if not content:
24
+ return {}, ""
25
+
26
+ frontmatter_pattern = r'^---\s*\n(.*?)\n---\s*\n'
27
+ match = re.match(frontmatter_pattern, content, re.DOTALL)
28
+
29
+ if match:
30
+ frontmatter_str = match.group(1)
31
+ try:
32
+ frontmatter = yaml.safe_load(frontmatter_str) or {}
33
+ content_without_frontmatter = content[match.end():]
34
+ logger.debug(f"[MarkdownParser] Frontmatter extraído: {len(frontmatter)} campos")
35
+ return frontmatter, content_without_frontmatter
36
+ except yaml.YAMLError as e:
37
+ logger.warning(f"[MarkdownParser] Error parseando YAML frontmatter: {e}")
38
+ return {}, content
39
+ logger.debug("[MarkdownParser] No se encontró frontmatter")
40
+ return {}, content
41
+
42
+
43
+ def extract_exercise_blocks(content: str) -> List[Dict[str, Union[str, None]]]:
44
+ """
45
+ Extrae bloques de ejercicio del formato MyST.
46
+
47
+ Busca bloques del tipo:
48
+ ```{exercise} N
49
+ :label: exN-XX
50
+ ...
51
+ ```
52
+
53
+ Args:
54
+ content: Contenido Markdown
55
+
56
+ Returns:
57
+ Lista de diccionarios con información de cada ejercicio
58
+ """
59
+ exercises = []
60
+
61
+ if not content:
62
+ return exercises
63
+
64
+ # Patrón para bloques de ejercicio MyST
65
+ # Captura delimitador (grupo 1), label (grupo 2) y contenido (grupo 3)
66
+ # Usa backreference \1 para coincidir con la longitud exacta del delimitador de cierre
67
+ exercise_pattern = r'(`{3,4})\{exercise\}(?:\s+\d+)?\s*\n:label:\s+(\S+)\s*\n(.*?)(?=\1)'
68
+
69
+ matches = re.finditer(exercise_pattern, content, re.DOTALL)
70
+
71
+ for match in matches:
72
+ # group(1) es el delimitador
73
+ label = match.group(2)
74
+ exercise_content = match.group(3).strip()
75
+
76
+ # Buscar si hay un include dentro
77
+ include_match = re.search(r'```\{include\}\s+(.+?)\s*```', exercise_content, re.DOTALL)
78
+ if include_match:
79
+ include_path = include_match.group(1).strip()
80
+ exercises.append({
81
+ 'label': label,
82
+ 'content': exercise_content,
83
+ 'include_path': include_path,
84
+ 'type': 'include'
85
+ })
86
+ else:
87
+ exercises.append({
88
+ 'label': label,
89
+ 'content': exercise_content,
90
+ 'include_path': None,
91
+ 'type': 'inline'
92
+ })
93
+
94
+ logger.debug(f"[MarkdownParser] Extraídos {len(exercises)} bloques de ejercicio")
95
+ return exercises
96
+
97
+
98
+ def extract_solution_blocks(content: str) -> List[Dict[str, Union[str, List[str]]]]:
99
+ """
100
+ Extrae bloques de solución del formato MyST.
101
+
102
+ Busca bloques del tipo:
103
+ ````{solution} exN-XX
104
+ :label: solution-exN-XX
105
+ ...
106
+ ````
107
+
108
+ Args:
109
+ content: Contenido Markdown
110
+
111
+ Returns:
112
+ Lista de diccionarios con información de cada solución
113
+ """
114
+ solutions = []
115
+
116
+ if not content:
117
+ return solutions
118
+
119
+ # Patrón para bloques de solución MyST
120
+ # Captura delimitador (grupo 1), exercise_label (grupo 2), solution_label (grupo 3), contenido (grupo 4)
121
+ solution_pattern = r'(`{3,4})\{solution\}\s+(\S+)\s*\n:label:\s+(\S+)\s*\n(.*?)(?=\1)'
122
+
123
+ matches = re.finditer(solution_pattern, content, re.DOTALL)
124
+
125
+ for match in matches:
126
+ # group(1) es delimitador
127
+ exercise_label = match.group(2)
128
+ solution_label = match.group(3)
129
+ solution_content = match.group(4).strip()
130
+
131
+ # Buscar includes dentro de la solución
132
+ include_matches = re.finditer(r'```\{include\}\s+(.+?)\s*```', solution_content, re.DOTALL)
133
+ include_paths = [m.group(1).strip() for m in include_matches]
134
+
135
+ solutions.append({
136
+ 'exercise_label': exercise_label,
137
+ 'label': solution_label,
138
+ 'content': solution_content,
139
+ 'include_paths': include_paths
140
+ })
141
+
142
+ logger.debug(f"[MarkdownParser] Extraídos {len(solutions)} bloques de solución")
143
+ return solutions
144
+
145
+
146
+ def read_markdown_file(file_path: Union[Path, str]) -> str:
147
+ """
148
+ Lee un archivo Markdown y retorna su contenido.
149
+
150
+ Args:
151
+ file_path: Ruta al archivo
152
+
153
+ Returns:
154
+ Contenido del archivo
155
+
156
+ Raises:
157
+ IOError: Si hay error leyendo el archivo
158
+ """
159
+ file_path = Path(file_path)
160
+ try:
161
+ with open(file_path, 'r', encoding='utf-8') as f:
162
+ content = f.read()
163
+ logger.debug(f"[MarkdownParser] Archivo leído exitosamente: {file_path} (longitud={len(content)})")
164
+ return content
165
+ except Exception as e:
166
+ logger.error(f"[MarkdownParser] Error leyendo archivo {file_path}: {e}")
167
+ raise IOError(f"Error leyendo archivo {file_path}: {e}")
168
+
169
+
170
+ def resolve_include_path(include_path: str, base_dir: Union[Path, str]) -> Path:
171
+ """
172
+ Resuelve una ruta de include relativa a un directorio base.
173
+
174
+ Args:
175
+ include_path: Ruta relativa del include
176
+ base_dir: Directorio base
177
+
178
+ Returns:
179
+ Ruta absoluta resuelta
180
+ """
181
+ # Limpiar la ruta (puede tener ./ o espacios)
182
+ clean_path = include_path.strip().lstrip('./')
183
+ resolved_path = (Path(base_dir) / clean_path).resolve()
184
+ logger.debug(f"[MarkdownParser] Ruta include resuelta: {include_path} -> {resolved_path}")
185
+ return resolved_path
160
186
 
@@ -1,144 +1,153 @@
1
- """
2
- Utilidades para extraer y analizar expresiones matemáticas de archivos Markdown.
3
- """
4
- import re
5
- from typing import List, Dict, Set
6
-
7
- # Patrones comunes para variables
8
- # Variables latinas: \vec{A}, A, \mathbf{B}, etc.
9
- LATIN_PATTERN = re.compile(r'\\vec\{([A-Za-z])\}|\\mathbf\{([A-Za-z])\}|\\hat\{([A-Za-z])\}|([A-Za-z])(?![a-z])')
10
-
11
- # Letras griegas: \alpha, \beta, \theta, etc.
12
- GREEK_PATTERN = re.compile(r'\\(alpha|beta|gamma|delta|epsilon|theta|phi|rho|omega|sigma|lambda|mu|nu|pi|tau)')
13
-
14
-
15
- def extract_math_expressions(content: str) -> List[str]:
16
- r"""
17
- Extrae todas las expresiones matemáticas del contenido.
18
-
19
- Busca expresiones en formato LaTeX:
20
- - Inline: $...$ o \(...\)
21
- - Display: $$...$$ o \[...\]
22
- - Math blocks: :::{math} ... :::
23
-
24
- Args:
25
- content: Contenido Markdown
26
-
27
- Returns:
28
- Lista de expresiones matemáticas encontradas
29
- """
30
- expressions = []
31
-
32
- # 1. Bloques math de MyST: :::{math} ... :::
33
- # Se procesan primero y se eliminan del contenido para evitar duplicados si contienen $ o $$
34
- math_block_pattern = r':::\{math\}\s*(.*?)\s*:::'
35
- for match in re.finditer(math_block_pattern, content, re.DOTALL):
36
- expr = match.group(1).strip()
37
- if expr:
38
- expressions.append(expr)
39
- content = re.sub(math_block_pattern, '', content, flags=re.DOTALL)
40
-
41
- # 2. Expresiones display: $$...$$ o \[...\]
42
- display_pattern = r'\$\$([^$]+)\$\$|\\\[([^\]]+)\\\]'
43
- for match in re.finditer(display_pattern, content, re.DOTALL):
44
- expr = match.group(1) or match.group(2)
45
- if expr:
46
- expressions.append(expr.strip())
47
- content = re.sub(display_pattern, '', content, flags=re.DOTALL)
48
-
49
- # 3. Expresiones inline: $...$ o \(...\)
50
- inline_pattern = r'\$([^$]+)\$|\\\(([^\)]+)\\\)'
51
- for match in re.finditer(inline_pattern, content):
52
- expr = match.group(1) or match.group(2)
53
- if expr:
54
- expressions.append(expr.strip())
55
-
56
- return expressions
57
-
58
-
59
- def extract_variables(math_expressions: List[str]) -> Set[str]:
60
- """
61
- Extrae variables de expresiones matemáticas.
62
-
63
- Identifica letras griegas, variables latinas, y símbolos comunes.
64
-
65
- Args:
66
- math_expressions: Lista de expresiones matemáticas
67
-
68
- Returns:
69
- Conjunto de variables identificadas
70
- """
71
- variables = set()
72
-
73
- for expr in math_expressions:
74
- # Buscar variables latinas
75
- for match in LATIN_PATTERN.finditer(expr):
76
- var = match.group(1) or match.group(2) or match.group(3) or match.group(4)
77
- if var and var.isalpha():
78
- variables.add(var)
79
-
80
- # Buscar letras griegas
81
- for match in GREEK_PATTERN.finditer(expr):
82
- variables.add(match.group(1))
83
-
84
- return variables
85
-
86
-
87
- def count_math_operations(expression: str) -> Dict[str, int]:
88
- """
89
- Cuenta operaciones matemáticas en una expresión.
90
-
91
- Args:
92
- expression: Expresión matemática
93
-
94
- Returns:
95
- Diccionario con conteo de operaciones
96
- """
97
- operations = {
98
- 'integrals': len(re.findall(r'\\int|\\oint', expression)),
99
- 'derivatives': len(re.findall(r'\\partial|\\nabla|\\frac\{d', expression)),
100
- 'sums': len(re.findall(r'\\sum|\\prod', expression)),
101
- 'vectors': len(re.findall(r'\\vec|\\mathbf', expression)),
102
- 'matrices': len(re.findall(r'\\begin\{matrix\}|\\begin\{pmatrix\}|\\begin\{bmatrix\}', expression)),
103
- 'functions': len(re.findall(r'\\sin|\\cos|\\tan|\\exp|\\log|\\ln', expression)),
104
- }
105
- return operations
106
-
107
-
108
- def estimate_complexity(expressions: List[str]) -> float:
109
- """
110
- Estima la complejidad matemática de un conjunto de expresiones.
111
-
112
- Args:
113
- expressions: Lista de expresiones matemáticas
114
-
115
- Returns:
116
- Puntuación de complejidad (mayor = más complejo)
117
- """
118
- if not expressions:
119
- return 0.0
120
-
121
- total_complexity = 0.0
122
-
123
- for expr in expressions:
124
- # Longitud de la expresión
125
- total_complexity += len(expr) * 0.01
126
-
127
- # Operaciones complejas
128
- ops = count_math_operations(expr)
129
- total_complexity += ops['integrals'] * 2.0
130
- total_complexity += ops['derivatives'] * 1.5
131
- total_complexity += ops['sums'] * 1.5
132
- total_complexity += ops['vectors'] * 1.0
133
- total_complexity += ops['matrices'] * 2.5
134
- total_complexity += ops['functions'] * 0.5
135
-
136
- # Número de variables
137
- vars_count = len(extract_variables([expr]))
138
- total_complexity += vars_count * 0.3
139
-
140
- # Bloques align (ecuaciones múltiples)
141
- if '\\begin{align' in expr or '\\begin{aligned' in expr:
142
- total_complexity += 2.0
143
-
144
- return total_complexity
1
+ """
2
+ Utilidades para extraer y analizar expresiones matemáticas de archivos Markdown.
3
+ """
4
+ import re
5
+ import logging
6
+ from typing import List, Dict, Set, Tuple
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+ # Patrones comunes para variables
11
+ # Variables latinas: \vec{A}, A, \mathbf{B}, etc.
12
+ LATIN_PATTERN = re.compile(r'\\vec\{([A-Za-z])\}|\\mathbf\{([A-Za-z])\}|\\hat\{([A-Za-z])\}|([A-Za-z])(?![a-z])')
13
+
14
+ # Letras griegas: \alpha, \beta, \theta, etc.
15
+ GREEK_PATTERN = re.compile(r'\\(alpha|beta|gamma|delta|epsilon|theta|phi|rho|omega|sigma|lambda|mu|nu|pi|tau)')
16
+
17
+
18
+ def extract_math_expressions(content: str) -> List[str]:
19
+ r"""
20
+ Extrae todas las expresiones matemáticas del contenido.
21
+
22
+ Busca expresiones en formato LaTeX:
23
+ - Inline: $...$ o \(...\)
24
+ - Display: $$...$$ o \[...\]
25
+ - Math blocks: :::{math} ... :::
26
+
27
+ Args:
28
+ content: Contenido Markdown
29
+
30
+ Returns:
31
+ Lista de expresiones matemáticas encontradas
32
+ """
33
+ if not content:
34
+ return []
35
+
36
+ expressions = []
37
+
38
+ # 1. Bloques math de MyST: :::{math} ... :::
39
+ # Se procesan primero y se eliminan del contenido para evitar duplicados si contienen $ o $$
40
+ math_block_pattern = r':::\{math\}\s*(.*?)\s*:::'
41
+ for match in re.finditer(math_block_pattern, content, re.DOTALL):
42
+ expr = match.group(1).strip()
43
+ if expr:
44
+ expressions.append(expr)
45
+ content = re.sub(math_block_pattern, '', content, flags=re.DOTALL)
46
+
47
+ # 2. Expresiones display: $$...$$ o \[...\]
48
+ display_pattern = r'\$\$([^$]+)\$\$|\\\[([^\]]+)\\\]'
49
+ for match in re.finditer(display_pattern, content, re.DOTALL):
50
+ expr = match.group(1) or match.group(2)
51
+ if expr:
52
+ expressions.append(expr.strip())
53
+ content = re.sub(display_pattern, '', content, flags=re.DOTALL)
54
+
55
+ # 3. Expresiones inline: $...$ o \(...\)
56
+ inline_pattern = r'\$([^$]+)\$|\\\(([^\)]+)\\\)'
57
+ for match in re.finditer(inline_pattern, content):
58
+ expr = match.group(1) or match.group(2)
59
+ if expr:
60
+ expressions.append(expr.strip())
61
+
62
+ logger.debug(f"[MathExtractor] Extraídas {len(expressions)} expresiones matemáticas del contenido")
63
+ return expressions
64
+
65
+
66
+ def extract_variables(math_expressions: List[str]) -> Set[str]:
67
+ """
68
+ Extrae variables de expresiones matemáticas.
69
+
70
+ Identifica letras griegas, variables latinas, y símbolos comunes.
71
+
72
+ Args:
73
+ math_expressions: Lista de expresiones matemáticas
74
+
75
+ Returns:
76
+ Conjunto de variables identificadas
77
+ """
78
+ variables = set()
79
+
80
+ for expr in math_expressions:
81
+ # Buscar variables latinas
82
+ for match in LATIN_PATTERN.finditer(expr):
83
+ var = match.group(1) or match.group(2) or match.group(3) or match.group(4)
84
+ if var and var.isalpha():
85
+ variables.add(var)
86
+
87
+ # Buscar letras griegas
88
+ for match in GREEK_PATTERN.finditer(expr):
89
+ variables.add(match.group(1))
90
+
91
+ logger.debug(f"[MathExtractor] Extraídas {len(variables)} variables de {len(math_expressions)} expresiones")
92
+ return variables
93
+
94
+
95
+ def count_math_operations(expression: str) -> Dict[str, int]:
96
+ """
97
+ Cuenta operaciones matemáticas en una expresión.
98
+
99
+ Args:
100
+ expression: Expresión matemática
101
+
102
+ Returns:
103
+ Diccionario con conteo de operaciones
104
+ """
105
+ operations = {
106
+ 'integrals': len(re.findall(r'\\int|\\oint', expression)),
107
+ 'derivatives': len(re.findall(r'\\partial|\\nabla|\\frac\{d', expression)),
108
+ 'sums': len(re.findall(r'\\sum|\\prod', expression)),
109
+ 'vectors': len(re.findall(r'\\vec|\\mathbf', expression)),
110
+ 'matrices': len(re.findall(r'\\begin\{matrix\}|\\begin\{pmatrix\}|\\begin\{bmatrix\}', expression)),
111
+ 'functions': len(re.findall(r'\\sin|\\cos|\\tan|\\exp|\\log|\\ln', expression)),
112
+ }
113
+ return operations
114
+
115
+
116
+ def estimate_complexity(expressions: List[str]) -> float:
117
+ """
118
+ Estima la complejidad matemática de un conjunto de expresiones.
119
+
120
+ Args:
121
+ expressions: Lista de expresiones matemáticas
122
+
123
+ Returns:
124
+ Puntuación de complejidad (mayor = más complejo)
125
+ """
126
+ if not expressions:
127
+ return 0.0
128
+
129
+ total_complexity = 0.0
130
+
131
+ for expr in expressions:
132
+ # Longitud de la expresión
133
+ total_complexity += len(expr) * 0.01
134
+
135
+ # Operaciones complejas
136
+ ops = count_math_operations(expr)
137
+ total_complexity += ops['integrals'] * 2.0
138
+ total_complexity += ops['derivatives'] * 1.5
139
+ total_complexity += ops['sums'] * 1.5
140
+ total_complexity += ops['vectors'] * 1.0
141
+ total_complexity += ops['matrices'] * 2.5
142
+ total_complexity += ops['functions'] * 0.5
143
+
144
+ # Número de variables
145
+ vars_count = len(extract_variables([expr]))
146
+ total_complexity += vars_count * 0.3
147
+
148
+ # Bloques align (ecuaciones múltiples)
149
+ if '\\begin{align' in expr or '\\begin{aligned' in expr:
150
+ total_complexity += 2.0
151
+
152
+ logger.debug(f"[MathExtractor] Complejidad estimada: {total_complexity:.2f} (de {len(expressions)} expresiones)")
153
+ return total_complexity
@@ -0,0 +1 @@
1
+ # Tests package for EvolutIA validation