sourcecode 0.12.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 -29
- 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.12.0.dist-info → sourcecode-0.14.0.dist-info}/METADATA +215 -6
- {sourcecode-0.12.0.dist-info → sourcecode-0.14.0.dist-info}/RECORD +13 -9
- {sourcecode-0.12.0.dist-info → sourcecode-0.14.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.12.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
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
"""CLI de sourcecode — interfaz de linea de comandos principal."""
|
|
2
|
-
from sourcecode.prepare_context import prepare_context as build_llm_context
|
|
3
2
|
from __future__ import annotations
|
|
4
3
|
|
|
5
4
|
import json
|
|
@@ -48,7 +47,7 @@ def main(
|
|
|
48
47
|
compact: bool = typer.Option(
|
|
49
48
|
False,
|
|
50
49
|
"--compact",
|
|
51
|
-
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",
|
|
52
51
|
),
|
|
53
52
|
dependencies: bool = typer.Option(
|
|
54
53
|
False,
|
|
@@ -118,6 +117,41 @@ def main(
|
|
|
118
117
|
"--semantics",
|
|
119
118
|
help="Incluir call graph semantico, linking cross-file de simbolos y resolucion avanzada de imports",
|
|
120
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
|
+
),
|
|
121
155
|
) -> None:
|
|
122
156
|
"""Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
|
|
123
157
|
# Validar formato
|
|
@@ -159,6 +193,7 @@ def main(
|
|
|
159
193
|
from sourcecode.metrics_analyzer import MetricsAnalyzer
|
|
160
194
|
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
161
195
|
from sourcecode.scanner import FileScanner
|
|
196
|
+
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
162
197
|
from sourcecode.schema import (
|
|
163
198
|
AnalysisMetadata,
|
|
164
199
|
DocRecord,
|
|
@@ -234,7 +269,6 @@ def main(
|
|
|
234
269
|
doc_analyzer = DocAnalyzer() if docs else None
|
|
235
270
|
metrics_analyzer = MetricsAnalyzer() if full_metrics else None
|
|
236
271
|
|
|
237
|
-
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
238
272
|
semantic_analyzer = SemanticAnalyzer() if semantics else None
|
|
239
273
|
|
|
240
274
|
root_manifests = [
|
|
@@ -293,7 +327,7 @@ def main(
|
|
|
293
327
|
doc_records.extend(root_doc_records)
|
|
294
328
|
doc_summaries.append(root_doc_summary)
|
|
295
329
|
|
|
296
|
-
file_metrics_records: list = []
|
|
330
|
+
file_metrics_records: list[Any] = []
|
|
297
331
|
metrics_summaries = []
|
|
298
332
|
if metrics_analyzer is not None:
|
|
299
333
|
root_metrics_tree = (
|
|
@@ -501,6 +535,29 @@ def main(
|
|
|
501
535
|
sm.project_summary = ProjectSummarizer(target).generate(sm)
|
|
502
536
|
sm.architecture_summary = ArchitectureSummarizer(target).generate(sm)
|
|
503
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
|
+
|
|
504
561
|
# 4. Serializar (con o sin modo compact)
|
|
505
562
|
if compact:
|
|
506
563
|
data = compact_view(sm)
|
|
@@ -537,33 +594,34 @@ def main(
|
|
|
537
594
|
# 5. Escribir output (CLI-04)
|
|
538
595
|
write_output(content, output=output)
|
|
539
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
|
+
|
|
540
606
|
from sourcecode.prepare_context import ContextBuilder
|
|
541
607
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
"""
|
|
548
|
-
Prepara contexto mínimo optimizado para que un LLM modifique el código.
|
|
549
|
-
"""
|
|
550
|
-
target = path.resolve()
|
|
551
|
-
|
|
552
|
-
if not target.exists() or not target.is_dir():
|
|
553
|
-
typer.echo(f"Error: '{target}' no es un directorio válido.", err=True)
|
|
554
|
-
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)
|
|
555
613
|
|
|
556
|
-
|
|
557
|
-
|
|
614
|
+
builder = ContextBuilder(target)
|
|
615
|
+
result = builder.prepare(task)
|
|
558
616
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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
|
+
}
|
|
568
626
|
|
|
569
|
-
|
|
627
|
+
typer.echo(json.dumps(out, indent=2, ensure_ascii=False))
|