sourcecode 0.3.0__py3-none-any.whl → 0.5.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.
sourcecode/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.3.0"
3
+ __version__ = "0.5.0"
sourcecode/cli.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
 
4
4
  import json
5
5
  from pathlib import Path
6
- from typing import Any, Optional
6
+ from typing import Any, Optional, cast
7
7
 
8
8
  import typer
9
9
 
@@ -16,6 +16,8 @@ app = typer.Typer(
16
16
  )
17
17
 
18
18
  FORMAT_CHOICES = ["json", "yaml"]
19
+ GRAPH_DETAIL_CHOICES = ["high", "medium", "full"]
20
+ GRAPH_EDGE_CHOICES = {"imports", "calls", "contains", "extends"}
19
21
 
20
22
 
21
23
  def version_callback(value: bool) -> None:
@@ -51,6 +53,28 @@ def main(
51
53
  "--dependencies",
52
54
  help="Incluir dependencias directas, versiones exactas y transitivas cuando haya lockfiles compatibles",
53
55
  ),
56
+ graph_modules: bool = typer.Option(
57
+ False,
58
+ "--graph-modules",
59
+ help="Incluir grafo estructural de modulos, imports y relaciones simples del codigo",
60
+ ),
61
+ graph_detail: str = typer.Option(
62
+ "high",
63
+ "--graph-detail",
64
+ help="Nivel de detalle del grafo: high|medium|full",
65
+ show_default=True,
66
+ ),
67
+ max_nodes: Optional[int] = typer.Option(
68
+ None,
69
+ "--max-nodes",
70
+ help="Limite de nodos para `--graph-modules` en modos high/medium",
71
+ min=1,
72
+ ),
73
+ graph_edges: Optional[str] = typer.Option(
74
+ None,
75
+ "--graph-edges",
76
+ help="Tipos de arista para `--graph-modules` separados por comas: imports,calls,contains,extends",
77
+ ),
54
78
  no_redact: bool = typer.Option(
55
79
  False,
56
80
  "--no-redact",
@@ -80,6 +104,12 @@ def main(
80
104
  err=True,
81
105
  )
82
106
  raise typer.Exit(code=1)
107
+ if graph_detail not in GRAPH_DETAIL_CHOICES:
108
+ typer.echo(
109
+ f"Error: valor invalido '{graph_detail}' para --graph-detail. Opciones: {', '.join(GRAPH_DETAIL_CHOICES)}",
110
+ err=True,
111
+ )
112
+ raise typer.Exit(code=1)
83
113
 
84
114
  # Resolver y validar path
85
115
  target = path.resolve()
@@ -95,6 +125,7 @@ def main(
95
125
 
96
126
  from sourcecode.dependency_analyzer import DependencyAnalyzer
97
127
  from sourcecode.detectors import ProjectDetector, build_default_detectors
128
+ from sourcecode.graph_analyzer import GraphAnalyzer, GraphDetail
98
129
  from sourcecode.redactor import SecretRedactor, redact_dict
99
130
  from sourcecode.scanner import FileScanner
100
131
  from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
@@ -118,11 +149,48 @@ def main(
118
149
  filtered[name] = value
119
150
  return filtered
120
151
 
152
+ def prune_workspace_paths(
153
+ tree: dict[str, Any], workspace_paths: list[str]
154
+ ) -> dict[str, Any]:
155
+ pruned = dict(tree)
156
+ for workspace_path in workspace_paths:
157
+ parts = [part for part in workspace_path.split("/") if part]
158
+ if not parts:
159
+ continue
160
+ node = pruned
161
+ for index, part in enumerate(parts):
162
+ if not isinstance(node, dict) or part not in node:
163
+ break
164
+ if index == len(parts) - 1:
165
+ node.pop(part, None)
166
+ break
167
+ child = node.get(part)
168
+ if not isinstance(child, dict):
169
+ break
170
+ node = child
171
+ return pruned
172
+
121
173
  file_tree = filter_sensitive_files(raw_tree)
122
174
  manifests = scanner.find_manifests()
123
175
  detector = ProjectDetector(build_default_detectors())
124
176
  workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
125
177
  dependency_analyzer = DependencyAnalyzer() if dependencies else None
178
+ graph_analyzer = GraphAnalyzer() if graph_modules else None
179
+ parsed_graph_edges = (
180
+ {edge.strip() for edge in graph_edges.split(",") if edge.strip()}
181
+ if graph_edges
182
+ else None
183
+ )
184
+ if parsed_graph_edges is not None:
185
+ invalid_edges = sorted(parsed_graph_edges - GRAPH_EDGE_CHOICES)
186
+ if invalid_edges:
187
+ typer.echo(
188
+ "Error: valores invalidos para --graph-edges: "
189
+ f"{', '.join(invalid_edges)}. Opciones: {', '.join(sorted(GRAPH_EDGE_CHOICES))}",
190
+ err=True,
191
+ )
192
+ raise typer.Exit(code=1)
193
+ graph_detail_typed = cast(GraphDetail, graph_detail)
126
194
 
127
195
  root_manifests = [
128
196
  manifest
@@ -142,6 +210,24 @@ def main(
142
210
  root_dependencies, root_summary = dependency_analyzer.analyze(target)
143
211
  dependency_records.extend(root_dependencies)
144
212
  dependency_summaries.append(root_summary)
213
+ module_graphs = []
214
+ if graph_analyzer is not None:
215
+ root_graph_tree = (
216
+ prune_workspace_paths(
217
+ file_tree,
218
+ [workspace.path for workspace in workspace_analysis.workspaces],
219
+ )
220
+ if workspace_analysis.workspaces
221
+ else file_tree
222
+ )
223
+ module_graphs.append(
224
+ graph_analyzer.analyze(
225
+ target,
226
+ root_graph_tree,
227
+ detail="full",
228
+ entry_points=entry_points,
229
+ )
230
+ )
145
231
 
146
232
  for workspace in workspace_analysis.workspaces:
147
233
  workspace_root = target / workspace.path
@@ -174,6 +260,17 @@ def main(
174
260
  )
175
261
  dependency_records.extend(workspace_dependencies)
176
262
  dependency_summaries.append(workspace_summary)
263
+ if graph_analyzer is not None:
264
+ workspace_graph = graph_analyzer.analyze(
265
+ workspace_root,
266
+ workspace_tree,
267
+ workspace=workspace.path,
268
+ detail="full",
269
+ entry_points=workspace_entry_points,
270
+ )
271
+ module_graphs.append(
272
+ graph_analyzer.prefix_graph(workspace_graph, workspace.path, workspace.path)
273
+ )
177
274
 
178
275
  stacks, project_type = detector.classify_results(
179
276
  file_tree,
@@ -186,6 +283,17 @@ def main(
186
283
  if dependency_analyzer is not None
187
284
  else None
188
285
  )
286
+ module_graph = (
287
+ graph_analyzer.merge_graphs(
288
+ module_graphs,
289
+ detail=graph_detail_typed,
290
+ edge_kinds=parsed_graph_edges,
291
+ max_nodes=max_nodes,
292
+ entry_points=entry_points,
293
+ )
294
+ if graph_analyzer is not None
295
+ else None
296
+ )
189
297
 
190
298
  # 3. Construir el schema
191
299
  metadata = AnalysisMetadata(analyzed_path=str(target))
@@ -197,6 +305,8 @@ def main(
197
305
  entry_points=entry_points,
198
306
  dependencies=dependency_records,
199
307
  dependency_summary=dependency_summary,
308
+ module_graph=module_graph,
309
+ module_graph_summary=module_graph.summary if module_graph is not None else None,
200
310
  )
201
311
 
202
312
  # 4. Serializar (con o sin modo compact)