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