sourcecode 0.13.0__py3-none-any.whl → 0.14.0__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.
Potentially problematic release.
This version of sourcecode might be problematic. Click here for more details.
- sourcecode/__init__.py +1 -1
- sourcecode/architecture_analyzer.py +348 -0
- sourcecode/cli.py +87 -28
- sourcecode/code_notes_analyzer.py +242 -0
- sourcecode/env_analyzer.py +320 -0
- sourcecode/git_analyzer.py +241 -0
- sourcecode/schema.py +500 -335
- sourcecode/serializer.py +106 -96
- sourcecode/summarizer.py +137 -26
- {sourcecode-0.13.0.dist-info → sourcecode-0.14.0.dist-info}/METADATA +215 -6
- {sourcecode-0.13.0.dist-info → sourcecode-0.14.0.dist-info}/RECORD +13 -9
- {sourcecode-0.13.0.dist-info → sourcecode-0.14.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.13.0.dist-info → sourcecode-0.14.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
"""Inferencia arquitectonica: domain clustering, layer detection y bounded context inference."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal, Optional
|
|
6
|
+
|
|
7
|
+
from sourcecode.schema import (
|
|
8
|
+
ArchitectureAnalysis,
|
|
9
|
+
ArchitectureDomain,
|
|
10
|
+
ArchitectureLayer,
|
|
11
|
+
BoundedContext,
|
|
12
|
+
ModuleGraph,
|
|
13
|
+
SourceMap,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
_TOOLING_PREFIXES = (
|
|
17
|
+
".claude/",
|
|
18
|
+
".vscode/",
|
|
19
|
+
"bin/",
|
|
20
|
+
".git/",
|
|
21
|
+
"__pycache__/",
|
|
22
|
+
"node_modules/",
|
|
23
|
+
".venv/",
|
|
24
|
+
"venv/",
|
|
25
|
+
".mypy_cache/",
|
|
26
|
+
".pytest_cache/",
|
|
27
|
+
"dist/",
|
|
28
|
+
"build/",
|
|
29
|
+
)
|
|
30
|
+
_SRC_TRANSPARENT = {"src", "lib", "app", "pkg"}
|
|
31
|
+
_CODE_EXTENSIONS = {
|
|
32
|
+
".py", ".js", ".ts", ".tsx", ".jsx", ".mjs", ".cjs",
|
|
33
|
+
".go", ".java", ".kt", ".rs", ".rb",
|
|
34
|
+
}
|
|
35
|
+
_GENERIC_NAMES = {"utils", "helpers", "common", "shared", "misc", "core", "root", ""}
|
|
36
|
+
|
|
37
|
+
DOMAIN_ROLES: dict[str, str] = {
|
|
38
|
+
"controllers": "HTTP request handlers",
|
|
39
|
+
"handlers": "HTTP request handlers",
|
|
40
|
+
"routes": "Route definitions",
|
|
41
|
+
"services": "Business logic",
|
|
42
|
+
"usecases": "Business logic",
|
|
43
|
+
"application": "Application layer",
|
|
44
|
+
"repositories": "Data access",
|
|
45
|
+
"repos": "Data access",
|
|
46
|
+
"store": "Data access",
|
|
47
|
+
"models": "Domain models",
|
|
48
|
+
"entities": "Domain entities",
|
|
49
|
+
"domain": "Domain core",
|
|
50
|
+
"infra": "Infrastructure",
|
|
51
|
+
"infrastructure": "Infrastructure",
|
|
52
|
+
"adapters": "Hexagonal adapters",
|
|
53
|
+
"ports": "Hexagonal ports",
|
|
54
|
+
"frontend": "Frontend/UI layer",
|
|
55
|
+
"backend": "Backend/API layer",
|
|
56
|
+
"components": "UI components",
|
|
57
|
+
"pages": "UI pages/views",
|
|
58
|
+
"views": "View layer",
|
|
59
|
+
"templates": "View templates",
|
|
60
|
+
"tests": "Test suite",
|
|
61
|
+
"test": "Test suite",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
LAYER_PATTERNS: dict[str, dict[str, list[str]]] = {
|
|
65
|
+
"mvc": {
|
|
66
|
+
"controller": ["controller", "controllers", "routes", "views", "handlers"],
|
|
67
|
+
"model": ["model", "models", "entity", "entities", "domain"],
|
|
68
|
+
"view": ["views", "templates", "pages", "components"],
|
|
69
|
+
},
|
|
70
|
+
"layered": {
|
|
71
|
+
"controller": ["controller", "controllers", "api", "routes", "handlers", "endpoints"],
|
|
72
|
+
"service": ["service", "services", "usecase", "usecases", "application"],
|
|
73
|
+
"repository": ["repository", "repositories", "repo", "repos", "store", "storage", "dao"],
|
|
74
|
+
"infrastructure": ["infra", "infrastructure", "persistence", "db", "database"],
|
|
75
|
+
},
|
|
76
|
+
"hexagonal": {
|
|
77
|
+
"port": ["port", "ports", "interface", "interfaces"],
|
|
78
|
+
"adapter": ["adapter", "adapters"],
|
|
79
|
+
"domain": ["domain", "core", "model", "models"],
|
|
80
|
+
},
|
|
81
|
+
"fullstack": {
|
|
82
|
+
"frontend": ["frontend", "client", "web", "ui", "pages", "components", "app"],
|
|
83
|
+
"backend": ["backend", "server", "api", "services"],
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class ArchitectureAnalyzer:
|
|
89
|
+
"""Analiza la arquitectura de un proyecto a partir de su estructura de ficheros y grafo de modulos."""
|
|
90
|
+
|
|
91
|
+
def analyze(
|
|
92
|
+
self,
|
|
93
|
+
root: Path,
|
|
94
|
+
sm: SourceMap,
|
|
95
|
+
graph: Optional[ModuleGraph] = None,
|
|
96
|
+
) -> ArchitectureAnalysis:
|
|
97
|
+
limitations: list[str] = []
|
|
98
|
+
|
|
99
|
+
# Step 1: filter paths
|
|
100
|
+
filtered = self._filter_paths(sm.file_paths)
|
|
101
|
+
if len(filtered) < 2:
|
|
102
|
+
return ArchitectureAnalysis(
|
|
103
|
+
requested=True,
|
|
104
|
+
pattern="unknown",
|
|
105
|
+
limitations=["Arquitectura no inferida: proyecto sin archivos de codigo suficientes"],
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Step 2: domain clustering
|
|
109
|
+
domains = self._cluster_domains(filtered)
|
|
110
|
+
|
|
111
|
+
# Step 3: layer detection
|
|
112
|
+
pattern, layers = self._detect_layers(filtered)
|
|
113
|
+
if pattern in (None, "flat", "unknown"):
|
|
114
|
+
if pattern == "flat":
|
|
115
|
+
limitations.append("Patron de capas no detectado: proyecto con estructura plana")
|
|
116
|
+
elif pattern == "unknown":
|
|
117
|
+
limitations.append("Patron de capas no reconocido: estructura de directorios sin senales claras")
|
|
118
|
+
|
|
119
|
+
# Step 4: bounded context inference
|
|
120
|
+
bounded_contexts = self._infer_bounded_contexts(domains, graph)
|
|
121
|
+
|
|
122
|
+
# Overall confidence
|
|
123
|
+
confidence: Literal["high", "medium", "low"]
|
|
124
|
+
if len(domains) >= 3 and pattern not in (None, "unknown"):
|
|
125
|
+
confidence = "high"
|
|
126
|
+
elif len(domains) >= 1:
|
|
127
|
+
confidence = "medium"
|
|
128
|
+
else:
|
|
129
|
+
confidence = "low"
|
|
130
|
+
|
|
131
|
+
method = "graph+heuristic" if graph is not None else "heuristic"
|
|
132
|
+
|
|
133
|
+
return ArchitectureAnalysis(
|
|
134
|
+
requested=True,
|
|
135
|
+
pattern=pattern,
|
|
136
|
+
domains=domains,
|
|
137
|
+
layers=layers,
|
|
138
|
+
bounded_contexts=bounded_contexts,
|
|
139
|
+
confidence=confidence,
|
|
140
|
+
method=method,
|
|
141
|
+
limitations=limitations,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# ------------------------------------------------------------------
|
|
145
|
+
# Private helpers
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def _is_tooling(self, path: str) -> bool:
|
|
149
|
+
norm = path.replace("\\", "/")
|
|
150
|
+
return any(norm.startswith(p) for p in _TOOLING_PREFIXES)
|
|
151
|
+
|
|
152
|
+
def _filter_paths(self, paths: list[str]) -> list[str]:
|
|
153
|
+
result = []
|
|
154
|
+
for p in paths:
|
|
155
|
+
norm = p.replace("\\", "/")
|
|
156
|
+
if self._is_tooling(norm):
|
|
157
|
+
continue
|
|
158
|
+
ext = Path(norm).suffix.lower()
|
|
159
|
+
if ext not in _CODE_EXTENSIONS:
|
|
160
|
+
continue
|
|
161
|
+
result.append(norm)
|
|
162
|
+
return result
|
|
163
|
+
|
|
164
|
+
def _extract_domain_segment(self, path: str) -> str:
|
|
165
|
+
parts = path.replace("\\", "/").split("/")
|
|
166
|
+
if len(parts) == 1:
|
|
167
|
+
return "root"
|
|
168
|
+
first = parts[0]
|
|
169
|
+
if first in _SRC_TRANSPARENT and len(parts) >= 3:
|
|
170
|
+
return parts[1]
|
|
171
|
+
return first
|
|
172
|
+
|
|
173
|
+
def _cluster_domains(self, paths: list[str]) -> list[ArchitectureDomain]:
|
|
174
|
+
groups: dict[str, list[str]] = {}
|
|
175
|
+
for p in paths:
|
|
176
|
+
seg = self._extract_domain_segment(p)
|
|
177
|
+
groups.setdefault(seg, []).append(p)
|
|
178
|
+
|
|
179
|
+
domains: list[ArchitectureDomain] = []
|
|
180
|
+
for name, files in groups.items():
|
|
181
|
+
if len(files) < 2:
|
|
182
|
+
continue
|
|
183
|
+
role = DOMAIN_ROLES.get(name, "")
|
|
184
|
+
domain_confidence: Literal["high", "medium", "low"]
|
|
185
|
+
if name in DOMAIN_ROLES:
|
|
186
|
+
domain_confidence = "high"
|
|
187
|
+
elif len(name) >= 5 and name not in _GENERIC_NAMES:
|
|
188
|
+
domain_confidence = "medium"
|
|
189
|
+
else:
|
|
190
|
+
domain_confidence = "low"
|
|
191
|
+
domains.append(ArchitectureDomain(name=name, files=files, role=role, confidence=domain_confidence))
|
|
192
|
+
return domains
|
|
193
|
+
|
|
194
|
+
def _detect_layers(self, paths: list[str]) -> tuple[str, list[ArchitectureLayer]]:
|
|
195
|
+
dir_names: set[str] = set()
|
|
196
|
+
for p in paths:
|
|
197
|
+
parts = p.replace("\\", "/").split("/")
|
|
198
|
+
for part in parts[:-1]:
|
|
199
|
+
dir_names.add(part.lower())
|
|
200
|
+
|
|
201
|
+
best_pattern = ""
|
|
202
|
+
best_score = 0
|
|
203
|
+
best_matched: dict[str, list[str]] = {}
|
|
204
|
+
|
|
205
|
+
for pattern_name, layer_keys in LAYER_PATTERNS.items():
|
|
206
|
+
matched: dict[str, list[str]] = {}
|
|
207
|
+
for layer_key, keywords in layer_keys.items():
|
|
208
|
+
matched_dirs = [d for d in dir_names if d in keywords]
|
|
209
|
+
if matched_dirs:
|
|
210
|
+
matched[layer_key] = matched_dirs
|
|
211
|
+
score = len(matched)
|
|
212
|
+
if score > best_score:
|
|
213
|
+
best_score = score
|
|
214
|
+
best_pattern = pattern_name
|
|
215
|
+
best_matched = matched
|
|
216
|
+
|
|
217
|
+
if best_score < 2:
|
|
218
|
+
# Check depth for flat vs unknown
|
|
219
|
+
max_depth = max(
|
|
220
|
+
len(p.replace("\\", "/").split("/")) - 1
|
|
221
|
+
for p in paths
|
|
222
|
+
)
|
|
223
|
+
flat_pattern = "flat" if max_depth <= 2 else "unknown"
|
|
224
|
+
return flat_pattern, []
|
|
225
|
+
|
|
226
|
+
# Build ArchitectureLayer list
|
|
227
|
+
layer_confidence: Literal["high", "medium", "low"] = "high" if best_score >= 3 else "medium"
|
|
228
|
+
layers: list[ArchitectureLayer] = []
|
|
229
|
+
for layer_key, matched_dirs in best_matched.items():
|
|
230
|
+
matched_files = [
|
|
231
|
+
p for p in paths
|
|
232
|
+
if any(
|
|
233
|
+
seg.lower() in matched_dirs
|
|
234
|
+
for seg in p.replace("\\", "/").split("/")[:-1]
|
|
235
|
+
)
|
|
236
|
+
]
|
|
237
|
+
layers.append(ArchitectureLayer(
|
|
238
|
+
name=layer_key,
|
|
239
|
+
pattern=best_pattern,
|
|
240
|
+
files=matched_files,
|
|
241
|
+
confidence=layer_confidence,
|
|
242
|
+
))
|
|
243
|
+
|
|
244
|
+
return best_pattern or "unknown", layers
|
|
245
|
+
|
|
246
|
+
def _infer_bounded_contexts(
|
|
247
|
+
self,
|
|
248
|
+
domains: list[ArchitectureDomain],
|
|
249
|
+
graph: Optional[ModuleGraph],
|
|
250
|
+
) -> list[BoundedContext]:
|
|
251
|
+
# Priority 1: use graph SCCs when available
|
|
252
|
+
if graph is not None:
|
|
253
|
+
sccs = self._find_sccs(graph)
|
|
254
|
+
bc_list: list[BoundedContext] = []
|
|
255
|
+
for scc in sccs:
|
|
256
|
+
if len(scc) < 2:
|
|
257
|
+
continue
|
|
258
|
+
# Derive name from most frequent directory segment among nodes
|
|
259
|
+
seg_counts: dict[str, int] = {}
|
|
260
|
+
for node_id in scc:
|
|
261
|
+
parts = node_id.replace("\\", "/").split("/")
|
|
262
|
+
seg = parts[0] if len(parts) == 1 else parts[-2] if len(parts) >= 2 else parts[0]
|
|
263
|
+
seg_counts[seg] = seg_counts.get(seg, 0) + 1
|
|
264
|
+
name = max(seg_counts, key=lambda k: seg_counts[k])
|
|
265
|
+
if name in _GENERIC_NAMES:
|
|
266
|
+
continue
|
|
267
|
+
bc_conf: Literal["high", "medium", "low"] = "high" if name in DOMAIN_ROLES else "medium"
|
|
268
|
+
bc_list.append(BoundedContext(name=name, modules=scc, confidence=bc_conf))
|
|
269
|
+
if bc_list:
|
|
270
|
+
return bc_list
|
|
271
|
+
|
|
272
|
+
# Priority 2: use domains as candidates
|
|
273
|
+
bc_from_domains: list[BoundedContext] = []
|
|
274
|
+
for d in domains:
|
|
275
|
+
if d.name in _GENERIC_NAMES:
|
|
276
|
+
continue
|
|
277
|
+
if d.confidence == "low":
|
|
278
|
+
continue
|
|
279
|
+
bc_from_domains.append(BoundedContext(
|
|
280
|
+
name=d.name,
|
|
281
|
+
modules=d.files,
|
|
282
|
+
confidence="medium",
|
|
283
|
+
))
|
|
284
|
+
return bc_from_domains
|
|
285
|
+
|
|
286
|
+
def _find_sccs(self, graph: ModuleGraph) -> list[list[str]]:
|
|
287
|
+
"""Kosaraju's algorithm for strongly connected components."""
|
|
288
|
+
import_edges = [
|
|
289
|
+
(e.source, e.target)
|
|
290
|
+
for e in graph.edges
|
|
291
|
+
if e.kind == "imports"
|
|
292
|
+
]
|
|
293
|
+
node_ids = [n.id for n in graph.nodes]
|
|
294
|
+
|
|
295
|
+
# Build adjacency lists
|
|
296
|
+
adj: dict[str, list[str]] = {n: [] for n in node_ids}
|
|
297
|
+
radj: dict[str, list[str]] = {n: [] for n in node_ids}
|
|
298
|
+
for src, tgt in import_edges:
|
|
299
|
+
if src in adj and tgt in adj:
|
|
300
|
+
adj[src].append(tgt)
|
|
301
|
+
radj[tgt].append(src)
|
|
302
|
+
|
|
303
|
+
visited: set[str] = set()
|
|
304
|
+
order: list[str] = []
|
|
305
|
+
|
|
306
|
+
def dfs1(v: str) -> None:
|
|
307
|
+
stack = [(v, False)]
|
|
308
|
+
while stack:
|
|
309
|
+
node, done = stack.pop()
|
|
310
|
+
if done:
|
|
311
|
+
order.append(node)
|
|
312
|
+
continue
|
|
313
|
+
if node in visited:
|
|
314
|
+
continue
|
|
315
|
+
visited.add(node)
|
|
316
|
+
stack.append((node, True))
|
|
317
|
+
for nb in adj[node]:
|
|
318
|
+
if nb not in visited:
|
|
319
|
+
stack.append((nb, False))
|
|
320
|
+
|
|
321
|
+
for n in node_ids:
|
|
322
|
+
if n not in visited:
|
|
323
|
+
dfs1(n)
|
|
324
|
+
|
|
325
|
+
visited2: set[str] = set()
|
|
326
|
+
sccs: list[list[str]] = []
|
|
327
|
+
|
|
328
|
+
def dfs2(v: str) -> list[str]:
|
|
329
|
+
comp: list[str] = []
|
|
330
|
+
stack = [v]
|
|
331
|
+
while stack:
|
|
332
|
+
node = stack.pop()
|
|
333
|
+
if node in visited2:
|
|
334
|
+
continue
|
|
335
|
+
visited2.add(node)
|
|
336
|
+
comp.append(node)
|
|
337
|
+
for nb in radj[node]:
|
|
338
|
+
if nb not in visited2:
|
|
339
|
+
stack.append(nb)
|
|
340
|
+
return comp
|
|
341
|
+
|
|
342
|
+
for n in reversed(order):
|
|
343
|
+
if n not in visited2:
|
|
344
|
+
comp = dfs2(n)
|
|
345
|
+
if len(comp) >= 2:
|
|
346
|
+
sccs.append(comp)
|
|
347
|
+
|
|
348
|
+
return sccs
|
sourcecode/cli.py
CHANGED
|
@@ -47,7 +47,7 @@ def main(
|
|
|
47
47
|
compact: bool = typer.Option(
|
|
48
48
|
False,
|
|
49
49
|
"--compact",
|
|
50
|
-
help="Output reducido (~500 tokens): tipo
|
|
50
|
+
help="Output reducido (~500-700 tokens): tipo, stacks, entradas, arbol nivel 1 y summaries de flags opcionales activos",
|
|
51
51
|
),
|
|
52
52
|
dependencies: bool = typer.Option(
|
|
53
53
|
False,
|
|
@@ -117,6 +117,41 @@ def main(
|
|
|
117
117
|
"--semantics",
|
|
118
118
|
help="Incluir call graph semantico, linking cross-file de simbolos y resolucion avanzada de imports",
|
|
119
119
|
),
|
|
120
|
+
architecture: bool = typer.Option(
|
|
121
|
+
False,
|
|
122
|
+
"--architecture",
|
|
123
|
+
help="Inferencia arquitectonica: dominios funcionales, capas (MVC/layered/hexagonal) y bounded contexts aproximados",
|
|
124
|
+
),
|
|
125
|
+
git_context: bool = typer.Option(
|
|
126
|
+
False,
|
|
127
|
+
"--git-context",
|
|
128
|
+
"-g",
|
|
129
|
+
help="Incluir contexto git: commits recientes, ficheros mas activos y cambios pendientes",
|
|
130
|
+
),
|
|
131
|
+
git_depth: int = typer.Option(
|
|
132
|
+
20,
|
|
133
|
+
"--git-depth",
|
|
134
|
+
help="Numero de commits recientes a incluir con --git-context (default: 20)",
|
|
135
|
+
min=1,
|
|
136
|
+
max=100,
|
|
137
|
+
),
|
|
138
|
+
git_days: int = typer.Option(
|
|
139
|
+
90,
|
|
140
|
+
"--git-days",
|
|
141
|
+
help="Ventana temporal en dias para detectar ficheros mas activos con --git-context (default: 90)",
|
|
142
|
+
min=1,
|
|
143
|
+
max=3650,
|
|
144
|
+
),
|
|
145
|
+
env_map: bool = typer.Option(
|
|
146
|
+
False,
|
|
147
|
+
"--env-map",
|
|
148
|
+
help="Incluir mapa de variables de entorno: claves, tipos, categorias y ficheros que las referencian",
|
|
149
|
+
),
|
|
150
|
+
code_notes: bool = typer.Option(
|
|
151
|
+
False,
|
|
152
|
+
"--code-notes",
|
|
153
|
+
help="Extraer anotaciones TODO/FIXME/HACK/NOTE/DEPRECATED/WARNING/BUG/XXX/OPTIMIZE con ubicacion y simbolo envolvente, y detectar ADRs en docs/decisions/, docs/adr/ y similares",
|
|
154
|
+
),
|
|
120
155
|
) -> None:
|
|
121
156
|
"""Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
|
|
122
157
|
# Validar formato
|
|
@@ -158,6 +193,7 @@ def main(
|
|
|
158
193
|
from sourcecode.metrics_analyzer import MetricsAnalyzer
|
|
159
194
|
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
160
195
|
from sourcecode.scanner import FileScanner
|
|
196
|
+
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
161
197
|
from sourcecode.schema import (
|
|
162
198
|
AnalysisMetadata,
|
|
163
199
|
DocRecord,
|
|
@@ -233,7 +269,6 @@ def main(
|
|
|
233
269
|
doc_analyzer = DocAnalyzer() if docs else None
|
|
234
270
|
metrics_analyzer = MetricsAnalyzer() if full_metrics else None
|
|
235
271
|
|
|
236
|
-
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
237
272
|
semantic_analyzer = SemanticAnalyzer() if semantics else None
|
|
238
273
|
|
|
239
274
|
root_manifests = [
|
|
@@ -292,7 +327,7 @@ def main(
|
|
|
292
327
|
doc_records.extend(root_doc_records)
|
|
293
328
|
doc_summaries.append(root_doc_summary)
|
|
294
329
|
|
|
295
|
-
file_metrics_records: list = []
|
|
330
|
+
file_metrics_records: list[Any] = []
|
|
296
331
|
metrics_summaries = []
|
|
297
332
|
if metrics_analyzer is not None:
|
|
298
333
|
root_metrics_tree = (
|
|
@@ -500,6 +535,29 @@ def main(
|
|
|
500
535
|
sm.project_summary = ProjectSummarizer(target).generate(sm)
|
|
501
536
|
sm.architecture_summary = ArchitectureSummarizer(target).generate(sm)
|
|
502
537
|
|
|
538
|
+
# Phase 13 Plan 04: Architectural Inference (--architecture flag)
|
|
539
|
+
if architecture:
|
|
540
|
+
from sourcecode.architecture_analyzer import ArchitectureAnalyzer
|
|
541
|
+
arch_graph = module_graph # None si --graph-modules no fue pasado
|
|
542
|
+
sm.architecture = ArchitectureAnalyzer().analyze(target, sm, arch_graph)
|
|
543
|
+
|
|
544
|
+
# Git Context (--git-context flag)
|
|
545
|
+
if git_context:
|
|
546
|
+
from sourcecode.git_analyzer import GitAnalyzer
|
|
547
|
+
sm.git_context = GitAnalyzer().analyze(target, depth=git_depth, days=git_days)
|
|
548
|
+
|
|
549
|
+
# Env Map (--env-map flag)
|
|
550
|
+
if env_map:
|
|
551
|
+
from sourcecode.env_analyzer import EnvAnalyzer
|
|
552
|
+
env_records, env_summary = EnvAnalyzer().analyze(target, file_tree)
|
|
553
|
+
sm = replace(sm, env_map=env_records, env_summary=env_summary)
|
|
554
|
+
|
|
555
|
+
# Code Notes (--code-notes flag)
|
|
556
|
+
if code_notes:
|
|
557
|
+
from sourcecode.code_notes_analyzer import CodeNotesAnalyzer
|
|
558
|
+
cn_notes, cn_adrs, cn_summary = CodeNotesAnalyzer().analyze(target)
|
|
559
|
+
sm = replace(sm, code_notes=cn_notes, code_adrs=cn_adrs, code_notes_summary=cn_summary)
|
|
560
|
+
|
|
503
561
|
# 4. Serializar (con o sin modo compact)
|
|
504
562
|
if compact:
|
|
505
563
|
data = compact_view(sm)
|
|
@@ -536,33 +594,34 @@ def main(
|
|
|
536
594
|
# 5. Escribir output (CLI-04)
|
|
537
595
|
write_output(content, output=output)
|
|
538
596
|
|
|
597
|
+
|
|
598
|
+
@app.command("prepare-context")
|
|
599
|
+
def prepare_context_cmd(
|
|
600
|
+
task: str = typer.Argument(..., help="Descripción de la tarea"),
|
|
601
|
+
path: Path = typer.Option(".", "--path", "-p", help="Directorio del proyecto"),
|
|
602
|
+
) -> None:
|
|
603
|
+
"""Prepara contexto mínimo optimizado para que un LLM modifique el código."""
|
|
604
|
+
from dataclasses import asdict
|
|
605
|
+
|
|
539
606
|
from sourcecode.prepare_context import ContextBuilder
|
|
540
607
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
"""
|
|
547
|
-
Prepara contexto mínimo optimizado para que un LLM modifique el código.
|
|
548
|
-
"""
|
|
549
|
-
target = path.resolve()
|
|
550
|
-
|
|
551
|
-
if not target.exists() or not target.is_dir():
|
|
552
|
-
typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
|
|
553
|
-
raise typer.Exit(code=1)
|
|
608
|
+
target = path.resolve()
|
|
609
|
+
|
|
610
|
+
if not target.exists() or not target.is_dir():
|
|
611
|
+
typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
|
|
612
|
+
raise typer.Exit(code=1)
|
|
554
613
|
|
|
555
|
-
|
|
556
|
-
|
|
614
|
+
builder = ContextBuilder(target)
|
|
615
|
+
result = builder.prepare(task)
|
|
557
616
|
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
617
|
+
out = {
|
|
618
|
+
"task": result.task,
|
|
619
|
+
"entry_points": result.entry_points,
|
|
620
|
+
"relevant_files": [asdict(f) for f in result.relevant_files],
|
|
621
|
+
"call_flow": result.call_flow,
|
|
622
|
+
"snippets": [asdict(s) for s in result.snippets],
|
|
623
|
+
"tests": result.tests,
|
|
624
|
+
"notes": result.notes,
|
|
625
|
+
}
|
|
567
626
|
|
|
568
|
-
|
|
627
|
+
typer.echo(json.dumps(out, indent=2, ensure_ascii=False))
|