sourcecode 0.4.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 +1 -1
- sourcecode/cli.py +68 -8
- sourcecode/graph_analyzer.py +484 -70
- sourcecode/schema.py +9 -0
- {sourcecode-0.4.0.dist-info → sourcecode-0.5.0.dist-info}/METADATA +45 -33
- {sourcecode-0.4.0.dist-info → sourcecode-0.5.0.dist-info}/RECORD +8 -8
- {sourcecode-0.4.0.dist-info → sourcecode-0.5.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.4.0.dist-info → sourcecode-0.5.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
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:
|
|
@@ -56,6 +58,23 @@ def main(
|
|
|
56
58
|
"--graph-modules",
|
|
57
59
|
help="Incluir grafo estructural de modulos, imports y relaciones simples del codigo",
|
|
58
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
|
+
),
|
|
59
78
|
no_redact: bool = typer.Option(
|
|
60
79
|
False,
|
|
61
80
|
"--no-redact",
|
|
@@ -85,6 +104,12 @@ def main(
|
|
|
85
104
|
err=True,
|
|
86
105
|
)
|
|
87
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)
|
|
88
113
|
|
|
89
114
|
# Resolver y validar path
|
|
90
115
|
target = path.resolve()
|
|
@@ -100,7 +125,7 @@ def main(
|
|
|
100
125
|
|
|
101
126
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
|
102
127
|
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
103
|
-
from sourcecode.graph_analyzer import GraphAnalyzer
|
|
128
|
+
from sourcecode.graph_analyzer import GraphAnalyzer, GraphDetail
|
|
104
129
|
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
105
130
|
from sourcecode.scanner import FileScanner
|
|
106
131
|
from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
|
|
@@ -151,6 +176,21 @@ def main(
|
|
|
151
176
|
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
152
177
|
dependency_analyzer = DependencyAnalyzer() if dependencies else None
|
|
153
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)
|
|
154
194
|
|
|
155
195
|
root_manifests = [
|
|
156
196
|
manifest
|
|
@@ -180,7 +220,14 @@ def main(
|
|
|
180
220
|
if workspace_analysis.workspaces
|
|
181
221
|
else file_tree
|
|
182
222
|
)
|
|
183
|
-
module_graphs.append(
|
|
223
|
+
module_graphs.append(
|
|
224
|
+
graph_analyzer.analyze(
|
|
225
|
+
target,
|
|
226
|
+
root_graph_tree,
|
|
227
|
+
detail="full",
|
|
228
|
+
entry_points=entry_points,
|
|
229
|
+
)
|
|
230
|
+
)
|
|
184
231
|
|
|
185
232
|
for workspace in workspace_analysis.workspaces:
|
|
186
233
|
workspace_root = target / workspace.path
|
|
@@ -215,10 +262,12 @@ def main(
|
|
|
215
262
|
dependency_summaries.append(workspace_summary)
|
|
216
263
|
if graph_analyzer is not None:
|
|
217
264
|
workspace_graph = graph_analyzer.analyze(
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
265
|
+
workspace_root,
|
|
266
|
+
workspace_tree,
|
|
267
|
+
workspace=workspace.path,
|
|
268
|
+
detail="full",
|
|
269
|
+
entry_points=workspace_entry_points,
|
|
270
|
+
)
|
|
222
271
|
module_graphs.append(
|
|
223
272
|
graph_analyzer.prefix_graph(workspace_graph, workspace.path, workspace.path)
|
|
224
273
|
)
|
|
@@ -234,7 +283,17 @@ def main(
|
|
|
234
283
|
if dependency_analyzer is not None
|
|
235
284
|
else None
|
|
236
285
|
)
|
|
237
|
-
module_graph =
|
|
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
|
+
)
|
|
238
297
|
|
|
239
298
|
# 3. Construir el schema
|
|
240
299
|
metadata = AnalysisMetadata(analyzed_path=str(target))
|
|
@@ -247,6 +306,7 @@ def main(
|
|
|
247
306
|
dependencies=dependency_records,
|
|
248
307
|
dependency_summary=dependency_summary,
|
|
249
308
|
module_graph=module_graph,
|
|
309
|
+
module_graph_summary=module_graph.summary if module_graph is not None else None,
|
|
250
310
|
)
|
|
251
311
|
|
|
252
312
|
# 4. Serializar (con o sin modo compact)
|
sourcecode/graph_analyzer.py
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
"""Analisis estructural offline para
|
|
1
|
+
"""Analisis estructural offline optimizado para LLMs y modo full."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import ast
|
|
5
5
|
import re
|
|
6
|
+
from collections import Counter, defaultdict
|
|
6
7
|
from dataclasses import replace
|
|
7
8
|
from pathlib import Path, PurePosixPath
|
|
8
|
-
from typing import Any, Optional
|
|
9
|
+
from typing import Any, Literal, Optional
|
|
9
10
|
|
|
10
|
-
from sourcecode.schema import GraphEdge, GraphNode, ModuleGraph, ModuleGraphSummary
|
|
11
|
+
from sourcecode.schema import EntryPoint, GraphEdge, GraphNode, ModuleGraph, ModuleGraphSummary
|
|
11
12
|
from sourcecode.tree_utils import flatten_file_tree
|
|
12
13
|
|
|
14
|
+
GraphDetail = Literal["high", "medium", "full"]
|
|
15
|
+
GraphImportance = Literal["high", "medium", "low"]
|
|
16
|
+
|
|
13
17
|
|
|
14
18
|
class GraphAnalyzer:
|
|
15
19
|
"""Construye un grafo estructural parcial y seguro del proyecto."""
|
|
@@ -19,6 +23,16 @@ class GraphAnalyzer:
|
|
|
19
23
|
_GO_EXTENSIONS = {".go"}
|
|
20
24
|
_JVM_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
21
25
|
_SUPPORTED_EXTENSIONS = _PYTHON_EXTENSIONS | _NODE_EXTENSIONS | _GO_EXTENSIONS | _JVM_EXTENSIONS
|
|
26
|
+
_DEFAULT_EDGE_KINDS: dict[GraphDetail, tuple[str, ...]] = {
|
|
27
|
+
"high": ("imports",),
|
|
28
|
+
"medium": ("imports", "calls"),
|
|
29
|
+
"full": ("imports", "calls", "contains", "extends"),
|
|
30
|
+
}
|
|
31
|
+
_DEFAULT_MAX_NODES: dict[GraphDetail, Optional[int]] = {
|
|
32
|
+
"high": 80,
|
|
33
|
+
"medium": 160,
|
|
34
|
+
"full": None,
|
|
35
|
+
}
|
|
22
36
|
|
|
23
37
|
def __init__(
|
|
24
38
|
self,
|
|
@@ -39,6 +53,98 @@ class GraphAnalyzer:
|
|
|
39
53
|
file_tree: dict[str, Any],
|
|
40
54
|
*,
|
|
41
55
|
workspace: str | None = None,
|
|
56
|
+
detail: GraphDetail = "full",
|
|
57
|
+
edge_kinds: set[str] | None = None,
|
|
58
|
+
max_nodes: int | None = None,
|
|
59
|
+
entry_points: list[EntryPoint] | None = None,
|
|
60
|
+
) -> ModuleGraph:
|
|
61
|
+
full_graph = self._analyze_full(root, file_tree, workspace=workspace)
|
|
62
|
+
return self._project_graph(
|
|
63
|
+
full_graph,
|
|
64
|
+
detail=detail,
|
|
65
|
+
edge_kinds=edge_kinds,
|
|
66
|
+
max_nodes=max_nodes,
|
|
67
|
+
entry_points=entry_points or [],
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def merge_graphs(
|
|
71
|
+
self,
|
|
72
|
+
graphs: list[ModuleGraph],
|
|
73
|
+
*,
|
|
74
|
+
detail: GraphDetail = "full",
|
|
75
|
+
edge_kinds: set[str] | None = None,
|
|
76
|
+
max_nodes: int | None = None,
|
|
77
|
+
entry_points: list[EntryPoint] | None = None,
|
|
78
|
+
) -> ModuleGraph:
|
|
79
|
+
nodes: list[GraphNode] = []
|
|
80
|
+
edges: list[GraphEdge] = []
|
|
81
|
+
limitations: list[str] = []
|
|
82
|
+
node_ids: set[str] = set()
|
|
83
|
+
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
84
|
+
for graph in graphs:
|
|
85
|
+
for node in graph.nodes:
|
|
86
|
+
self._append_node(nodes, node_ids, node)
|
|
87
|
+
for edge in graph.edges:
|
|
88
|
+
self._append_edge(edges, edge_keys, edge)
|
|
89
|
+
limitations.extend(graph.summary.limitations)
|
|
90
|
+
merged = ModuleGraph(
|
|
91
|
+
nodes=nodes,
|
|
92
|
+
edges=edges,
|
|
93
|
+
summary=ModuleGraphSummary(
|
|
94
|
+
requested=bool(graphs),
|
|
95
|
+
node_count=len(nodes),
|
|
96
|
+
edge_count=len(edges),
|
|
97
|
+
languages=sorted({node.language for node in nodes}),
|
|
98
|
+
methods=sorted({edge.method for edge in edges}),
|
|
99
|
+
limitations=self._unique(limitations),
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
return self._project_graph(
|
|
103
|
+
merged,
|
|
104
|
+
detail=detail,
|
|
105
|
+
edge_kinds=edge_kinds,
|
|
106
|
+
max_nodes=max_nodes,
|
|
107
|
+
entry_points=entry_points or [],
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
def prefix_graph(self, graph: ModuleGraph, prefix: str, workspace: str) -> ModuleGraph:
|
|
111
|
+
"""Reescribe ids y paths de un grafo de workspace al espacio de paths del repo."""
|
|
112
|
+
node_id_map: dict[str, str] = {}
|
|
113
|
+
prefixed_nodes: list[GraphNode] = []
|
|
114
|
+
clean_prefix = prefix.strip("/")
|
|
115
|
+
for node in graph.nodes:
|
|
116
|
+
prefixed_path = f"{clean_prefix}/{node.path}".strip("/")
|
|
117
|
+
new_id = self._node_id(node.kind, prefixed_path, node.symbol)
|
|
118
|
+
node_id_map[node.id] = new_id
|
|
119
|
+
prefixed_nodes.append(
|
|
120
|
+
replace(
|
|
121
|
+
node,
|
|
122
|
+
id=new_id,
|
|
123
|
+
path=prefixed_path,
|
|
124
|
+
workspace=workspace,
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
prefixed_edges: list[GraphEdge] = []
|
|
128
|
+
for edge in graph.edges:
|
|
129
|
+
prefixed_edges.append(
|
|
130
|
+
replace(
|
|
131
|
+
edge,
|
|
132
|
+
source=node_id_map.get(edge.source, edge.source),
|
|
133
|
+
target=node_id_map.get(edge.target, edge.target),
|
|
134
|
+
)
|
|
135
|
+
)
|
|
136
|
+
return ModuleGraph(
|
|
137
|
+
nodes=prefixed_nodes,
|
|
138
|
+
edges=prefixed_edges,
|
|
139
|
+
summary=replace(graph.summary),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _analyze_full(
|
|
143
|
+
self,
|
|
144
|
+
root: Path,
|
|
145
|
+
file_tree: dict[str, Any],
|
|
146
|
+
*,
|
|
147
|
+
workspace: str | None,
|
|
42
148
|
) -> ModuleGraph:
|
|
43
149
|
source_files = [
|
|
44
150
|
path
|
|
@@ -47,9 +153,7 @@ class GraphAnalyzer:
|
|
|
47
153
|
]
|
|
48
154
|
limitations: list[str] = []
|
|
49
155
|
if len(source_files) > self.max_files:
|
|
50
|
-
limitations.append(
|
|
51
|
-
f"max_files_reached:{len(source_files)}>{self.max_files}"
|
|
52
|
-
)
|
|
156
|
+
limitations.append(f"max_files_reached:{len(source_files)}>{self.max_files}")
|
|
53
157
|
source_files = source_files[: self.max_files]
|
|
54
158
|
|
|
55
159
|
nodes: list[GraphNode] = []
|
|
@@ -58,7 +162,9 @@ class GraphAnalyzer:
|
|
|
58
162
|
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
59
163
|
|
|
60
164
|
python_modules = self._build_python_module_map(source_files)
|
|
61
|
-
node_modules = {
|
|
165
|
+
node_modules = {
|
|
166
|
+
path: path for path in source_files if Path(path).suffix in self._NODE_EXTENSIONS
|
|
167
|
+
}
|
|
62
168
|
go_imports = self._build_go_import_map(root, source_files)
|
|
63
169
|
jvm_types = self._build_jvm_type_map(root, source_files)
|
|
64
170
|
|
|
@@ -114,78 +220,392 @@ class GraphAnalyzer:
|
|
|
114
220
|
limitations.append("graph_budget_reached")
|
|
115
221
|
break
|
|
116
222
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if len(edges) > self.max_edges:
|
|
120
|
-
edges = edges[: self.max_edges]
|
|
121
|
-
|
|
122
|
-
summary = ModuleGraphSummary(
|
|
123
|
-
requested=True,
|
|
124
|
-
node_count=len(nodes),
|
|
125
|
-
edge_count=len(edges),
|
|
126
|
-
languages=sorted({node.language for node in nodes}),
|
|
127
|
-
methods=sorted({edge.method for edge in edges}),
|
|
128
|
-
limitations=self._unique(limitations),
|
|
129
|
-
)
|
|
130
|
-
return ModuleGraph(nodes=nodes, edges=edges, summary=summary)
|
|
131
|
-
|
|
132
|
-
def merge_graphs(self, graphs: list[ModuleGraph]) -> ModuleGraph:
|
|
133
|
-
nodes: list[GraphNode] = []
|
|
134
|
-
edges: list[GraphEdge] = []
|
|
135
|
-
limitations: list[str] = []
|
|
136
|
-
node_ids: set[str] = set()
|
|
137
|
-
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
138
|
-
for graph in graphs:
|
|
139
|
-
for node in graph.nodes:
|
|
140
|
-
self._append_node(nodes, node_ids, node)
|
|
141
|
-
for edge in graph.edges:
|
|
142
|
-
self._append_edge(edges, edge_keys, edge)
|
|
143
|
-
limitations.extend(graph.summary.limitations)
|
|
223
|
+
nodes = nodes[: self.max_nodes]
|
|
224
|
+
edges = edges[: self.max_edges]
|
|
144
225
|
return ModuleGraph(
|
|
145
226
|
nodes=nodes,
|
|
146
227
|
edges=edges,
|
|
147
228
|
summary=ModuleGraphSummary(
|
|
148
|
-
requested=
|
|
229
|
+
requested=True,
|
|
149
230
|
node_count=len(nodes),
|
|
150
231
|
edge_count=len(edges),
|
|
151
232
|
languages=sorted({node.language for node in nodes}),
|
|
152
233
|
methods=sorted({edge.method for edge in edges}),
|
|
234
|
+
detail="full",
|
|
235
|
+
edge_kinds=sorted({edge.kind for edge in edges}),
|
|
153
236
|
limitations=self._unique(limitations),
|
|
154
237
|
),
|
|
155
238
|
)
|
|
156
239
|
|
|
157
|
-
def
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
240
|
+
def _project_graph(
|
|
241
|
+
self,
|
|
242
|
+
graph: ModuleGraph,
|
|
243
|
+
*,
|
|
244
|
+
detail: GraphDetail,
|
|
245
|
+
edge_kinds: set[str] | None,
|
|
246
|
+
max_nodes: int | None,
|
|
247
|
+
entry_points: list[EntryPoint],
|
|
248
|
+
) -> ModuleGraph:
|
|
249
|
+
active_edge_kinds = edge_kinds or set(self._DEFAULT_EDGE_KINDS[detail])
|
|
250
|
+
budget = (
|
|
251
|
+
max_nodes if detail != "full" and max_nodes is not None else self._DEFAULT_MAX_NODES[detail]
|
|
252
|
+
)
|
|
253
|
+
ranked_nodes = self._rank_nodes(graph, entry_points)
|
|
254
|
+
importance_by_id = {node.id: node.importance for node in ranked_nodes}
|
|
255
|
+
if detail == "full":
|
|
256
|
+
projected_nodes = ranked_nodes
|
|
257
|
+
projected_edges = [
|
|
258
|
+
edge for edge in graph.edges if edge.kind in active_edge_kinds
|
|
259
|
+
]
|
|
260
|
+
truncated = False
|
|
261
|
+
base_limitations = list(graph.summary.limitations)
|
|
262
|
+
elif detail == "medium":
|
|
263
|
+
projected_nodes = self._select_medium_nodes(ranked_nodes, entry_points)
|
|
264
|
+
projected_nodes = self._expand_medium_call_targets(projected_nodes, ranked_nodes, graph.edges)
|
|
265
|
+
projected_edges = [
|
|
266
|
+
edge
|
|
267
|
+
for edge in graph.edges
|
|
268
|
+
if edge.kind in active_edge_kinds
|
|
269
|
+
and edge.source in {node.id for node in projected_nodes}
|
|
270
|
+
and edge.target in {node.id for node in projected_nodes}
|
|
271
|
+
]
|
|
272
|
+
projected_nodes, projected_edges, truncated, base_limitations = self._apply_budget(
|
|
273
|
+
projected_nodes,
|
|
274
|
+
projected_edges,
|
|
275
|
+
importance_by_id,
|
|
276
|
+
budget,
|
|
277
|
+
list(graph.summary.limitations),
|
|
278
|
+
)
|
|
279
|
+
else:
|
|
280
|
+
collapsed_nodes, collapsed_edges = self._collapse_high_graph(
|
|
281
|
+
ranked_nodes,
|
|
282
|
+
graph.edges,
|
|
283
|
+
entry_points,
|
|
284
|
+
)
|
|
285
|
+
projected_nodes, projected_edges, truncated, base_limitations = self._apply_budget(
|
|
286
|
+
collapsed_nodes,
|
|
287
|
+
[edge for edge in collapsed_edges if edge.kind in active_edge_kinds],
|
|
288
|
+
{node.id: node.importance for node in collapsed_nodes},
|
|
289
|
+
budget,
|
|
290
|
+
list(graph.summary.limitations),
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
summary = self._build_summary(
|
|
294
|
+
projected_nodes,
|
|
295
|
+
projected_edges,
|
|
296
|
+
detail=detail,
|
|
297
|
+
edge_kinds=active_edge_kinds,
|
|
298
|
+
budget=budget,
|
|
299
|
+
entry_points=entry_points,
|
|
300
|
+
limitations=base_limitations,
|
|
301
|
+
truncated=truncated,
|
|
302
|
+
)
|
|
303
|
+
return ModuleGraph(nodes=projected_nodes, edges=projected_edges, summary=summary)
|
|
304
|
+
|
|
305
|
+
def _rank_nodes(self, graph: ModuleGraph, entry_points: list[EntryPoint]) -> list[GraphNode]:
|
|
306
|
+
module_entry_ids = self._entry_point_module_ids(entry_points, graph.nodes)
|
|
307
|
+
incoming_imports = Counter(
|
|
308
|
+
edge.target for edge in graph.edges if edge.kind == "imports"
|
|
309
|
+
)
|
|
310
|
+
incoming_calls = Counter(
|
|
311
|
+
edge.target for edge in graph.edges if edge.kind == "calls"
|
|
312
|
+
)
|
|
313
|
+
outgoing_calls = Counter(
|
|
314
|
+
edge.source for edge in graph.edges if edge.kind == "calls"
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
scored_nodes: list[tuple[int, GraphNode]] = []
|
|
162
318
|
for node in graph.nodes:
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
319
|
+
score = 0
|
|
320
|
+
if node.id in module_entry_ids:
|
|
321
|
+
score += 100
|
|
322
|
+
if node.kind == "module":
|
|
323
|
+
score += 20
|
|
324
|
+
score += incoming_imports[node.id] * 10
|
|
325
|
+
elif node.kind == "function":
|
|
326
|
+
score += outgoing_calls[node.id] * 20
|
|
327
|
+
score += incoming_calls[node.id] * 8
|
|
328
|
+
elif node.kind == "class":
|
|
329
|
+
score += incoming_imports[node.id] * 6
|
|
330
|
+
|
|
331
|
+
if node.path.endswith(("main.py", "index.ts", "index.js", "__main__.py")):
|
|
332
|
+
score += 20
|
|
333
|
+
|
|
334
|
+
importance = self._importance_from_score(score, node.kind)
|
|
335
|
+
scored_nodes.append((score, replace(node, importance=importance)))
|
|
336
|
+
|
|
337
|
+
scored_nodes.sort(
|
|
338
|
+
key=lambda item: (
|
|
339
|
+
-item[0],
|
|
340
|
+
0 if item[1].kind == "module" else 1,
|
|
341
|
+
item[1].path,
|
|
342
|
+
item[1].symbol or "",
|
|
173
343
|
)
|
|
174
|
-
|
|
175
|
-
for
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
344
|
+
)
|
|
345
|
+
return [node for _score, node in scored_nodes]
|
|
346
|
+
|
|
347
|
+
def _importance_from_score(self, score: int, kind: str) -> GraphImportance:
|
|
348
|
+
if score >= 40:
|
|
349
|
+
return "high"
|
|
350
|
+
if kind == "module" and score >= 20:
|
|
351
|
+
return "medium"
|
|
352
|
+
if score >= 12:
|
|
353
|
+
return "medium"
|
|
354
|
+
return "low"
|
|
355
|
+
|
|
356
|
+
def _select_medium_nodes(
|
|
357
|
+
self,
|
|
358
|
+
ranked_nodes: list[GraphNode],
|
|
359
|
+
entry_points: list[EntryPoint],
|
|
360
|
+
) -> list[GraphNode]:
|
|
361
|
+
entry_module_ids = self._entry_point_module_ids(entry_points, ranked_nodes)
|
|
362
|
+
selected: list[GraphNode] = []
|
|
363
|
+
seen: set[str] = set()
|
|
364
|
+
for node in ranked_nodes:
|
|
365
|
+
include = node.kind == "module" or node.importance == "high"
|
|
366
|
+
if node.id in entry_module_ids:
|
|
367
|
+
include = True
|
|
368
|
+
if include and node.id not in seen:
|
|
369
|
+
seen.add(node.id)
|
|
370
|
+
selected.append(node)
|
|
371
|
+
return selected
|
|
372
|
+
|
|
373
|
+
def _expand_medium_call_targets(
|
|
374
|
+
self,
|
|
375
|
+
selected_nodes: list[GraphNode],
|
|
376
|
+
ranked_nodes: list[GraphNode],
|
|
377
|
+
edges: list[GraphEdge],
|
|
378
|
+
) -> list[GraphNode]:
|
|
379
|
+
node_map = {node.id: node for node in ranked_nodes}
|
|
380
|
+
selected_ids = {node.id for node in selected_nodes}
|
|
381
|
+
expanded = list(selected_nodes)
|
|
382
|
+
for edge in edges:
|
|
383
|
+
if edge.kind != "calls":
|
|
384
|
+
continue
|
|
385
|
+
if edge.source in selected_ids and edge.target in node_map and edge.target not in selected_ids:
|
|
386
|
+
selected_ids.add(edge.target)
|
|
387
|
+
expanded.append(node_map[edge.target])
|
|
388
|
+
return expanded
|
|
389
|
+
|
|
390
|
+
def _collapse_high_graph(
|
|
391
|
+
self,
|
|
392
|
+
ranked_nodes: list[GraphNode],
|
|
393
|
+
edges: list[GraphEdge],
|
|
394
|
+
entry_points: list[EntryPoint],
|
|
395
|
+
) -> tuple[list[GraphNode], list[GraphEdge]]:
|
|
396
|
+
module_nodes = [node for node in ranked_nodes if node.kind == "module"]
|
|
397
|
+
entry_module_ids = self._entry_point_module_ids(entry_points, module_nodes)
|
|
398
|
+
group_map = self._build_high_group_map(module_nodes, entry_module_ids)
|
|
399
|
+
grouped_nodes: dict[str, GraphNode] = {}
|
|
400
|
+
for node in module_nodes:
|
|
401
|
+
group_path = group_map[node.id]
|
|
402
|
+
if group_path not in grouped_nodes:
|
|
403
|
+
importance: GraphImportance = "high" if node.id in entry_module_ids else "medium"
|
|
404
|
+
grouped_nodes[group_path] = GraphNode(
|
|
405
|
+
id=f"module:{group_path}",
|
|
406
|
+
kind="module",
|
|
407
|
+
language=node.language,
|
|
408
|
+
path=group_path,
|
|
409
|
+
display_name=group_path,
|
|
410
|
+
workspace=node.workspace,
|
|
411
|
+
importance=importance,
|
|
181
412
|
)
|
|
413
|
+
elif node.importance == "high":
|
|
414
|
+
grouped_nodes[group_path] = replace(
|
|
415
|
+
grouped_nodes[group_path],
|
|
416
|
+
importance="high",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
grouped_edges: list[GraphEdge] = []
|
|
420
|
+
edge_keys: set[tuple[str, str, str, str]] = set()
|
|
421
|
+
for edge in edges:
|
|
422
|
+
if edge.kind != "imports":
|
|
423
|
+
continue
|
|
424
|
+
source_group = group_map.get(edge.source)
|
|
425
|
+
target_group = group_map.get(edge.target)
|
|
426
|
+
if source_group is None or target_group is None or source_group == target_group:
|
|
427
|
+
continue
|
|
428
|
+
grouped_edge = GraphEdge(
|
|
429
|
+
source=f"module:{source_group}",
|
|
430
|
+
target=f"module:{target_group}",
|
|
431
|
+
kind="imports",
|
|
432
|
+
confidence=edge.confidence,
|
|
433
|
+
method=edge.method,
|
|
182
434
|
)
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
435
|
+
self._append_edge(grouped_edges, edge_keys, grouped_edge)
|
|
436
|
+
|
|
437
|
+
return list(grouped_nodes.values()), grouped_edges
|
|
438
|
+
|
|
439
|
+
def _build_high_group_map(
|
|
440
|
+
self,
|
|
441
|
+
module_nodes: list[GraphNode],
|
|
442
|
+
entry_module_ids: set[str],
|
|
443
|
+
) -> dict[str, str]:
|
|
444
|
+
modules_by_parent: dict[str, list[GraphNode]] = defaultdict(list)
|
|
445
|
+
for node in module_nodes:
|
|
446
|
+
parent = str(PurePosixPath(node.path).parent)
|
|
447
|
+
modules_by_parent[parent].append(node)
|
|
448
|
+
|
|
449
|
+
group_map: dict[str, str] = {}
|
|
450
|
+
for node in module_nodes:
|
|
451
|
+
parent = str(PurePosixPath(node.path).parent)
|
|
452
|
+
if parent not in {".", ""} and len(modules_by_parent[parent]) > 1 and node.id not in entry_module_ids:
|
|
453
|
+
group_map[node.id] = parent
|
|
454
|
+
else:
|
|
455
|
+
stem_path = str(PurePosixPath(node.path).with_suffix(""))
|
|
456
|
+
if stem_path.endswith("/__init__"):
|
|
457
|
+
stem_path = stem_path[: -len("/__init__")]
|
|
458
|
+
group_map[node.id] = stem_path or node.path
|
|
459
|
+
return group_map
|
|
460
|
+
|
|
461
|
+
def _apply_budget(
|
|
462
|
+
self,
|
|
463
|
+
nodes: list[GraphNode],
|
|
464
|
+
edges: list[GraphEdge],
|
|
465
|
+
importance_by_id: dict[str, GraphImportance | None],
|
|
466
|
+
budget: int | None,
|
|
467
|
+
limitations: list[str],
|
|
468
|
+
) -> tuple[list[GraphNode], list[GraphEdge], bool, list[str]]:
|
|
469
|
+
if budget is None or len(nodes) <= budget:
|
|
470
|
+
return nodes, edges, False, limitations
|
|
471
|
+
|
|
472
|
+
priority = {"high": 0, "medium": 1, "low": 2, None: 3}
|
|
473
|
+
trimmed_nodes = sorted(
|
|
474
|
+
nodes,
|
|
475
|
+
key=lambda node: (
|
|
476
|
+
priority[importance_by_id.get(node.id)],
|
|
477
|
+
0 if node.kind == "module" else 1,
|
|
478
|
+
node.path,
|
|
479
|
+
node.symbol or "",
|
|
480
|
+
),
|
|
481
|
+
)[:budget]
|
|
482
|
+
kept_ids = {node.id for node in trimmed_nodes}
|
|
483
|
+
trimmed_edges = [
|
|
484
|
+
edge
|
|
485
|
+
for edge in edges
|
|
486
|
+
if edge.source in kept_ids and edge.target in kept_ids
|
|
487
|
+
]
|
|
488
|
+
new_limitations = list(limitations)
|
|
489
|
+
new_limitations.append(f"node_budget_applied:{len(nodes)}->{budget}")
|
|
490
|
+
return trimmed_nodes, trimmed_edges, True, self._unique(new_limitations)
|
|
491
|
+
|
|
492
|
+
def _build_summary(
|
|
493
|
+
self,
|
|
494
|
+
nodes: list[GraphNode],
|
|
495
|
+
edges: list[GraphEdge],
|
|
496
|
+
*,
|
|
497
|
+
detail: GraphDetail,
|
|
498
|
+
edge_kinds: set[str],
|
|
499
|
+
budget: int | None,
|
|
500
|
+
entry_points: list[EntryPoint],
|
|
501
|
+
limitations: list[str],
|
|
502
|
+
truncated: bool,
|
|
503
|
+
) -> ModuleGraphSummary:
|
|
504
|
+
return ModuleGraphSummary(
|
|
505
|
+
requested=True,
|
|
506
|
+
node_count=len(nodes),
|
|
507
|
+
edge_count=len(edges),
|
|
508
|
+
languages=sorted({node.language for node in nodes}),
|
|
509
|
+
methods=sorted({edge.method for edge in edges}),
|
|
510
|
+
main_flows=self._derive_main_flows(nodes, edges, entry_points),
|
|
511
|
+
layers=self._infer_layers(nodes),
|
|
512
|
+
entry_points_count=len(entry_points),
|
|
513
|
+
truncated=truncated,
|
|
514
|
+
detail=detail,
|
|
515
|
+
max_nodes_applied=budget,
|
|
516
|
+
edge_kinds=sorted(edge_kinds),
|
|
517
|
+
limitations=self._unique(limitations),
|
|
187
518
|
)
|
|
188
519
|
|
|
520
|
+
def _derive_main_flows(
|
|
521
|
+
self,
|
|
522
|
+
nodes: list[GraphNode],
|
|
523
|
+
edges: list[GraphEdge],
|
|
524
|
+
entry_points: list[EntryPoint],
|
|
525
|
+
) -> list[str]:
|
|
526
|
+
node_map = {node.id: node for node in nodes}
|
|
527
|
+
adjacency: dict[str, list[str]] = defaultdict(list)
|
|
528
|
+
for edge in edges:
|
|
529
|
+
if edge.kind not in {"imports", "calls"}:
|
|
530
|
+
continue
|
|
531
|
+
adjacency[edge.source].append(edge.target)
|
|
532
|
+
|
|
533
|
+
flows: list[str] = []
|
|
534
|
+
entry_module_ids = self._entry_point_module_ids(entry_points, nodes)
|
|
535
|
+
for source_id in sorted(entry_module_ids):
|
|
536
|
+
if source_id not in adjacency and source_id not in node_map:
|
|
537
|
+
continue
|
|
538
|
+
walk = [self._node_label(node_map[source_id])]
|
|
539
|
+
current = source_id
|
|
540
|
+
visited = {source_id}
|
|
541
|
+
for _ in range(3):
|
|
542
|
+
candidates = [
|
|
543
|
+
target for target in adjacency.get(current, [])
|
|
544
|
+
if target in node_map and target not in visited
|
|
545
|
+
]
|
|
546
|
+
if not candidates:
|
|
547
|
+
break
|
|
548
|
+
next_target = sorted(
|
|
549
|
+
candidates,
|
|
550
|
+
key=lambda node_id: (
|
|
551
|
+
0 if (node_map[node_id].importance == "high") else 1,
|
|
552
|
+
0 if node_map[node_id].kind == "module" else 1,
|
|
553
|
+
node_map[node_id].path,
|
|
554
|
+
),
|
|
555
|
+
)[0]
|
|
556
|
+
visited.add(next_target)
|
|
557
|
+
walk.append(self._node_label(node_map[next_target]))
|
|
558
|
+
current = next_target
|
|
559
|
+
if len(walk) > 1:
|
|
560
|
+
flows.append(" -> ".join(walk))
|
|
561
|
+
if len(flows) >= 3:
|
|
562
|
+
break
|
|
563
|
+
return flows
|
|
564
|
+
|
|
565
|
+
def _infer_layers(self, nodes: list[GraphNode]) -> list[str]:
|
|
566
|
+
layers: list[str] = []
|
|
567
|
+
for node in nodes:
|
|
568
|
+
if node.kind != "module":
|
|
569
|
+
continue
|
|
570
|
+
parts = PurePosixPath(node.path).parts
|
|
571
|
+
if not parts:
|
|
572
|
+
continue
|
|
573
|
+
layer = parts[0] if len(parts) > 1 else PurePosixPath(node.path).stem
|
|
574
|
+
if layer not in layers:
|
|
575
|
+
layers.append(layer)
|
|
576
|
+
return layers[:10]
|
|
577
|
+
|
|
578
|
+
def _entry_point_module_ids(
|
|
579
|
+
self,
|
|
580
|
+
entry_points: list[EntryPoint],
|
|
581
|
+
nodes: list[GraphNode],
|
|
582
|
+
) -> set[str]:
|
|
583
|
+
module_ids = {node.id for node in nodes if node.kind == "module"}
|
|
584
|
+
resolved: set[str] = set()
|
|
585
|
+
for entry_point in entry_points:
|
|
586
|
+
entry_path = entry_point.path.strip("/")
|
|
587
|
+
candidates = [f"module:{entry_path}"]
|
|
588
|
+
if "/" in entry_path:
|
|
589
|
+
parent = entry_path.rsplit("/", 1)[0]
|
|
590
|
+
candidates.extend(
|
|
591
|
+
[
|
|
592
|
+
f"module:{parent}/main.py",
|
|
593
|
+
f"module:{parent}/src/main.py",
|
|
594
|
+
f"module:{parent}/index.ts",
|
|
595
|
+
f"module:{parent}/src/index.ts",
|
|
596
|
+
f"module:{parent}/index.js",
|
|
597
|
+
f"module:{parent}/src/index.js",
|
|
598
|
+
]
|
|
599
|
+
)
|
|
600
|
+
for candidate in candidates:
|
|
601
|
+
if candidate in module_ids:
|
|
602
|
+
resolved.add(candidate)
|
|
603
|
+
break
|
|
604
|
+
return resolved
|
|
605
|
+
|
|
606
|
+
def _node_label(self, node: GraphNode) -> str:
|
|
607
|
+
return node.display_name or node.symbol or node.path
|
|
608
|
+
|
|
189
609
|
def _analyze_python_file(
|
|
190
610
|
self,
|
|
191
611
|
relative_path: str,
|
|
@@ -288,12 +708,8 @@ class GraphAnalyzer:
|
|
|
288
708
|
)
|
|
289
709
|
for alias in ast_node.names:
|
|
290
710
|
local_name = alias.asname or alias.name
|
|
291
|
-
imported_function_node_ids[local_name] =
|
|
292
|
-
|
|
293
|
-
)
|
|
294
|
-
imported_class_node_ids[local_name] = (
|
|
295
|
-
f"class:{target_path}:{alias.name}"
|
|
296
|
-
)
|
|
711
|
+
imported_function_node_ids[local_name] = f"function:{target_path}:{alias.name}"
|
|
712
|
+
imported_class_node_ids[local_name] = f"class:{target_path}:{alias.name}"
|
|
297
713
|
|
|
298
714
|
for top_level in tree.body:
|
|
299
715
|
if isinstance(top_level, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
@@ -321,9 +737,7 @@ class GraphAnalyzer:
|
|
|
321
737
|
continue
|
|
322
738
|
for base in top_level.bases:
|
|
323
739
|
if isinstance(base, ast.Name):
|
|
324
|
-
target_id = class_node_ids.get(base.id) or imported_class_node_ids.get(
|
|
325
|
-
base.id
|
|
326
|
-
)
|
|
740
|
+
target_id = class_node_ids.get(base.id) or imported_class_node_ids.get(base.id)
|
|
327
741
|
if target_id is not None:
|
|
328
742
|
edges.append(
|
|
329
743
|
GraphEdge(
|
sourcecode/schema.py
CHANGED
|
@@ -107,6 +107,7 @@ class GraphNode:
|
|
|
107
107
|
symbol: Optional[str] = None
|
|
108
108
|
display_name: Optional[str] = None
|
|
109
109
|
workspace: Optional[str] = None
|
|
110
|
+
importance: Optional[Literal["high", "medium", "low"]] = None
|
|
110
111
|
|
|
111
112
|
|
|
112
113
|
@dataclass
|
|
@@ -129,6 +130,13 @@ class ModuleGraphSummary:
|
|
|
129
130
|
edge_count: int = 0
|
|
130
131
|
languages: list[str] = field(default_factory=list)
|
|
131
132
|
methods: list[str] = field(default_factory=list)
|
|
133
|
+
main_flows: list[str] = field(default_factory=list)
|
|
134
|
+
layers: list[str] = field(default_factory=list)
|
|
135
|
+
entry_points_count: int = 0
|
|
136
|
+
truncated: bool = False
|
|
137
|
+
detail: Optional[Literal["high", "medium", "full"]] = None
|
|
138
|
+
max_nodes_applied: Optional[int] = None
|
|
139
|
+
edge_kinds: list[str] = field(default_factory=list)
|
|
132
140
|
limitations: list[str] = field(default_factory=list)
|
|
133
141
|
|
|
134
142
|
|
|
@@ -161,3 +169,4 @@ class SourceMap:
|
|
|
161
169
|
dependencies: list[DependencyRecord] = field(default_factory=list)
|
|
162
170
|
dependency_summary: Optional[DependencySummary] = None
|
|
163
171
|
module_graph: Optional[ModuleGraph] = None
|
|
172
|
+
module_graph_summary: Optional[ModuleGraphSummary] = None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
|
|
5
5
|
License: MIT
|
|
6
6
|
Requires-Python: >=3.9
|
|
@@ -58,6 +58,14 @@ Include an internal module graph with imports and simple structural relations:
|
|
|
58
58
|
sourcecode . --graph-modules
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
+
Choose how much graph detail you want:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
sourcecode . --graph-modules --graph-detail high
|
|
65
|
+
sourcecode . --graph-modules --graph-detail medium
|
|
66
|
+
sourcecode . --graph-modules --graph-detail full
|
|
67
|
+
```
|
|
68
|
+
|
|
61
69
|
Show the version:
|
|
62
70
|
|
|
63
71
|
```bash
|
|
@@ -70,7 +78,10 @@ Main options:
|
|
|
70
78
|
- `--output PATH`: write to a file instead of `stdout`.
|
|
71
79
|
- `--compact`: return a reduced view with `schema_version`, `project_type`, `stacks`, `entry_points`, and `file_tree_depth1`.
|
|
72
80
|
- `--dependencies`: include direct dependencies, resolved versions, and transitive relationships when lockfiles make that possible.
|
|
73
|
-
- `--graph-modules`: include a structural graph
|
|
81
|
+
- `--graph-modules`: include a structural graph optimized for repository reasoning.
|
|
82
|
+
- `--graph-detail high|medium|full`: choose a summarized, balanced, or full-fidelity graph. Default: `high`.
|
|
83
|
+
- `--max-nodes INTEGER`: cap graph size in `high` and `medium` modes.
|
|
84
|
+
- `--graph-edges imports,calls,contains,extends`: override the default edge kinds for the selected detail level.
|
|
74
85
|
- `--depth INTEGER`: maximum file tree depth.
|
|
75
86
|
- `--no-redact`: disable secret redaction.
|
|
76
87
|
|
|
@@ -169,6 +180,7 @@ The full schema includes:
|
|
|
169
180
|
- `dependencies`: optional dependency records with declared and resolved versions.
|
|
170
181
|
- `dependency_summary`: optional summary with ecosystem coverage, counts, and known limitations.
|
|
171
182
|
- `module_graph`: optional structural graph with nodes, edges, and analysis limits.
|
|
183
|
+
- `module_graph_summary`: compact graph summary optimized for downstream LLM consumption.
|
|
172
184
|
|
|
173
185
|
Example dependency block:
|
|
174
186
|
|
|
@@ -219,52 +231,52 @@ Example module graph block:
|
|
|
219
231
|
"module_graph": {
|
|
220
232
|
"nodes": [
|
|
221
233
|
{
|
|
222
|
-
"id": "module:app
|
|
234
|
+
"id": "module:app",
|
|
223
235
|
"kind": "module",
|
|
224
236
|
"language": "python",
|
|
225
|
-
"path": "app
|
|
237
|
+
"path": "app",
|
|
226
238
|
"symbol": null,
|
|
227
|
-
"display_name": "app
|
|
228
|
-
"workspace": null
|
|
229
|
-
|
|
230
|
-
{
|
|
231
|
-
"id": "function:app/main.py:run",
|
|
232
|
-
"kind": "function",
|
|
233
|
-
"language": "python",
|
|
234
|
-
"path": "app/main.py",
|
|
235
|
-
"symbol": "run",
|
|
236
|
-
"display_name": "run",
|
|
237
|
-
"workspace": null
|
|
238
|
-
}
|
|
239
|
-
],
|
|
240
|
-
"edges": [
|
|
241
|
-
{
|
|
242
|
-
"source": "module:app/main.py",
|
|
243
|
-
"target": "module:app/utils.py",
|
|
244
|
-
"kind": "imports",
|
|
245
|
-
"confidence": "high",
|
|
246
|
-
"method": "ast"
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
"source": "function:app/main.py:run",
|
|
250
|
-
"target": "function:app/utils.py:helper",
|
|
251
|
-
"kind": "calls",
|
|
252
|
-
"confidence": "medium",
|
|
253
|
-
"method": "ast"
|
|
239
|
+
"display_name": "app",
|
|
240
|
+
"workspace": null,
|
|
241
|
+
"importance": "high"
|
|
254
242
|
}
|
|
255
243
|
],
|
|
244
|
+
"edges": [],
|
|
256
245
|
"summary": {
|
|
257
246
|
"requested": true,
|
|
258
|
-
"node_count":
|
|
259
|
-
"edge_count":
|
|
247
|
+
"node_count": 1,
|
|
248
|
+
"edge_count": 0,
|
|
260
249
|
"languages": ["python"],
|
|
261
250
|
"methods": ["ast"],
|
|
251
|
+
"main_flows": [],
|
|
252
|
+
"layers": ["app"],
|
|
253
|
+
"entry_points_count": 1,
|
|
254
|
+
"truncated": false,
|
|
255
|
+
"detail": "high",
|
|
256
|
+
"max_nodes_applied": 80,
|
|
257
|
+
"edge_kinds": ["imports"],
|
|
262
258
|
"limitations": []
|
|
263
259
|
}
|
|
260
|
+
},
|
|
261
|
+
"module_graph_summary": {
|
|
262
|
+
"requested": true,
|
|
263
|
+
"node_count": 1,
|
|
264
|
+
"edge_count": 0,
|
|
265
|
+
"main_flows": [],
|
|
266
|
+
"layers": ["app"],
|
|
267
|
+
"entry_points_count": 1,
|
|
268
|
+
"truncated": false,
|
|
269
|
+
"limitations": []
|
|
264
270
|
}
|
|
265
271
|
}
|
|
266
272
|
```
|
|
267
273
|
|
|
274
|
+
`--graph-modules` is now tiered for LLM workflows:
|
|
275
|
+
|
|
276
|
+
- `high`: summarized graph, modules only, imports only, directory collapsing when useful.
|
|
277
|
+
- `medium`: balanced graph with key functions and selected call edges.
|
|
278
|
+
- `full`: full-fidelity graph, equivalent to the previous exhaustive behavior.
|
|
279
|
+
|
|
268
280
|
Graph analysis is also offline and conservative. `sourcecode` prefers partial but defensible edges over pretending to build a perfect semantic call graph, and it records parse failures, unresolved imports, or analysis budgets in `module_graph.summary.limitations`.
|
|
269
281
|
|
|
270
282
|
Detailed reference: [docs/schema.md](/Users/user/Documents/workspace/atlas/atlas-cli/docs/schema.md).
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=IMpF3qM4eqwxM-5Ikwwjo1JciPAcsosSo9qB_3jqdRY,99
|
|
2
2
|
sourcecode/classifier.py,sha256=7ubGTn5vfw9z-9eosLIYDtQUu7d8dvB4XKKDxysUVSM,6910
|
|
3
|
-
sourcecode/cli.py,sha256=
|
|
3
|
+
sourcecode/cli.py,sha256=zpnh4NB46fWkBuP3el5vtgBH1CSmxzRmjRtR0NymR_U,12193
|
|
4
4
|
sourcecode/dependency_analyzer.py,sha256=c8tSbUpug-2avKNWCP4-_e2_uhBwWl96LwL1Be8O90M,39067
|
|
5
|
-
sourcecode/graph_analyzer.py,sha256=
|
|
5
|
+
sourcecode/graph_analyzer.py,sha256=S5z8mrGfLwN26Mt2IaCR4dOpQ_APU16cfihSp640eGQ,45241
|
|
6
6
|
sourcecode/redactor.py,sha256=abSf5jH_IzYzpjF3lEN6hCwdXxvmyHs-3DHz9fd8Mic,2883
|
|
7
7
|
sourcecode/scanner.py,sha256=TvcuD77m_NF8OlTj3XH1b_jO0qrto8c90fND96ecqb0,6182
|
|
8
|
-
sourcecode/schema.py,sha256=
|
|
8
|
+
sourcecode/schema.py,sha256=YaO1kWgWwH1iA8dUEQPJOLz2G-YRMrUEVXfkJo01xWE,4976
|
|
9
9
|
sourcecode/serializer.py,sha256=hezTYOAAvZ2yPn7Te7Eu42ajbMhVCsUkuUZtqx4wjAs,3085
|
|
10
10
|
sourcecode/tree_utils.py,sha256=5WHQc2L-AGqxKFfYYcQftE6hETxgUxP9MWDuaThcAUw,991
|
|
11
11
|
sourcecode/workspace.py,sha256=1L4xH38gU89fLeMYSuF3ft7lSdCyTfMfJ9NQt5q4Ric,6904
|
|
@@ -28,7 +28,7 @@ sourcecode/detectors/rust.py,sha256=iUz9lE4HfBy8CBaDwGhfh6kLsNIovrH2ay2v3y6qNDc,
|
|
|
28
28
|
sourcecode/detectors/systems.py,sha256=qf5M9tio1H6XDorSstTWIxKe0xVLR_QGTJpFGyhDz4A,1690
|
|
29
29
|
sourcecode/detectors/terraform.py,sha256=5EOKQtTViWo7u3tFK0h45YD8GQPwhrww0cOicxGPPn4,1732
|
|
30
30
|
sourcecode/detectors/tooling.py,sha256=wPvudLCJZ8_cr9GIx44C8jkLAlEN9uTUZcJsP7eVYnQ,1937
|
|
31
|
-
sourcecode-0.
|
|
32
|
-
sourcecode-0.
|
|
33
|
-
sourcecode-0.
|
|
34
|
-
sourcecode-0.
|
|
31
|
+
sourcecode-0.5.0.dist-info/METADATA,sha256=bAVVzEAxUwWc_PGiVzqffpiQh3r6q_Q91WJu7xEgo6M,7976
|
|
32
|
+
sourcecode-0.5.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
33
|
+
sourcecode-0.5.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
34
|
+
sourcecode-0.5.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|