sourcecode 0.9.0__py3-none-any.whl → 0.11.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/architecture_summary.py +248 -0
- sourcecode/cli.py +34 -1
- sourcecode/detectors/go.py +7 -1
- sourcecode/detectors/nodejs.py +30 -11
- sourcecode/detectors/python.py +28 -6
- sourcecode/prepare_context.py +197 -0
- sourcecode/schema.py +2 -0
- sourcecode/serializer.py +2 -2
- sourcecode/summarizer.py +114 -5
- {sourcecode-0.9.0.dist-info → sourcecode-0.11.0.dist-info}/METADATA +7 -6
- {sourcecode-0.9.0.dist-info → sourcecode-0.11.0.dist-info}/RECORD +14 -12
- {sourcecode-0.9.0.dist-info → sourcecode-0.11.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.9.0.dist-info → sourcecode-0.11.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
"""Resumen arquitectonico estatico y compacto para consumo LLM."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import ast
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sourcecode.schema import EntryPoint, SourceMap, StackDetection
|
|
10
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
11
|
+
|
|
12
|
+
_TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
13
|
+
_PYTHON_EXTENSIONS = {".py"}
|
|
14
|
+
_NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
15
|
+
_GO_EXTENSIONS = {".go"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ArchitectureSummarizer:
|
|
19
|
+
"""Construye un resumen arquitectonico estatico de 3-5 lineas."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, root: Path) -> None:
|
|
22
|
+
self.root = root
|
|
23
|
+
|
|
24
|
+
def generate(self, sm: SourceMap) -> str | None:
|
|
25
|
+
try:
|
|
26
|
+
return self._build_summary(sm)
|
|
27
|
+
except Exception:
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
def _build_summary(self, sm: SourceMap) -> str | None:
|
|
31
|
+
file_paths = [
|
|
32
|
+
path for path in flatten_file_tree(sm.file_tree)
|
|
33
|
+
if not self._is_tooling_path(path)
|
|
34
|
+
]
|
|
35
|
+
if not file_paths:
|
|
36
|
+
return None
|
|
37
|
+
|
|
38
|
+
entry_points = [
|
|
39
|
+
entry for entry in sm.entry_points
|
|
40
|
+
if not self._is_tooling_path(entry.path)
|
|
41
|
+
]
|
|
42
|
+
if not entry_points:
|
|
43
|
+
fallback = self._infer_fallback_entry_points(file_paths, sm.stacks)
|
|
44
|
+
entry_points = fallback[:1]
|
|
45
|
+
|
|
46
|
+
if not entry_points:
|
|
47
|
+
return "Arquitectura no inferida con suficiente evidencia estatica."
|
|
48
|
+
|
|
49
|
+
entry_point = entry_points[0]
|
|
50
|
+
content = self._read_file(entry_point.path)
|
|
51
|
+
if content is None:
|
|
52
|
+
return f"Entry point principal: {entry_point.path}. Arquitectura no inferida con suficiente evidencia estatica."
|
|
53
|
+
|
|
54
|
+
lines = [self._describe_entry_point(entry_point, sm.project_type)]
|
|
55
|
+
suffix = Path(entry_point.path).suffix
|
|
56
|
+
if suffix in _PYTHON_EXTENSIONS:
|
|
57
|
+
lines.extend(self._summarize_python_entry(entry_point.path, content))
|
|
58
|
+
elif suffix in _NODE_EXTENSIONS:
|
|
59
|
+
lines.extend(self._summarize_node_entry(entry_point.path, content))
|
|
60
|
+
elif suffix in _GO_EXTENSIONS:
|
|
61
|
+
lines.extend(self._summarize_go_entry(entry_point.path, content))
|
|
62
|
+
else:
|
|
63
|
+
lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
|
|
64
|
+
|
|
65
|
+
unique_lines: list[str] = []
|
|
66
|
+
seen: set[str] = set()
|
|
67
|
+
for line in lines:
|
|
68
|
+
line = line.strip()
|
|
69
|
+
if not line or line in seen:
|
|
70
|
+
continue
|
|
71
|
+
seen.add(line)
|
|
72
|
+
unique_lines.append(line)
|
|
73
|
+
return "\n".join(unique_lines[:5]) if unique_lines else None
|
|
74
|
+
|
|
75
|
+
def _summarize_python_entry(self, path: str, content: str) -> list[str]:
|
|
76
|
+
try:
|
|
77
|
+
tree = ast.parse(content)
|
|
78
|
+
except SyntaxError:
|
|
79
|
+
return ["Orquesta modulos Python, pero el entry point no pudo parsearse por completo."]
|
|
80
|
+
|
|
81
|
+
package_prefix = self._python_package_prefix(path)
|
|
82
|
+
imported_modules: list[str] = []
|
|
83
|
+
optional_analyzers: list[str] = []
|
|
84
|
+
uses_serializer = False
|
|
85
|
+
uses_redactor = False
|
|
86
|
+
|
|
87
|
+
for node in ast.walk(tree):
|
|
88
|
+
if isinstance(node, ast.ImportFrom) and isinstance(node.module, str):
|
|
89
|
+
if package_prefix and node.module.startswith(package_prefix):
|
|
90
|
+
imported_modules.append(node.module.rsplit(".", 1)[-1])
|
|
91
|
+
if node.module == "sourcecode.serializer":
|
|
92
|
+
uses_serializer = True
|
|
93
|
+
if node.module == "sourcecode.redactor":
|
|
94
|
+
uses_redactor = True
|
|
95
|
+
elif isinstance(node, ast.Assign):
|
|
96
|
+
optional = self._extract_optional_analyzer(node)
|
|
97
|
+
if optional:
|
|
98
|
+
optional_analyzers.append(optional)
|
|
99
|
+
|
|
100
|
+
lines: list[str] = []
|
|
101
|
+
core_modules = self._format_module_list(imported_modules)
|
|
102
|
+
if core_modules:
|
|
103
|
+
lines.append(f"Orquesta modulos internos: {core_modules}.")
|
|
104
|
+
if optional_analyzers:
|
|
105
|
+
lines.append(f"Activa analisis opcionales: {', '.join(optional_analyzers)}.")
|
|
106
|
+
if uses_serializer:
|
|
107
|
+
output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta."
|
|
108
|
+
if uses_redactor:
|
|
109
|
+
output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta con redaccion de secretos."
|
|
110
|
+
lines.append(output_line)
|
|
111
|
+
elif not lines:
|
|
112
|
+
lines.append("Orquesta modulos Python internos sin un patron de salida claramente inferible.")
|
|
113
|
+
return lines
|
|
114
|
+
|
|
115
|
+
def _summarize_node_entry(self, path: str, content: str) -> list[str]:
|
|
116
|
+
imports = re.findall(r"""from\s+['"](\.?\.?/[^'"]+)['"]|require\(['"](\.?\.?/[^'"]+)['"]\)""", content)
|
|
117
|
+
modules = [item for pair in imports for item in pair if item]
|
|
118
|
+
lines: list[str] = []
|
|
119
|
+
if modules:
|
|
120
|
+
formatted = self._format_module_list([self._module_label(module) for module in modules])
|
|
121
|
+
if formatted:
|
|
122
|
+
lines.append(f"Orquesta modulos internos: {formatted}.")
|
|
123
|
+
lines.append("Produce la salida principal del entry point JavaScript/TypeScript detectado.")
|
|
124
|
+
return lines
|
|
125
|
+
|
|
126
|
+
def _summarize_go_entry(self, path: str, content: str) -> list[str]:
|
|
127
|
+
imports = re.findall(r'"([^"]+)"', content)
|
|
128
|
+
internal = [module for module in imports if not module.startswith(("fmt", "net/", "os", "context"))]
|
|
129
|
+
lines: list[str] = []
|
|
130
|
+
if internal:
|
|
131
|
+
formatted = self._format_module_list([self._module_label(module) for module in internal])
|
|
132
|
+
if formatted:
|
|
133
|
+
lines.append(f"Orquesta paquetes internos: {formatted}.")
|
|
134
|
+
lines.append("Produce la salida principal del binario Go detectado.")
|
|
135
|
+
return lines
|
|
136
|
+
|
|
137
|
+
def _describe_entry_point(self, entry_point: EntryPoint, project_type: str | None) -> str:
|
|
138
|
+
if entry_point.kind == "cli" or entry_point.path.endswith("cli.py"):
|
|
139
|
+
return f"Entry point principal: {entry_point.path} expone la CLI del proyecto."
|
|
140
|
+
if entry_point.kind == "web":
|
|
141
|
+
return f"Entry point principal: {entry_point.path} arranca la interfaz web."
|
|
142
|
+
if project_type == "api" or entry_point.kind == "server":
|
|
143
|
+
return f"Entry point principal: {entry_point.path} arranca el servicio principal."
|
|
144
|
+
if entry_point.kind == "binary":
|
|
145
|
+
return f"Entry point principal: {entry_point.path} arranca el binario principal."
|
|
146
|
+
return f"Entry point principal: {entry_point.path} coordina el flujo principal del proyecto."
|
|
147
|
+
|
|
148
|
+
def _extract_optional_analyzer(self, node: ast.Assign) -> str | None:
|
|
149
|
+
value = node.value
|
|
150
|
+
if not isinstance(value, ast.IfExp):
|
|
151
|
+
return None
|
|
152
|
+
if not isinstance(value.test, ast.Name):
|
|
153
|
+
return None
|
|
154
|
+
if not isinstance(value.body, ast.Call):
|
|
155
|
+
return None
|
|
156
|
+
if not isinstance(value.body.func, ast.Name):
|
|
157
|
+
return None
|
|
158
|
+
analyzer_name = value.body.func.id
|
|
159
|
+
if not analyzer_name.endswith("Analyzer"):
|
|
160
|
+
return None
|
|
161
|
+
return f"{analyzer_name} (--{value.test.id.replace('_', '-')})"
|
|
162
|
+
|
|
163
|
+
def _infer_fallback_entry_points(
|
|
164
|
+
self, file_paths: list[str], stacks: list[StackDetection]
|
|
165
|
+
) -> list[EntryPoint]:
|
|
166
|
+
candidates: list[EntryPoint] = []
|
|
167
|
+
stack_name = stacks[0].stack if stacks else "unknown"
|
|
168
|
+
ordered_paths = sorted(file_paths, key=self._fallback_priority)
|
|
169
|
+
for path in ordered_paths:
|
|
170
|
+
if path.endswith(("cli.py", "__main__.py", "main.py")):
|
|
171
|
+
candidates.append(
|
|
172
|
+
EntryPoint(
|
|
173
|
+
path=path,
|
|
174
|
+
stack=stack_name,
|
|
175
|
+
kind="cli",
|
|
176
|
+
source="convention",
|
|
177
|
+
confidence="medium",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
elif path.endswith(("app/page.tsx", "pages/index.js", "server.js")):
|
|
181
|
+
candidates.append(
|
|
182
|
+
EntryPoint(
|
|
183
|
+
path=path,
|
|
184
|
+
stack=stack_name,
|
|
185
|
+
kind="web" if "page" in path or "pages/" in path else "server",
|
|
186
|
+
source="convention",
|
|
187
|
+
confidence="medium",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
elif path.endswith("main.go"):
|
|
191
|
+
candidates.append(
|
|
192
|
+
EntryPoint(
|
|
193
|
+
path=path,
|
|
194
|
+
stack=stack_name,
|
|
195
|
+
kind="binary",
|
|
196
|
+
source="convention",
|
|
197
|
+
confidence="medium",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
return candidates
|
|
201
|
+
|
|
202
|
+
def _fallback_priority(self, path: str) -> tuple[int, int, str]:
|
|
203
|
+
return (
|
|
204
|
+
0 if "/cli.py" in path or path.endswith("cli.py") else 1,
|
|
205
|
+
0 if path.startswith("src/") else 1,
|
|
206
|
+
path,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def _format_module_list(self, modules: list[str]) -> str:
|
|
210
|
+
normalized = [self._module_label(module) for module in modules]
|
|
211
|
+
filtered = [module for module in normalized if module and not self._is_tooling_path(module)]
|
|
212
|
+
if not filtered:
|
|
213
|
+
return ""
|
|
214
|
+
ordered: list[str] = []
|
|
215
|
+
seen: set[str] = set()
|
|
216
|
+
for module in filtered:
|
|
217
|
+
if module not in seen:
|
|
218
|
+
seen.add(module)
|
|
219
|
+
ordered.append(module)
|
|
220
|
+
return ", ".join(ordered[:8])
|
|
221
|
+
|
|
222
|
+
def _module_label(self, module: str) -> str:
|
|
223
|
+
cleaned = module.strip().strip("./")
|
|
224
|
+
if "/" in cleaned:
|
|
225
|
+
return cleaned.split("/")[-1]
|
|
226
|
+
if "." in cleaned:
|
|
227
|
+
return cleaned.rsplit(".", 1)[-1]
|
|
228
|
+
return cleaned
|
|
229
|
+
|
|
230
|
+
def _python_package_prefix(self, path: str) -> str:
|
|
231
|
+
parts = Path(path).parts
|
|
232
|
+
if len(parts) >= 3 and parts[0] == "src":
|
|
233
|
+
return f"{parts[1]}."
|
|
234
|
+
if len(parts) >= 2:
|
|
235
|
+
return f"{parts[0]}."
|
|
236
|
+
return ""
|
|
237
|
+
|
|
238
|
+
def _read_file(self, relative_path: str) -> str | None:
|
|
239
|
+
try:
|
|
240
|
+
return (self.root / relative_path).read_text(encoding="utf-8", errors="replace")
|
|
241
|
+
except OSError:
|
|
242
|
+
return None
|
|
243
|
+
|
|
244
|
+
def _is_tooling_path(self, path: str | None) -> bool:
|
|
245
|
+
if not path:
|
|
246
|
+
return False
|
|
247
|
+
normalized = path.strip().lstrip("/")
|
|
248
|
+
return normalized.startswith(_TOOLING_PREFIXES)
|
sourcecode/cli.py
CHANGED
|
@@ -474,6 +474,7 @@ def main(
|
|
|
474
474
|
)
|
|
475
475
|
|
|
476
476
|
# Phase 9: LLM Output Quality — poblar campos derivados
|
|
477
|
+
from sourcecode.architecture_summary import ArchitectureSummarizer
|
|
477
478
|
from sourcecode.summarizer import ProjectSummarizer
|
|
478
479
|
from sourcecode.tree_utils import flatten_file_tree
|
|
479
480
|
|
|
@@ -496,7 +497,8 @@ def main(
|
|
|
496
497
|
sm.key_dependencies = sorted(direct_deps, key=_dep_sort_key)[:15]
|
|
497
498
|
|
|
498
499
|
# LQN-02: resumen NL deterministico
|
|
499
|
-
sm.project_summary = ProjectSummarizer().generate(sm)
|
|
500
|
+
sm.project_summary = ProjectSummarizer(target).generate(sm)
|
|
501
|
+
sm.architecture_summary = ArchitectureSummarizer(target).generate(sm)
|
|
500
502
|
|
|
501
503
|
# 4. Serializar (con o sin modo compact)
|
|
502
504
|
if compact:
|
|
@@ -533,3 +535,34 @@ def main(
|
|
|
533
535
|
|
|
534
536
|
# 5. Escribir output (CLI-04)
|
|
535
537
|
write_output(content, output=output)
|
|
538
|
+
|
|
539
|
+
from sourcecode.prepare_context import ContextBuilder
|
|
540
|
+
|
|
541
|
+
@app.command("prepare-context")
|
|
542
|
+
def prepare_context_cmd(
|
|
543
|
+
task: str = typer.Argument(..., help="Descripción de la tarea"),
|
|
544
|
+
path: Path = typer.Option(".", help="Directorio del proyecto"),
|
|
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)
|
|
554
|
+
|
|
555
|
+
builder = ContextBuilder(target)
|
|
556
|
+
result = builder.prepare(task)
|
|
557
|
+
|
|
558
|
+
output = {
|
|
559
|
+
"task": result.task,
|
|
560
|
+
"entry_points": result.entry_points,
|
|
561
|
+
"relevant_files": [f.__dict__ for f in result.relevant_files],
|
|
562
|
+
"call_flow": result.call_flow,
|
|
563
|
+
"snippets": [s.__dict__ for s in result.snippets],
|
|
564
|
+
"tests": result.tests,
|
|
565
|
+
"notes": result.notes,
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
typer.echo(json.dumps(output, indent=2, ensure_ascii=False))
|
sourcecode/detectors/go.py
CHANGED
|
@@ -39,7 +39,13 @@ class GoDetector(AbstractDetector):
|
|
|
39
39
|
]
|
|
40
40
|
preferred = [path for path in entry_candidates if path.startswith("cmd/")] or entry_candidates
|
|
41
41
|
entry_points = [
|
|
42
|
-
EntryPoint(
|
|
42
|
+
EntryPoint(
|
|
43
|
+
path=path,
|
|
44
|
+
stack="go",
|
|
45
|
+
kind="binary",
|
|
46
|
+
source="convention",
|
|
47
|
+
confidence="medium",
|
|
48
|
+
)
|
|
43
49
|
for path in unique_strings(preferred)
|
|
44
50
|
]
|
|
45
51
|
stack = StackDetection(
|
sourcecode/detectors/nodejs.py
CHANGED
|
@@ -77,20 +77,33 @@ class NodejsDetector(AbstractDetector):
|
|
|
77
77
|
def _collect_entry_points(
|
|
78
78
|
self, context: DetectionContext, package_json: dict[str, Any]
|
|
79
79
|
) -> list[EntryPoint]:
|
|
80
|
-
|
|
80
|
+
entry_points: list[EntryPoint] = []
|
|
81
|
+
seen: set[str] = set()
|
|
81
82
|
main = package_json.get("main")
|
|
82
83
|
if isinstance(main, str) and main.strip():
|
|
83
|
-
|
|
84
|
+
path = main.strip()
|
|
85
|
+
if path_exists_in_tree(context.file_tree, path):
|
|
86
|
+
seen.add(path)
|
|
87
|
+
entry_points.append(
|
|
88
|
+
EntryPoint(
|
|
89
|
+
path=path,
|
|
90
|
+
stack="nodejs",
|
|
91
|
+
kind="server",
|
|
92
|
+
source="package.json",
|
|
93
|
+
confidence="high",
|
|
94
|
+
)
|
|
95
|
+
)
|
|
84
96
|
|
|
97
|
+
convention_candidates: list[str] = []
|
|
85
98
|
bin_field = package_json.get("bin")
|
|
86
99
|
if isinstance(bin_field, str) and bin_field.strip():
|
|
87
|
-
|
|
100
|
+
convention_candidates.append(bin_field.strip())
|
|
88
101
|
elif isinstance(bin_field, dict):
|
|
89
|
-
|
|
102
|
+
convention_candidates.extend(
|
|
90
103
|
str(value).strip() for value in bin_field.values() if isinstance(value, str) and value.strip()
|
|
91
104
|
)
|
|
92
105
|
|
|
93
|
-
|
|
106
|
+
convention_candidates.extend(
|
|
94
107
|
[
|
|
95
108
|
"server.js",
|
|
96
109
|
"src/index.js",
|
|
@@ -103,11 +116,17 @@ class NodejsDetector(AbstractDetector):
|
|
|
103
116
|
]
|
|
104
117
|
)
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
119
|
+
for path in unique_strings(convention_candidates):
|
|
120
|
+
if path in seen or not path_exists_in_tree(context.file_tree, path):
|
|
121
|
+
continue
|
|
122
|
+
kind = "web" if path.startswith(("app/", "pages/")) else "server"
|
|
123
|
+
entry_points.append(
|
|
124
|
+
EntryPoint(
|
|
125
|
+
path=path,
|
|
126
|
+
stack="nodejs",
|
|
127
|
+
kind=kind,
|
|
128
|
+
source="convention",
|
|
129
|
+
confidence="medium",
|
|
112
130
|
)
|
|
131
|
+
)
|
|
113
132
|
return entry_points
|
sourcecode/detectors/python.py
CHANGED
|
@@ -132,7 +132,7 @@ class PythonDetector(AbstractDetector):
|
|
|
132
132
|
return {match.lower() for match in matches}
|
|
133
133
|
|
|
134
134
|
def _collect_entry_points(self, context: DetectionContext) -> list[EntryPoint]:
|
|
135
|
-
|
|
135
|
+
declared_candidates: list[str] = []
|
|
136
136
|
pyproject = load_toml_file(context.root / "pyproject.toml")
|
|
137
137
|
if pyproject:
|
|
138
138
|
project = pyproject.get("project", {})
|
|
@@ -142,16 +142,38 @@ class PythonDetector(AbstractDetector):
|
|
|
142
142
|
for value in scripts.values():
|
|
143
143
|
if isinstance(value, str) and ":" in value:
|
|
144
144
|
module, _callable = value.split(":", 1)
|
|
145
|
-
|
|
145
|
+
declared_candidates.append(module.replace(".", "/") + ".py")
|
|
146
146
|
|
|
147
|
-
candidates.extend(["__main__.py", "main.py", "app.py", "manage.py", "src/main.py"])
|
|
148
147
|
entry_points: list[EntryPoint] = []
|
|
149
|
-
|
|
148
|
+
declared = set()
|
|
149
|
+
for path in unique_strings(declared_candidates):
|
|
150
150
|
if path_exists_in_tree(context.file_tree, path):
|
|
151
|
-
|
|
151
|
+
declared.add(path)
|
|
152
|
+
kind = "cli" if path.endswith(("__main__.py", "main.py", "cli.py")) else "app"
|
|
152
153
|
entry_points.append(
|
|
153
|
-
EntryPoint(
|
|
154
|
+
EntryPoint(
|
|
155
|
+
path=path,
|
|
156
|
+
stack="python",
|
|
157
|
+
kind=kind,
|
|
158
|
+
source="pyproject.toml",
|
|
159
|
+
confidence="high",
|
|
160
|
+
)
|
|
154
161
|
)
|
|
162
|
+
|
|
163
|
+
convention_candidates = ["cli.py", "__main__.py", "main.py", "app.py", "manage.py", "src/main.py"]
|
|
164
|
+
for path in unique_strings(convention_candidates):
|
|
165
|
+
if path in declared or not path_exists_in_tree(context.file_tree, path):
|
|
166
|
+
continue
|
|
167
|
+
kind = "cli" if path.endswith(("__main__.py", "main.py", "cli.py")) else "app"
|
|
168
|
+
entry_points.append(
|
|
169
|
+
EntryPoint(
|
|
170
|
+
path=path,
|
|
171
|
+
stack="python",
|
|
172
|
+
kind=kind,
|
|
173
|
+
source="convention",
|
|
174
|
+
confidence="medium",
|
|
175
|
+
)
|
|
176
|
+
)
|
|
155
177
|
return entry_points
|
|
156
178
|
|
|
157
179
|
def _detect_package_manager(
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any, Dict, List, Optional
|
|
6
|
+
|
|
7
|
+
from sourcecode.semantic_analyzer import SemanticAnalyzer
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# =========================
|
|
11
|
+
# Output schema (simple)
|
|
12
|
+
# =========================
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class ContextSnippet:
|
|
16
|
+
file: str
|
|
17
|
+
symbol: str
|
|
18
|
+
code: str
|
|
19
|
+
reason: str
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ContextFile:
|
|
24
|
+
path: str
|
|
25
|
+
role: str
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ContextResult:
|
|
30
|
+
task: str
|
|
31
|
+
entry_points: List[Dict[str, str]]
|
|
32
|
+
relevant_files: List[ContextFile]
|
|
33
|
+
call_flow: List[Dict[str, Any]]
|
|
34
|
+
snippets: List[ContextSnippet]
|
|
35
|
+
tests: List[Dict[str, str]]
|
|
36
|
+
notes: List[str]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# =========================
|
|
40
|
+
# Core Builder
|
|
41
|
+
# =========================
|
|
42
|
+
|
|
43
|
+
class ContextBuilder:
|
|
44
|
+
def __init__(self, root: Path) -> None:
|
|
45
|
+
self.root = root
|
|
46
|
+
self.analyzer = SemanticAnalyzer(root)
|
|
47
|
+
|
|
48
|
+
# -------------------------
|
|
49
|
+
# Public API
|
|
50
|
+
# -------------------------
|
|
51
|
+
|
|
52
|
+
def prepare(self, task: str) -> ContextResult:
|
|
53
|
+
"""
|
|
54
|
+
Entry point principal.
|
|
55
|
+
"""
|
|
56
|
+
semantic = self.analyzer.analyze()
|
|
57
|
+
|
|
58
|
+
entry_points = self._detect_entrypoints(semantic)
|
|
59
|
+
relevant_files = self._select_relevant_files(task, entry_points)
|
|
60
|
+
snippets = self._extract_snippets(relevant_files)
|
|
61
|
+
tests = self._find_related_tests(relevant_files)
|
|
62
|
+
call_flow = self._build_call_flow(entry_points)
|
|
63
|
+
|
|
64
|
+
notes = self._generate_notes(task)
|
|
65
|
+
|
|
66
|
+
return ContextResult(
|
|
67
|
+
task=task,
|
|
68
|
+
entry_points=entry_points,
|
|
69
|
+
relevant_files=relevant_files,
|
|
70
|
+
call_flow=call_flow,
|
|
71
|
+
snippets=snippets,
|
|
72
|
+
tests=tests,
|
|
73
|
+
notes=notes,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# -------------------------
|
|
77
|
+
# Heurísticas básicas
|
|
78
|
+
# -------------------------
|
|
79
|
+
|
|
80
|
+
def _detect_entrypoints(self, semantic: Any) -> List[Dict[str, str]]:
|
|
81
|
+
"""
|
|
82
|
+
MVP: detecta cli.py o main functions
|
|
83
|
+
"""
|
|
84
|
+
entrypoints: List[Dict[str, str]] = []
|
|
85
|
+
|
|
86
|
+
for path in semantic.file_paths:
|
|
87
|
+
if path.endswith("cli.py") or path.endswith("__main__.py"):
|
|
88
|
+
entrypoints.append({
|
|
89
|
+
"file": path,
|
|
90
|
+
"symbol": "main",
|
|
91
|
+
"reason": "CLI entrypoint"
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
return entrypoints
|
|
95
|
+
|
|
96
|
+
def _select_relevant_files(
|
|
97
|
+
self,
|
|
98
|
+
task: str,
|
|
99
|
+
entry_points: List[Dict[str, str]],
|
|
100
|
+
) -> List[ContextFile]:
|
|
101
|
+
"""
|
|
102
|
+
Heurística simple:
|
|
103
|
+
- incluir entrypoints
|
|
104
|
+
- incluir archivos con palabras clave del task
|
|
105
|
+
"""
|
|
106
|
+
files: List[ContextFile] = []
|
|
107
|
+
|
|
108
|
+
for ep in entry_points:
|
|
109
|
+
files.append(ContextFile(path=ep["file"], role="entrypoint"))
|
|
110
|
+
|
|
111
|
+
keywords = task.lower().split()
|
|
112
|
+
|
|
113
|
+
for path in self.analyzer.scan.file_paths: # fallback simple
|
|
114
|
+
if any(k in path.lower() for k in keywords):
|
|
115
|
+
files.append(ContextFile(path=path, role="matched"))
|
|
116
|
+
|
|
117
|
+
# deduplicar
|
|
118
|
+
seen = set()
|
|
119
|
+
unique = []
|
|
120
|
+
for f in files:
|
|
121
|
+
if f.path not in seen:
|
|
122
|
+
seen.add(f.path)
|
|
123
|
+
unique.append(f)
|
|
124
|
+
|
|
125
|
+
return unique[:8]
|
|
126
|
+
|
|
127
|
+
def _extract_snippets(self, files: List[ContextFile]) -> List[ContextSnippet]:
|
|
128
|
+
"""
|
|
129
|
+
Extrae primeras ~30 líneas como snippet simple.
|
|
130
|
+
"""
|
|
131
|
+
snippets: List[ContextSnippet] = []
|
|
132
|
+
|
|
133
|
+
for f in files:
|
|
134
|
+
abs_path = self.root / f.path
|
|
135
|
+
if not abs_path.exists():
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
content = abs_path.read_text(encoding="utf-8", errors="ignore")
|
|
140
|
+
except Exception:
|
|
141
|
+
continue
|
|
142
|
+
|
|
143
|
+
snippet = "\n".join(content.splitlines()[:30])
|
|
144
|
+
|
|
145
|
+
snippets.append(ContextSnippet(
|
|
146
|
+
file=f.path,
|
|
147
|
+
symbol="module",
|
|
148
|
+
code=snippet,
|
|
149
|
+
reason=f"contenido inicial de {f.role}"
|
|
150
|
+
))
|
|
151
|
+
|
|
152
|
+
return snippets[:12]
|
|
153
|
+
|
|
154
|
+
def _find_related_tests(self, files: List[ContextFile]) -> List[Dict[str, str]]:
|
|
155
|
+
"""
|
|
156
|
+
Busca tests por naming convention.
|
|
157
|
+
"""
|
|
158
|
+
tests: List[Dict[str, str]] = []
|
|
159
|
+
|
|
160
|
+
for f in files:
|
|
161
|
+
name = Path(f.path).name
|
|
162
|
+
test_name = f"test_{name}"
|
|
163
|
+
|
|
164
|
+
test_path = self.root / "tests" / test_name
|
|
165
|
+
if test_path.exists():
|
|
166
|
+
tests.append({
|
|
167
|
+
"file": f"tests/{test_name}",
|
|
168
|
+
"reason": "posible test relacionado"
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
return tests
|
|
172
|
+
|
|
173
|
+
def _build_call_flow(self, entry_points: List[Dict[str, str]]) -> List[Dict[str, Any]]:
|
|
174
|
+
"""
|
|
175
|
+
MVP: placeholder (hasta que uses call graph real)
|
|
176
|
+
"""
|
|
177
|
+
flow: List[Dict[str, Any]] = []
|
|
178
|
+
|
|
179
|
+
for ep in entry_points:
|
|
180
|
+
flow.append({
|
|
181
|
+
"from": ep["symbol"],
|
|
182
|
+
"to": ["unknown"]
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
return flow
|
|
186
|
+
|
|
187
|
+
def _generate_notes(self, task: str) -> List[str]:
|
|
188
|
+
notes: List[str] = []
|
|
189
|
+
|
|
190
|
+
if "flag" in task or "--" in task:
|
|
191
|
+
notes.append("probablemente necesitas modificar argumentos CLI")
|
|
192
|
+
if "test" in task:
|
|
193
|
+
notes.append("asegúrate de actualizar tests relacionados")
|
|
194
|
+
if "refactor" in task:
|
|
195
|
+
notes.append("mantener compatibilidad hacia atrás")
|
|
196
|
+
|
|
197
|
+
return notes
|
sourcecode/schema.py
CHANGED
|
@@ -66,6 +66,7 @@ class EntryPoint:
|
|
|
66
66
|
stack: str
|
|
67
67
|
kind: str = "entry"
|
|
68
68
|
source: str = "manifest"
|
|
69
|
+
confidence: Literal["high", "medium", "low"] = "high"
|
|
69
70
|
|
|
70
71
|
|
|
71
72
|
@dataclass
|
|
@@ -322,6 +323,7 @@ class SourceMap:
|
|
|
322
323
|
# Phase 9: LLM Output Quality
|
|
323
324
|
file_paths: list[str] = field(default_factory=list)
|
|
324
325
|
project_summary: Optional[str] = None
|
|
326
|
+
architecture_summary: Optional[str] = None
|
|
325
327
|
key_dependencies: list[DependencyRecord] = field(default_factory=list)
|
|
326
328
|
# Phase 10: Code Quality Metrics
|
|
327
329
|
file_metrics: list[FileMetrics] = field(default_factory=list)
|
sourcecode/serializer.py
CHANGED
|
@@ -52,7 +52,7 @@ def compact_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
52
52
|
"""Proyeccion compacta del SourceMap (~500-700 tokens).
|
|
53
53
|
|
|
54
54
|
Incluye: schema_version, project_type, stacks, entry_points,
|
|
55
|
-
project_summary (siempre),
|
|
55
|
+
project_summary (siempre), architecture_summary (siempre),
|
|
56
56
|
dependency_summary (cuando requested=True),
|
|
57
57
|
file_tree_depth1 (backward compat).
|
|
58
58
|
|
|
@@ -73,9 +73,9 @@ def compact_view(sm: SourceMap) -> dict[str, Any]:
|
|
|
73
73
|
"schema_version": sm.metadata.schema_version,
|
|
74
74
|
"project_type": sm.project_type,
|
|
75
75
|
"project_summary": sm.project_summary,
|
|
76
|
+
"architecture_summary": sm.architecture_summary,
|
|
76
77
|
"stacks": [asdict(stack) for stack in sm.stacks],
|
|
77
78
|
"entry_points": [asdict(entry_point) for entry_point in sm.entry_points],
|
|
78
|
-
"file_paths": sm.file_paths,
|
|
79
79
|
"file_tree_depth1": depth1,
|
|
80
80
|
"dependency_summary": dep_summary_dict,
|
|
81
81
|
}
|
sourcecode/summarizer.py
CHANGED
|
@@ -4,12 +4,20 @@ Sin llamadas a API — solo templates aplicados sobre SourceMap.
|
|
|
4
4
|
"""
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from sourcecode.detectors.parsers import load_json_file, load_toml_file
|
|
7
10
|
from sourcecode.schema import SourceMap
|
|
8
11
|
|
|
12
|
+
_TOOLING_PREFIXES = (".claude/", ".vscode/", "bin/")
|
|
13
|
+
|
|
9
14
|
|
|
10
15
|
class ProjectSummarizer:
|
|
11
16
|
"""Genera project_summary: string NL deterministica desde SourceMap."""
|
|
12
17
|
|
|
18
|
+
def __init__(self, root: Path | None = None) -> None:
|
|
19
|
+
self.root = root
|
|
20
|
+
|
|
13
21
|
def generate(self, sm: SourceMap) -> str:
|
|
14
22
|
"""Retorna descripcion NL del proyecto. Nunca lanza excepcion."""
|
|
15
23
|
try:
|
|
@@ -18,9 +26,14 @@ class ProjectSummarizer:
|
|
|
18
26
|
return "Proyecto analizado."
|
|
19
27
|
|
|
20
28
|
def _build_summary(self, sm: SourceMap) -> str:
|
|
29
|
+
description = self._read_project_description()
|
|
30
|
+
if description:
|
|
31
|
+
return self._merge_description_with_structure(description, sm)
|
|
32
|
+
|
|
21
33
|
# Determinar stack primario
|
|
22
|
-
|
|
23
|
-
|
|
34
|
+
non_tooling_stacks = self._filter_non_tooling_stacks(sm)
|
|
35
|
+
primary_stacks = [s for s in non_tooling_stacks if s.primary]
|
|
36
|
+
all_stacks = primary_stacks if primary_stacks else non_tooling_stacks
|
|
24
37
|
|
|
25
38
|
if not all_stacks:
|
|
26
39
|
return "Proyecto sin stack detectado."
|
|
@@ -44,7 +57,7 @@ class ProjectSummarizer:
|
|
|
44
57
|
fw_part = f" ({', '.join(frameworks[:3])})" if frameworks else ""
|
|
45
58
|
|
|
46
59
|
# Entry points (max 3)
|
|
47
|
-
ep_paths = [ep.path for ep in sm.entry_points[:3]
|
|
60
|
+
ep_paths = [ep.path for ep in sm.entry_points if not self._is_tooling_path(ep.path)][:3]
|
|
48
61
|
ep_part = f" Entry points: {', '.join(ep_paths)}." if ep_paths else ""
|
|
49
62
|
|
|
50
63
|
# Dependencias
|
|
@@ -60,9 +73,105 @@ class ProjectSummarizer:
|
|
|
60
73
|
|
|
61
74
|
# Monorepo: variante especial
|
|
62
75
|
if project_type == "monorepo":
|
|
63
|
-
stacks_desc = ", ".join(sorted({s.stack.capitalize() for s in
|
|
64
|
-
n_ws = len({s.workspace for s in
|
|
76
|
+
stacks_desc = ", ".join(sorted({s.stack.capitalize() for s in non_tooling_stacks}))
|
|
77
|
+
n_ws = len({s.workspace for s in non_tooling_stacks if s.workspace})
|
|
65
78
|
ws_part = f" con {n_ws} workspaces" if n_ws > 0 else ""
|
|
66
79
|
return f"Monorepo{ws_part} en {stacks_desc}.{ep_part}{dep_part}"
|
|
67
80
|
|
|
68
81
|
return f"{type_label} en {stack_name}{fw_part}.{ep_part}{dep_part}"
|
|
82
|
+
|
|
83
|
+
def _read_project_description(self) -> str | None:
|
|
84
|
+
if self.root is None:
|
|
85
|
+
return None
|
|
86
|
+
pyproject = load_toml_file(self.root / "pyproject.toml")
|
|
87
|
+
if pyproject:
|
|
88
|
+
project = pyproject.get("project", {})
|
|
89
|
+
if isinstance(project, dict):
|
|
90
|
+
description = project.get("description")
|
|
91
|
+
if isinstance(description, str) and description.strip():
|
|
92
|
+
return description.strip()
|
|
93
|
+
|
|
94
|
+
package_json = load_json_file(self.root / "package.json")
|
|
95
|
+
if package_json:
|
|
96
|
+
description = package_json.get("description")
|
|
97
|
+
if isinstance(description, str) and description.strip():
|
|
98
|
+
return description.strip()
|
|
99
|
+
|
|
100
|
+
return self._read_readme_paragraph()
|
|
101
|
+
|
|
102
|
+
def _read_readme_paragraph(self) -> str | None:
|
|
103
|
+
if self.root is None:
|
|
104
|
+
return None
|
|
105
|
+
for name in ("README.md", "README.rst", "README.txt"):
|
|
106
|
+
path = self.root / name
|
|
107
|
+
try:
|
|
108
|
+
content = path.read_text(encoding="utf-8", errors="replace")
|
|
109
|
+
except OSError:
|
|
110
|
+
continue
|
|
111
|
+
paragraph = self._extract_first_useful_paragraph(content)
|
|
112
|
+
if paragraph:
|
|
113
|
+
return paragraph
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def _extract_first_useful_paragraph(self, content: str) -> str | None:
|
|
117
|
+
lines: list[str] = []
|
|
118
|
+
in_code_block = False
|
|
119
|
+
for raw_line in content.splitlines():
|
|
120
|
+
line = raw_line.strip()
|
|
121
|
+
if line.startswith("```"):
|
|
122
|
+
in_code_block = not in_code_block
|
|
123
|
+
continue
|
|
124
|
+
if in_code_block or not line or line.startswith(("#", "<!--")):
|
|
125
|
+
if lines:
|
|
126
|
+
break
|
|
127
|
+
continue
|
|
128
|
+
lines.append(line)
|
|
129
|
+
if not lines:
|
|
130
|
+
return None
|
|
131
|
+
return " ".join(lines).strip()
|
|
132
|
+
|
|
133
|
+
def _merge_description_with_structure(self, description: str, sm: SourceMap) -> str:
|
|
134
|
+
parts = [description.rstrip(".")]
|
|
135
|
+
entry_points = [ep.path for ep in sm.entry_points if not self._is_tooling_path(ep.path)][:2]
|
|
136
|
+
if entry_points:
|
|
137
|
+
parts.append(f"Entry points: {', '.join(entry_points)}")
|
|
138
|
+
|
|
139
|
+
non_tooling_stacks = self._filter_non_tooling_stacks(sm)
|
|
140
|
+
if non_tooling_stacks:
|
|
141
|
+
primary = self._select_summary_primary_stack(non_tooling_stacks)
|
|
142
|
+
frameworks = [framework.name for framework in primary.frameworks[:2]]
|
|
143
|
+
stack_label = primary.stack.capitalize()
|
|
144
|
+
if frameworks:
|
|
145
|
+
parts.append(f"Stack principal: {stack_label} ({', '.join(frameworks)})")
|
|
146
|
+
else:
|
|
147
|
+
parts.append(f"Stack principal: {stack_label}")
|
|
148
|
+
|
|
149
|
+
return ". ".join(parts) + "."
|
|
150
|
+
|
|
151
|
+
def _filter_non_tooling_stacks(self, sm: SourceMap) -> list:
|
|
152
|
+
filtered = [
|
|
153
|
+
stack for stack in sm.stacks
|
|
154
|
+
if not self._is_tooling_path(stack.root) and not self._is_tooling_path(stack.workspace)
|
|
155
|
+
]
|
|
156
|
+
return filtered or sm.stacks
|
|
157
|
+
|
|
158
|
+
def _select_summary_primary_stack(self, stacks: list) -> Any:
|
|
159
|
+
def score(stack: Any) -> tuple[int, int, int, int]:
|
|
160
|
+
root_manifest_hits = 0
|
|
161
|
+
if self.root is not None:
|
|
162
|
+
root_manifest_hits = sum(
|
|
163
|
+
1 for manifest in stack.manifests
|
|
164
|
+
if (self.root / manifest).is_file()
|
|
165
|
+
)
|
|
166
|
+
framework_hits = len(stack.frameworks)
|
|
167
|
+
primary_hint = 1 if stack.primary else 0
|
|
168
|
+
confidence = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
|
|
169
|
+
return (root_manifest_hits, framework_hits, primary_hint, confidence)
|
|
170
|
+
|
|
171
|
+
return max(stacks, key=score)
|
|
172
|
+
|
|
173
|
+
def _is_tooling_path(self, path: str | None) -> bool:
|
|
174
|
+
if not path:
|
|
175
|
+
return False
|
|
176
|
+
normalized = path.strip().lstrip("/")
|
|
177
|
+
return normalized.startswith(_TOOLING_PREFIXES)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.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
|
|
@@ -94,7 +94,7 @@ sourcecode --version
|
|
|
94
94
|
| `PATH` | `.` | Directory to analyze. |
|
|
95
95
|
| `--format json\|yaml` | `json` | Output format. |
|
|
96
96
|
| `--output PATH` | stdout | Write to a file instead of stdout. |
|
|
97
|
-
| `--compact` | off | Reduced output (~500-700 tokens): `schema_version`, `project_type`, `project_summary`, `
|
|
97
|
+
| `--compact` | off | Reduced output (~500-700 tokens): `schema_version`, `project_type`, `project_summary`, `architecture_summary`, `stacks`, `entry_points`, `file_tree_depth1`, and `dependency_summary` when available. |
|
|
98
98
|
| `--dependencies` | off | Include direct dependencies, resolved versions, and transitive relationships when lockfiles make that possible. Also populates `key_dependencies`. |
|
|
99
99
|
| `--graph-modules` | off | Include a structural module graph with imports and simple relations. |
|
|
100
100
|
| `--graph-detail high\|medium\|full` | `high` | Graph detail level: summarized (high), balanced (medium), or full-fidelity (full). |
|
|
@@ -118,6 +118,7 @@ The full schema (`SourceMap`) includes the following fields:
|
|
|
118
118
|
| `file_tree` | object | Repository tree where `null` represents a file and `{}` represents a directory. |
|
|
119
119
|
| `file_paths` | array | Flat list of all project paths derived from `file_tree`, with forward-slash separators. Always present; respects `--depth`. |
|
|
120
120
|
| `project_summary` | string\|null | Deterministic natural-language description of the project generated from detected stacks, entry points, and dependencies. Present when stacks are detected. |
|
|
121
|
+
| `architecture_summary` | string\|null | Static summary of the main execution flow, orchestrated modules, and output produced by the project. Present when enough structural evidence is available. |
|
|
121
122
|
| `stacks` | array | Stack detections with confidence, frameworks, manifests, `primary`, `root`, `workspace`, and `signals`. |
|
|
122
123
|
| `project_type` | string\|null | Overall project classification. |
|
|
123
124
|
| `entry_points` | array | Detected entry points by stack. |
|
|
@@ -155,6 +156,7 @@ Real output from a Python FastAPI project:
|
|
|
155
156
|
"schema_version": "1.0",
|
|
156
157
|
"project_type": "api",
|
|
157
158
|
"project_summary": "API en Python (FastAPI). Entry points: src/main.py. 12 dependencias (python).",
|
|
159
|
+
"architecture_summary": null,
|
|
158
160
|
"stacks": [
|
|
159
161
|
{
|
|
160
162
|
"stack": "python",
|
|
@@ -179,7 +181,6 @@ Real output from a Python FastAPI project:
|
|
|
179
181
|
"source": "manifest"
|
|
180
182
|
}
|
|
181
183
|
],
|
|
182
|
-
"file_paths": ["pyproject.toml", "src/main.py", "src/routes.py", "tests/test_main.py"],
|
|
183
184
|
"file_tree_depth1": {
|
|
184
185
|
"pyproject.toml": null,
|
|
185
186
|
"src": {},
|
|
@@ -371,7 +372,7 @@ Different modes optimize for different tradeoffs between context size and depth
|
|
|
371
372
|
|
|
372
373
|
Best for: initial orientation, deciding what to explore next, fast handoffs between agents.
|
|
373
374
|
|
|
374
|
-
Includes: `project_summary` (instant project description), `
|
|
375
|
+
Includes: `project_summary` (instant project description), `architecture_summary` (execution-oriented static summary), `stacks`, `entry_points`, `file_tree_depth1`, and `dependency_summary` when `--dependencies` was also requested.
|
|
375
376
|
|
|
376
377
|
```bash
|
|
377
378
|
sourcecode --compact .
|
|
@@ -404,9 +405,9 @@ sourcecode --docs --docs-depth full . # include methods
|
|
|
404
405
|
- `"Aplicacion web en Node.js (Next.js, React). Entry points: app/page.tsx."`
|
|
405
406
|
- `"Monorepo con 2 workspaces en Node.js, Python."`
|
|
406
407
|
|
|
407
|
-
**`
|
|
408
|
+
**`architecture_summary` field**
|
|
408
409
|
|
|
409
|
-
`
|
|
410
|
+
`architecture_summary` is a static 3-5 line summary oriented to execution flow. It answers what the main entry point does, which modules it orchestrates, and what the project produces. In compact mode it replaces the low-signal value that `file_paths` used to occupy.
|
|
410
411
|
|
|
411
412
|
**`key_dependencies` field**
|
|
412
413
|
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=R1kLSVb6MQgOcPPp7JrOi_v8YHooGoXpqDAgs0Nn_7E,100
|
|
2
|
+
sourcecode/architecture_summary.py,sha256=6FCTEHmbESxx4lmc4A61x5DPvI57oJAUnaJIZ-GjRkc,10371
|
|
2
3
|
sourcecode/classifier.py,sha256=7ubGTn5vfw9z-9eosLIYDtQUu7d8dvB4XKKDxysUVSM,6910
|
|
3
|
-
sourcecode/cli.py,sha256=
|
|
4
|
+
sourcecode/cli.py,sha256=_05FuIgLWiVDqq2OUbgTeX6os3vUeLDvQZpbfJw8FzE,21217
|
|
4
5
|
sourcecode/coverage_parser.py,sha256=0IfE-0BNF1NxTjRnAuHnn1U0VzcB_aGBQMITR6Qsy3E,19701
|
|
5
6
|
sourcecode/dependency_analyzer.py,sha256=c8tSbUpug-2avKNWCP4-_e2_uhBwWl96LwL1Be8O90M,39067
|
|
6
7
|
sourcecode/doc_analyzer.py,sha256=nnTOsO1tfZvaDPy5PKjRLlg6EQZ-2ONRbXlxDeogH8o,19891
|
|
7
8
|
sourcecode/graph_analyzer.py,sha256=S5z8mrGfLwN26Mt2IaCR4dOpQ_APU16cfihSp640eGQ,45241
|
|
8
9
|
sourcecode/metrics_analyzer.py,sha256=zQrIPKDrGIUq4iptXbckflOj2V9897e_6vGZKEwa4b4,20124
|
|
10
|
+
sourcecode/prepare_context.py,sha256=X43dEPe2cb7uMwlpQwVJMe0rQIZ0juaJ7H3F37sPbNc,5371
|
|
9
11
|
sourcecode/redactor.py,sha256=abSf5jH_IzYzpjF3lEN6hCwdXxvmyHs-3DHz9fd8Mic,2883
|
|
10
12
|
sourcecode/scanner.py,sha256=TvcuD77m_NF8OlTj3XH1b_jO0qrto8c90fND96ecqb0,6182
|
|
11
|
-
sourcecode/schema.py,sha256=
|
|
13
|
+
sourcecode/schema.py,sha256=PAXaAGhJOseyjSYi24vwqOQr52ugjFfMuTe-pP6lnXs,10217
|
|
12
14
|
sourcecode/semantic_analyzer.py,sha256=8loG-y_4fN7wUKmGhy9z1yAfn-xV-6yjYklFr3wxCW8,79347
|
|
13
|
-
sourcecode/serializer.py,sha256=
|
|
14
|
-
sourcecode/summarizer.py,sha256=
|
|
15
|
+
sourcecode/serializer.py,sha256=5_hzf-imtFgYRGjJ_ZMXuK76ZnDbgJ_RGYgVlewhMz0,3304
|
|
16
|
+
sourcecode/summarizer.py,sha256=wCtD4sQ3b81qd7mnqiODJJRgeSk_AMchkaXuxJ_WRd8,7011
|
|
15
17
|
sourcecode/tree_utils.py,sha256=5WHQc2L-AGqxKFfYYcQftE6hETxgUxP9MWDuaThcAUw,991
|
|
16
18
|
sourcecode/workspace.py,sha256=1L4xH38gU89fLeMYSuF3ft7lSdCyTfMfJ9NQt5q4Ric,6904
|
|
17
19
|
sourcecode/detectors/__init__.py,sha256=A0AACJFF6HWf_RgatNtWu3PUzstcKtIGM9f1PoFcJug,1987
|
|
@@ -19,21 +21,21 @@ sourcecode/detectors/base.py,sha256=mLqDNil8VkFRNbzwO2wPbmw_aaVg3WR_WzzRjU58xbU,
|
|
|
19
21
|
sourcecode/detectors/dart.py,sha256=77KDgBY-xkbzLjs0Emw6c0NuQY-tQZR5PCHa9r0t2OE,1476
|
|
20
22
|
sourcecode/detectors/dotnet.py,sha256=BofpMPVOJmiUpCFpkd0y7zSzzBj4jeXZkZR5AksBg8w,2017
|
|
21
23
|
sourcecode/detectors/elixir.py,sha256=Aurq99BvJvZ1k9lNBiI-RGOJzi8e7maQd225IG128PE,1759
|
|
22
|
-
sourcecode/detectors/go.py,sha256=
|
|
24
|
+
sourcecode/detectors/go.py,sha256=UAOmAX3YbpwuXSw2UBNYYDR6Mj5zJeLYK3vyNU9yb5I,1868
|
|
23
25
|
sourcecode/detectors/heuristic.py,sha256=Phimturkvi5ZwNdt005TizYTrllZk1pqHpfXOkFZJbc,1943
|
|
24
26
|
sourcecode/detectors/java.py,sha256=XdONBlcBb0Vo-2hvG77Fw00X41d2KlHFCPXJCuzwAl0,3547
|
|
25
27
|
sourcecode/detectors/jvm_ext.py,sha256=ViHKwUzlKXjpjNNB6_wK0XptR29VUsdLobW2U-mS5BM,2665
|
|
26
|
-
sourcecode/detectors/nodejs.py,sha256=
|
|
28
|
+
sourcecode/detectors/nodejs.py,sha256=Epkz8vHdTscjQzCCZHuhOehMu0MrSPNXzfrQU3fHkMA,4535
|
|
27
29
|
sourcecode/detectors/parsers.py,sha256=CQhhFGk8lSXruIfLe571-1HOId3W0ssnY2ioHMGtOrg,2338
|
|
28
30
|
sourcecode/detectors/php.py,sha256=6l5pFlUuKrrC6XlWPSgYTEnnPm3hoFxMM4DRAnjL4nM,1920
|
|
29
31
|
sourcecode/detectors/project.py,sha256=QOTt2p55Wt6FNUSo0_1P7iojM_CsVevvQqHQmLqVxYk,6022
|
|
30
|
-
sourcecode/detectors/python.py,sha256=
|
|
32
|
+
sourcecode/detectors/python.py,sha256=cib7EmEpQSF8X1sI3oAaGxHqP1rg2g0RkmJIuR8VHsM,7714
|
|
31
33
|
sourcecode/detectors/ruby.py,sha256=3_Hd5YqlErpTtyTRcUTPnMp1TF92hT-TnYYeclLbF1c,1625
|
|
32
34
|
sourcecode/detectors/rust.py,sha256=iUz9lE4HfBy8CBaDwGhfh6kLsNIovrH2ay2v3y6qNDc,2298
|
|
33
35
|
sourcecode/detectors/systems.py,sha256=qf5M9tio1H6XDorSstTWIxKe0xVLR_QGTJpFGyhDz4A,1690
|
|
34
36
|
sourcecode/detectors/terraform.py,sha256=5EOKQtTViWo7u3tFK0h45YD8GQPwhrww0cOicxGPPn4,1732
|
|
35
37
|
sourcecode/detectors/tooling.py,sha256=wPvudLCJZ8_cr9GIx44C8jkLAlEN9uTUZcJsP7eVYnQ,1937
|
|
36
|
-
sourcecode-0.
|
|
37
|
-
sourcecode-0.
|
|
38
|
-
sourcecode-0.
|
|
39
|
-
sourcecode-0.
|
|
38
|
+
sourcecode-0.11.0.dist-info/METADATA,sha256=6jKGk6co2EKF_2PuO3mg559EeI3FODtg9bXJnrFoPEc,15093
|
|
39
|
+
sourcecode-0.11.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
40
|
+
sourcecode-0.11.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
41
|
+
sourcecode-0.11.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|