sourcecode 0.21.0__py3-none-any.whl → 0.22.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 +72 -23
- sourcecode/classifier.py +3 -1
- sourcecode/cli.py +30 -24
- sourcecode/dependency_analyzer.py +75 -0
- sourcecode/detectors/base.py +2 -0
- sourcecode/detectors/heuristic.py +2 -0
- sourcecode/detectors/nodejs.py +6 -1
- sourcecode/detectors/project.py +10 -1
- sourcecode/detectors/python.py +63 -14
- sourcecode/scanner.py +58 -2
- sourcecode/schema.py +1 -0
- sourcecode/serializer.py +187 -0
- sourcecode/summarizer.py +25 -10
- sourcecode/tree_utils.py +14 -0
- {sourcecode-0.21.0.dist-info → sourcecode-0.22.0.dist-info}/METADATA +1 -1
- {sourcecode-0.21.0.dist-info → sourcecode-0.22.0.dist-info}/RECORD +19 -19
- {sourcecode-0.21.0.dist-info → sourcecode-0.22.0.dist-info}/WHEEL +0 -0
- {sourcecode-0.21.0.dist-info → sourcecode-0.22.0.dist-info}/entry_points.txt +0 -0
sourcecode/__init__.py
CHANGED
|
@@ -14,6 +14,20 @@ _NODE_EXTENSIONS = {".js", ".jsx", ".ts", ".tsx", ".mjs", ".cjs"}
|
|
|
14
14
|
_GO_EXTENSIONS = {".go"}
|
|
15
15
|
_JAVA_EXTENSIONS = {".java", ".kt", ".scala"}
|
|
16
16
|
|
|
17
|
+
_CORE_DETECTION_MODULES = {"scanner", "detectors", "classifier", "workspace"}
|
|
18
|
+
|
|
19
|
+
_OPTIONAL_LABEL_MAP: dict[str, str] = {
|
|
20
|
+
"DependencyAnalyzer": "dependencias",
|
|
21
|
+
"GraphAnalyzer": "grafo de módulos",
|
|
22
|
+
"DocAnalyzer": "docs y docstrings",
|
|
23
|
+
"MetricsAnalyzer": "métricas de calidad",
|
|
24
|
+
"SemanticAnalyzer": "semántica y call graph",
|
|
25
|
+
"ArchitectureAnalyzer": "inferencia arquitectónica",
|
|
26
|
+
"GitAnalyzer": "contexto git",
|
|
27
|
+
"EnvAnalyzer": "variables de entorno",
|
|
28
|
+
"CodeNotesAnalyzer": "anotaciones de código",
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
|
|
18
32
|
class ArchitectureSummarizer:
|
|
19
33
|
"""Construye un resumen arquitectonico estatico de 3-5 lineas."""
|
|
@@ -51,18 +65,25 @@ class ArchitectureSummarizer:
|
|
|
51
65
|
if content is None:
|
|
52
66
|
return f"Entry point principal: {entry_point.path}. Arquitectura no inferida con suficiente evidencia estatica."
|
|
53
67
|
|
|
54
|
-
lines = [self._describe_entry_point(entry_point, sm.project_type)]
|
|
55
68
|
suffix = Path(entry_point.path).suffix
|
|
56
69
|
if suffix in _PYTHON_EXTENSIONS:
|
|
57
|
-
|
|
70
|
+
lang_lines = self._summarize_python_entry(entry_point.path, content)
|
|
58
71
|
elif suffix in _NODE_EXTENSIONS:
|
|
59
|
-
|
|
72
|
+
lang_lines = self._summarize_node_entry(entry_point.path, content)
|
|
60
73
|
elif suffix in _GO_EXTENSIONS:
|
|
61
|
-
|
|
74
|
+
lang_lines = self._summarize_go_entry(entry_point.path, content)
|
|
62
75
|
elif suffix in _JAVA_EXTENSIONS:
|
|
63
|
-
|
|
76
|
+
lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
|
|
64
77
|
else:
|
|
65
|
-
|
|
78
|
+
lang_lines = []
|
|
79
|
+
|
|
80
|
+
if lang_lines:
|
|
81
|
+
# Product-level description available — no need for internal "Entry point: ..." header
|
|
82
|
+
lines = lang_lines
|
|
83
|
+
else:
|
|
84
|
+
lines = [self._describe_entry_point(entry_point, sm.project_type)]
|
|
85
|
+
if not lang_lines:
|
|
86
|
+
lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
|
|
66
87
|
|
|
67
88
|
unique_lines: list[str] = []
|
|
68
89
|
seen: set[str] = set()
|
|
@@ -78,11 +99,11 @@ class ArchitectureSummarizer:
|
|
|
78
99
|
try:
|
|
79
100
|
tree = ast.parse(content)
|
|
80
101
|
except SyntaxError:
|
|
81
|
-
return [
|
|
102
|
+
return []
|
|
82
103
|
|
|
83
104
|
package_prefix = self._python_package_prefix(path)
|
|
84
105
|
imported_modules: list[str] = []
|
|
85
|
-
|
|
106
|
+
optional_class_names: list[str] = []
|
|
86
107
|
uses_serializer = False
|
|
87
108
|
uses_redactor = False
|
|
88
109
|
|
|
@@ -90,30 +111,58 @@ class ArchitectureSummarizer:
|
|
|
90
111
|
if isinstance(node, ast.ImportFrom) and isinstance(node.module, str):
|
|
91
112
|
if package_prefix and node.module.startswith(package_prefix):
|
|
92
113
|
imported_modules.append(node.module.rsplit(".", 1)[-1])
|
|
93
|
-
if node.module
|
|
114
|
+
if node.module.endswith(".serializer") or "serializer" in node.module:
|
|
94
115
|
uses_serializer = True
|
|
95
|
-
if node.module
|
|
116
|
+
if node.module.endswith(".redactor") or "redactor" in node.module:
|
|
96
117
|
uses_redactor = True
|
|
97
118
|
elif isinstance(node, ast.Assign):
|
|
98
|
-
|
|
99
|
-
if
|
|
100
|
-
|
|
119
|
+
cls_name = self._extract_optional_analyzer_class_name(node)
|
|
120
|
+
if cls_name:
|
|
121
|
+
optional_class_names.append(cls_name)
|
|
101
122
|
|
|
123
|
+
module_set = set(imported_modules)
|
|
102
124
|
lines: list[str] = []
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if
|
|
107
|
-
lines.append(
|
|
125
|
+
|
|
126
|
+
# Detection line — infer from core modules present
|
|
127
|
+
has_core = bool(module_set & _CORE_DETECTION_MODULES)
|
|
128
|
+
if has_core:
|
|
129
|
+
lines.append("Analiza el árbol del repositorio y detecta stack, entrypoints y tipo de proyecto.")
|
|
130
|
+
elif module_set:
|
|
131
|
+
lines.append("Analiza el proyecto y produce información estructurada.")
|
|
132
|
+
|
|
133
|
+
# Output line
|
|
108
134
|
if uses_serializer:
|
|
109
|
-
|
|
135
|
+
out = "Produce un SourceMap serializable en JSON/YAML"
|
|
110
136
|
if uses_redactor:
|
|
111
|
-
|
|
112
|
-
lines.append(
|
|
113
|
-
|
|
114
|
-
|
|
137
|
+
out += " con redacción de secretos"
|
|
138
|
+
lines.append(out + ".")
|
|
139
|
+
|
|
140
|
+
# Optional capabilities line
|
|
141
|
+
opt_labels = [
|
|
142
|
+
_OPTIONAL_LABEL_MAP[cls]
|
|
143
|
+
for cls in optional_class_names
|
|
144
|
+
if cls in _OPTIONAL_LABEL_MAP
|
|
145
|
+
]
|
|
146
|
+
if opt_labels:
|
|
147
|
+
if len(opt_labels) > 1:
|
|
148
|
+
joined = ", ".join(opt_labels[:-1]) + " y " + opt_labels[-1]
|
|
149
|
+
else:
|
|
150
|
+
joined = opt_labels[0]
|
|
151
|
+
lines.append(f"Opcionalmente añade {joined}.")
|
|
152
|
+
|
|
115
153
|
return lines
|
|
116
154
|
|
|
155
|
+
def _extract_optional_analyzer_class_name(self, node: ast.Assign) -> str | None:
|
|
156
|
+
value = node.value
|
|
157
|
+
if not isinstance(value, ast.IfExp):
|
|
158
|
+
return None
|
|
159
|
+
if not isinstance(value.body, ast.Call):
|
|
160
|
+
return None
|
|
161
|
+
if not isinstance(value.body.func, ast.Name):
|
|
162
|
+
return None
|
|
163
|
+
cls_name = value.body.func.id
|
|
164
|
+
return cls_name if cls_name.endswith("Analyzer") else None
|
|
165
|
+
|
|
117
166
|
def _summarize_node_entry(self, path: str, content: str) -> list[str]:
|
|
118
167
|
imports = re.findall(r"""from\s+['"](\.?\.?/[^'"]+)['"]|require\(['"](\.?\.?/[^'"]+)['"]\)""", content)
|
|
119
168
|
modules = [item for pair in imports for item in pair if item]
|
sourcecode/classifier.py
CHANGED
|
@@ -128,7 +128,9 @@ class TypeClassifier:
|
|
|
128
128
|
has_api = False
|
|
129
129
|
for stack in stacks:
|
|
130
130
|
frameworks = {framework.name for framework in stack.frameworks}
|
|
131
|
-
if frameworks & _WEB_FRAMEWORKS
|
|
131
|
+
if frameworks & _WEB_FRAMEWORKS:
|
|
132
|
+
has_web = True
|
|
133
|
+
elif stack.stack == "nodejs" and stack.detection_method != "heuristic":
|
|
132
134
|
has_web = True
|
|
133
135
|
if frameworks & _API_FRAMEWORKS or stack.stack in _API_STACKS:
|
|
134
136
|
has_api = True
|
sourcecode/cli.py
CHANGED
|
@@ -78,7 +78,12 @@ def main(
|
|
|
78
78
|
no_tree: bool = typer.Option(
|
|
79
79
|
False,
|
|
80
80
|
"--no-tree",
|
|
81
|
-
help="Suprimir file_tree y file_paths del output (
|
|
81
|
+
help="Suprimir file_tree y file_paths del output (ahora deprecado: el arbol ya no se incluye por defecto)",
|
|
82
|
+
),
|
|
83
|
+
tree: bool = typer.Option(
|
|
84
|
+
False,
|
|
85
|
+
"--tree",
|
|
86
|
+
help="Incluir file_tree completo y file_paths en el output (capa deep-dive)",
|
|
82
87
|
),
|
|
83
88
|
no_redact: bool = typer.Option(
|
|
84
89
|
False,
|
|
@@ -114,7 +119,7 @@ def main(
|
|
|
114
119
|
full_metrics: bool = typer.Option(
|
|
115
120
|
False,
|
|
116
121
|
"--full-metrics",
|
|
117
|
-
help="
|
|
122
|
+
help="Auditoria tecnica: LOC, simbolos, complejidad ciclomatica y cobertura por fichero. No incluido en --agent (uso: CI, code review, no context principal para agentes IA)",
|
|
118
123
|
),
|
|
119
124
|
semantics: bool = typer.Option(
|
|
120
125
|
False,
|
|
@@ -159,7 +164,7 @@ def main(
|
|
|
159
164
|
agent: bool = typer.Option(
|
|
160
165
|
False,
|
|
161
166
|
"--agent",
|
|
162
|
-
help="Modo agente:
|
|
167
|
+
help="Modo agente: output estructurado y sin ruido para consumo por IA. Incluye identidad, entrypoints, arquitectura, dependencias clave, señales operacionales y gaps. Sin arbol de ficheros ni secciones vacias.",
|
|
163
168
|
),
|
|
164
169
|
) -> None:
|
|
165
170
|
"""Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
|
|
@@ -216,7 +221,7 @@ def main(
|
|
|
216
221
|
SourceMap,
|
|
217
222
|
StackDetection,
|
|
218
223
|
)
|
|
219
|
-
from sourcecode.serializer import compact_view, normalize_source_map, validate_cross_analyzer_consistency, validate_source_map, write_output
|
|
224
|
+
from sourcecode.serializer import agent_view, compact_view, normalize_source_map, standard_view, validate_cross_analyzer_consistency, validate_source_map, write_output
|
|
220
225
|
from sourcecode.workspace import WorkspaceAnalyzer
|
|
221
226
|
|
|
222
227
|
# 1. Escanear el directorio (SCAN-01 a SCAN-05)
|
|
@@ -235,15 +240,13 @@ def main(
|
|
|
235
240
|
_java_min_depth = 8
|
|
236
241
|
effective_depth = max(depth, _java_min_depth) if _is_java and depth < _java_min_depth else depth
|
|
237
242
|
|
|
238
|
-
# --agent:
|
|
243
|
+
# --agent: enable signal analyzers; output via agent_view (not compact)
|
|
239
244
|
if agent:
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
_agent_flags.append("--no-tree")
|
|
246
|
-
typer.echo(f"[agent] {' '.join(_agent_flags)}", err=True)
|
|
245
|
+
dependencies = True
|
|
246
|
+
env_map = True
|
|
247
|
+
code_notes = True
|
|
248
|
+
no_tree = True # agents never need the raw file tree
|
|
249
|
+
typer.echo("[agent] dependencies env-map code-notes (no-tree)", err=True)
|
|
247
250
|
|
|
248
251
|
scanner = FileScanner(target, max_depth=effective_depth)
|
|
249
252
|
raw_tree = scanner.scan_tree()
|
|
@@ -562,16 +565,20 @@ def main(
|
|
|
562
565
|
p.replace("\\", "/") for p in flatten_file_tree(sm.file_tree)
|
|
563
566
|
]
|
|
564
567
|
|
|
565
|
-
# LQN-05: top-15 dependencias directas de manifest/lockfile
|
|
568
|
+
# LQN-05: top-15 dependencias directas de manifest/lockfile, ordenadas por rol
|
|
566
569
|
if dependency_analyzer is not None:
|
|
570
|
+
from sourcecode.dependency_analyzer import _ROLE_PRIORITY
|
|
571
|
+
|
|
567
572
|
primary_ecosystem = sm.stacks[0].stack if sm.stacks else ""
|
|
568
573
|
direct_deps = [
|
|
569
574
|
d for d in sm.dependencies
|
|
570
575
|
if d.scope != "transitive" and d.source in {"manifest", "lockfile"}
|
|
571
576
|
]
|
|
572
577
|
|
|
573
|
-
def _dep_sort_key(d: Any) -> tuple[int, str]:
|
|
574
|
-
|
|
578
|
+
def _dep_sort_key(d: Any) -> tuple[int, int, str]:
|
|
579
|
+
role_order = _ROLE_PRIORITY.get(d.role or "runtime", 5)
|
|
580
|
+
eco_order = 0 if d.ecosystem == primary_ecosystem else 1
|
|
581
|
+
return (role_order, eco_order, d.name.lower())
|
|
575
582
|
|
|
576
583
|
sm.key_dependencies = sorted(direct_deps, key=_dep_sort_key)[:15]
|
|
577
584
|
|
|
@@ -616,24 +623,23 @@ def main(
|
|
|
616
623
|
for _finding in validate_cross_analyzer_consistency(sm, strict=False):
|
|
617
624
|
typer.echo(f"[consistency] {_finding}", err=True)
|
|
618
625
|
|
|
619
|
-
# 4. Serializar
|
|
620
|
-
if
|
|
626
|
+
# 4. Serializar
|
|
627
|
+
if agent:
|
|
628
|
+
data = agent_view(sm)
|
|
629
|
+
if not no_redact:
|
|
630
|
+
data = redact_dict(data)
|
|
631
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
632
|
+
elif compact:
|
|
621
633
|
data = compact_view(sm, no_tree=no_tree)
|
|
622
|
-
# Aplicar redaccion sobre el dict del compact view
|
|
623
634
|
if not no_redact:
|
|
624
635
|
data = redact_dict(data)
|
|
625
636
|
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
626
637
|
else:
|
|
627
|
-
|
|
628
|
-
raw_dict = asdict(sm)
|
|
629
|
-
if no_tree:
|
|
630
|
-
raw_dict.pop("file_tree", None)
|
|
631
|
-
raw_dict.pop("file_paths", None)
|
|
638
|
+
raw_dict = standard_view(sm, include_tree=tree and not no_tree)
|
|
632
639
|
if not no_redact:
|
|
633
640
|
raw_dict = redact_dict(raw_dict)
|
|
634
641
|
|
|
635
642
|
if format == "yaml":
|
|
636
|
-
# Para YAML, serializar el dict directamente con ruamel.yaml
|
|
637
643
|
from io import StringIO
|
|
638
644
|
|
|
639
645
|
from ruamel.yaml import YAML
|
|
@@ -12,6 +12,77 @@ from ruamel.yaml import YAML
|
|
|
12
12
|
from sourcecode.detectors.parsers import load_json_file, load_toml_file, read_text_lines
|
|
13
13
|
from sourcecode.schema import DependencyRecord, DependencySummary
|
|
14
14
|
|
|
15
|
+
# ── Role inference ────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
_PY_TESTTOOLS: frozenset[str] = frozenset({
|
|
18
|
+
"pytest", "unittest2", "hypothesis", "nose", "nose2", "coverage", "tox",
|
|
19
|
+
"factory-boy", "faker", "responses", "moto", "freezegun", "httpretty",
|
|
20
|
+
"respx", "pytest-cov", "pytest-mock", "pytest-asyncio", "pytest-xdist",
|
|
21
|
+
})
|
|
22
|
+
_PY_DEVTOOLS: frozenset[str] = frozenset({
|
|
23
|
+
"mypy", "ruff", "flake8", "black", "pylint", "bandit", "isort",
|
|
24
|
+
"pre-commit", "sphinx", "mkdocs", "pyright", "pyflakes", "autopep8",
|
|
25
|
+
"pycodestyle", "pydocstyle", "vulture",
|
|
26
|
+
})
|
|
27
|
+
_PY_SERIALIZATION: frozenset[str] = frozenset({
|
|
28
|
+
"ruamel.yaml", "pyyaml", "orjson", "msgpack", "ujson", "simplejson",
|
|
29
|
+
"cbor2", "tomli", "tomllib", "toml",
|
|
30
|
+
})
|
|
31
|
+
_PY_PARSING: frozenset[str] = frozenset({
|
|
32
|
+
"pathspec", "gitpython", "lxml", "beautifulsoup4", "html5lib",
|
|
33
|
+
"regex", "pyparsing",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
_NODE_TESTTOOLS: frozenset[str] = frozenset({
|
|
37
|
+
"jest", "@jest/globals", "mocha", "jasmine", "chai", "karma",
|
|
38
|
+
"cypress", "playwright", "@playwright/test", "vitest", "@vitest/ui",
|
|
39
|
+
"sinon", "nock", "supertest",
|
|
40
|
+
})
|
|
41
|
+
_NODE_DEVTOOLS: frozenset[str] = frozenset({
|
|
42
|
+
"eslint", "prettier", "typescript", "@types/node", "webpack", "rollup",
|
|
43
|
+
"parcel", "@babel/core", "husky", "lint-staged", "nodemon", "ts-node", "tsx",
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
_DEV_SCOPES: frozenset[str] = frozenset({"dev", "optional"})
|
|
47
|
+
|
|
48
|
+
_ROLE_PRIORITY: dict[str, int] = {
|
|
49
|
+
"runtime": 0,
|
|
50
|
+
"parsing": 1,
|
|
51
|
+
"serialization": 2,
|
|
52
|
+
"testtool": 3,
|
|
53
|
+
"devtool": 4,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _infer_role(name: str, ecosystem: str, scope: str) -> str:
|
|
58
|
+
"""Infer dependency role: runtime | parsing | serialization | devtool | testtool."""
|
|
59
|
+
n = name.lower()
|
|
60
|
+
is_dev = scope in _DEV_SCOPES or scope.startswith(("optional:", "group:"))
|
|
61
|
+
|
|
62
|
+
if ecosystem == "python":
|
|
63
|
+
if n in _PY_TESTTOOLS or n.startswith("pytest-"):
|
|
64
|
+
return "testtool"
|
|
65
|
+
if n in _PY_DEVTOOLS:
|
|
66
|
+
return "devtool"
|
|
67
|
+
if is_dev:
|
|
68
|
+
return "devtool"
|
|
69
|
+
if n in _PY_SERIALIZATION:
|
|
70
|
+
return "serialization"
|
|
71
|
+
if n in _PY_PARSING:
|
|
72
|
+
return "parsing"
|
|
73
|
+
return "runtime"
|
|
74
|
+
|
|
75
|
+
if ecosystem == "nodejs":
|
|
76
|
+
if n in _NODE_TESTTOOLS or n.startswith("@testing-library/"):
|
|
77
|
+
return "testtool"
|
|
78
|
+
if n in _NODE_DEVTOOLS:
|
|
79
|
+
return "devtool"
|
|
80
|
+
if is_dev:
|
|
81
|
+
return "devtool"
|
|
82
|
+
return "runtime"
|
|
83
|
+
|
|
84
|
+
return "devtool" if is_dev else "runtime"
|
|
85
|
+
|
|
15
86
|
|
|
16
87
|
class DependencyAnalyzer:
|
|
17
88
|
"""Resuelve dependencias desde manifests y lockfiles sin ejecutar toolchains."""
|
|
@@ -41,6 +112,10 @@ class DependencyAnalyzer:
|
|
|
41
112
|
limitations.extend(handler_limitations)
|
|
42
113
|
|
|
43
114
|
deduped = self._dedupe(records)
|
|
115
|
+
deduped = [
|
|
116
|
+
replace(r, role=_infer_role(r.name, r.ecosystem, r.scope))
|
|
117
|
+
for r in deduped
|
|
118
|
+
]
|
|
44
119
|
return deduped, self._build_summary(deduped, limitations)
|
|
45
120
|
|
|
46
121
|
def merge_summaries(self, summaries: Iterable[DependencySummary]) -> DependencySummary:
|
sourcecode/detectors/base.py
CHANGED
|
@@ -15,6 +15,8 @@ class DetectionContext:
|
|
|
15
15
|
root: Path
|
|
16
16
|
file_tree: dict[str, Any]
|
|
17
17
|
manifests: list[str] = field(default_factory=list)
|
|
18
|
+
manifest_types: dict[str, str] = field(default_factory=dict)
|
|
19
|
+
# manifest_types: {filename → "application"|"workspace"|"auxiliary"|"config"}
|
|
18
20
|
|
|
19
21
|
|
|
20
22
|
class AbstractDetector(ABC):
|
|
@@ -43,6 +43,8 @@ class HeuristicDetector(AbstractDetector):
|
|
|
43
43
|
paths = flatten_file_tree(context.file_tree)
|
|
44
44
|
counts: Counter[str] = Counter()
|
|
45
45
|
for path in paths:
|
|
46
|
+
if path.startswith("."):
|
|
47
|
+
continue
|
|
46
48
|
for extension, stack in _EXTENSION_MAP.items():
|
|
47
49
|
if path.endswith(extension):
|
|
48
50
|
counts[stack] += 1
|
sourcecode/detectors/nodejs.py
CHANGED
|
@@ -28,7 +28,12 @@ class NodejsDetector(AbstractDetector):
|
|
|
28
28
|
priority = 20
|
|
29
29
|
|
|
30
30
|
def can_detect(self, context: DetectionContext) -> bool:
|
|
31
|
-
|
|
31
|
+
if "package.json" not in context.manifests:
|
|
32
|
+
return False
|
|
33
|
+
if not path_exists_in_tree(context.file_tree, "package.json"):
|
|
34
|
+
return False
|
|
35
|
+
manifest_type = context.manifest_types.get("package.json", "application")
|
|
36
|
+
return manifest_type not in {"auxiliary", "config"}
|
|
32
37
|
|
|
33
38
|
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
34
39
|
package_json = load_json_file(context.root / "package.json")
|
sourcecode/detectors/project.py
CHANGED
|
@@ -7,6 +7,7 @@ from typing import Any
|
|
|
7
7
|
from sourcecode.classifier import TypeClassifier
|
|
8
8
|
from sourcecode.detectors.base import AbstractDetector, DetectionContext
|
|
9
9
|
from sourcecode.detectors.tooling import collect_tooling_signals, infer_package_manager
|
|
10
|
+
from sourcecode.scanner import classify_manifest
|
|
10
11
|
from sourcecode.schema import EntryPoint, FrameworkDetection, StackDetection
|
|
11
12
|
|
|
12
13
|
_CONFIDENCE_RANK = {"low": 0, "medium": 1, "high": 2}
|
|
@@ -30,7 +31,15 @@ class ProjectDetector:
|
|
|
30
31
|
manifests: Sequence[str],
|
|
31
32
|
) -> tuple[list[StackDetection], list[EntryPoint], str | None]:
|
|
32
33
|
manifest_names = [Path(manifest).name for manifest in manifests]
|
|
33
|
-
|
|
34
|
+
manifest_types = {
|
|
35
|
+
Path(m).name: classify_manifest(m, root) for m in manifests
|
|
36
|
+
}
|
|
37
|
+
context = DetectionContext(
|
|
38
|
+
root=root,
|
|
39
|
+
file_tree=file_tree,
|
|
40
|
+
manifests=manifest_names,
|
|
41
|
+
manifest_types=manifest_types,
|
|
42
|
+
)
|
|
34
43
|
merged_stacks: dict[str, StackDetection] = {}
|
|
35
44
|
merged_entry_points: dict[str, EntryPoint] = {}
|
|
36
45
|
|
sourcecode/detectors/python.py
CHANGED
|
@@ -12,7 +12,7 @@ from sourcecode.detectors.base import (
|
|
|
12
12
|
)
|
|
13
13
|
from sourcecode.detectors.parsers import load_toml_file, read_text_lines, unique_strings
|
|
14
14
|
from sourcecode.schema import FrameworkDetection
|
|
15
|
-
from sourcecode.tree_utils import path_exists_in_tree
|
|
15
|
+
from sourcecode.tree_utils import find_files_by_name, path_exists_in_tree
|
|
16
16
|
|
|
17
17
|
_FRAMEWORK_MAP = {
|
|
18
18
|
"fastapi": "FastAPI",
|
|
@@ -146,35 +146,84 @@ class PythonDetector(AbstractDetector):
|
|
|
146
146
|
entry_points: list[EntryPoint] = []
|
|
147
147
|
declared = set()
|
|
148
148
|
for path in unique_strings(declared_candidates):
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
actual = path
|
|
150
|
+
if not path_exists_in_tree(context.file_tree, path):
|
|
151
|
+
src_path = f"src/{path}"
|
|
152
|
+
if path_exists_in_tree(context.file_tree, src_path):
|
|
153
|
+
actual = src_path
|
|
154
|
+
else:
|
|
155
|
+
continue
|
|
156
|
+
declared.add(actual)
|
|
157
|
+
kind = "cli" if actual.endswith(("__main__.py", "main.py", "cli.py")) else "app"
|
|
158
|
+
entry_points.append(
|
|
159
|
+
EntryPoint(
|
|
160
|
+
path=actual,
|
|
161
|
+
stack="python",
|
|
162
|
+
kind=kind,
|
|
163
|
+
source="pyproject.toml",
|
|
164
|
+
confidence="high",
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
_CONVENTION_NAMES = {"cli.py", "__main__.py", "main.py", "app.py", "manage.py"}
|
|
169
|
+
seen_paths = set(declared)
|
|
170
|
+
for fname in _CONVENTION_NAMES:
|
|
171
|
+
for path in find_files_by_name(context.file_tree, fname):
|
|
172
|
+
if path in seen_paths or self._is_tooling_path(path):
|
|
173
|
+
continue
|
|
174
|
+
seen_paths.add(path)
|
|
175
|
+
kind = "cli" if fname in ("cli.py", "__main__.py", "main.py") else "app"
|
|
152
176
|
entry_points.append(
|
|
153
177
|
EntryPoint(
|
|
154
178
|
path=path,
|
|
155
179
|
stack="python",
|
|
156
180
|
kind=kind,
|
|
157
|
-
source="
|
|
158
|
-
confidence="
|
|
181
|
+
source="convention",
|
|
182
|
+
confidence="medium",
|
|
159
183
|
)
|
|
160
184
|
)
|
|
161
185
|
|
|
162
|
-
|
|
163
|
-
for
|
|
164
|
-
if
|
|
186
|
+
# code signal: scan Python files for if __name__ == "__main__" guard
|
|
187
|
+
for py_path in self._find_main_guard_files(context):
|
|
188
|
+
if py_path in seen_paths:
|
|
165
189
|
continue
|
|
166
|
-
|
|
190
|
+
seen_paths.add(py_path)
|
|
167
191
|
entry_points.append(
|
|
168
192
|
EntryPoint(
|
|
169
|
-
path=
|
|
193
|
+
path=py_path,
|
|
170
194
|
stack="python",
|
|
171
|
-
kind=
|
|
172
|
-
source="
|
|
173
|
-
confidence="
|
|
195
|
+
kind="script",
|
|
196
|
+
source="code_signal",
|
|
197
|
+
confidence="low",
|
|
174
198
|
)
|
|
175
199
|
)
|
|
200
|
+
|
|
176
201
|
return entry_points
|
|
177
202
|
|
|
203
|
+
_MAIN_GUARD_RE = re.compile(r"^if __name__\s*==\s*['\"]__main__['\"]", re.MULTILINE)
|
|
204
|
+
|
|
205
|
+
def _find_main_guard_files(self, context: DetectionContext) -> list[str]:
|
|
206
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
207
|
+
results: list[str] = []
|
|
208
|
+
for path in flatten_file_tree(context.file_tree):
|
|
209
|
+
if not path.endswith(".py") or self._is_tooling_path(path):
|
|
210
|
+
continue
|
|
211
|
+
try:
|
|
212
|
+
content = (context.root / path).read_text(encoding="utf-8", errors="replace")
|
|
213
|
+
if self._MAIN_GUARD_RE.search(content):
|
|
214
|
+
results.append(path)
|
|
215
|
+
except OSError:
|
|
216
|
+
continue
|
|
217
|
+
return results
|
|
218
|
+
|
|
219
|
+
def _is_tooling_path(self, path: str) -> bool:
|
|
220
|
+
parts = path.split("/")
|
|
221
|
+
return any(
|
|
222
|
+
part.startswith(".")
|
|
223
|
+
or part in {"tests", "test", "__pycache__", "node_modules", "venv", ".venv"}
|
|
224
|
+
for part in parts[:-1]
|
|
225
|
+
)
|
|
226
|
+
|
|
178
227
|
def _detect_package_manager(
|
|
179
228
|
self, context: DetectionContext, *, pyproject: dict[str, Any] | None
|
|
180
229
|
) -> str:
|
sourcecode/scanner.py
CHANGED
|
@@ -48,6 +48,57 @@ MANIFEST_NAMES: frozenset[str] = frozenset({
|
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
# Manifest types: application | library | tooling | workspace | config | auxiliary
|
|
52
|
+
_APPLICATION_MANIFESTS: frozenset[str] = frozenset({
|
|
53
|
+
"pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile",
|
|
54
|
+
"package.json", "go.mod", "Cargo.toml", "pom.xml", "build.gradle",
|
|
55
|
+
"build.gradle.kts", "composer.json", "Gemfile", "pubspec.yaml",
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
_CONFIG_MANIFESTS: frozenset[str] = frozenset({
|
|
59
|
+
".npmrc", ".yarnrc", ".yarnrc.yml", "renovate.json", ".renovaterc",
|
|
60
|
+
"dependabot.yml",
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def classify_manifest(manifest_path: str, root: Path) -> str:
|
|
65
|
+
"""Classify a manifest as application | workspace | auxiliary | config | tooling.
|
|
66
|
+
|
|
67
|
+
Rules (in priority order):
|
|
68
|
+
1. In a hidden directory (.claude/, .vscode/, etc.) → auxiliary
|
|
69
|
+
2. In a known editor/agent config dir → auxiliary
|
|
70
|
+
3. At root level with a known manifest name → application
|
|
71
|
+
4. One level deep in a non-hidden dir → workspace
|
|
72
|
+
5. Otherwise → auxiliary
|
|
73
|
+
"""
|
|
74
|
+
try:
|
|
75
|
+
rel = Path(manifest_path).resolve().relative_to(root.resolve())
|
|
76
|
+
except ValueError:
|
|
77
|
+
rel = Path(manifest_path)
|
|
78
|
+
|
|
79
|
+
parts = rel.parts
|
|
80
|
+
if not parts:
|
|
81
|
+
return "auxiliary"
|
|
82
|
+
|
|
83
|
+
# Any hidden directory in the path → auxiliary tooling
|
|
84
|
+
if any(p.startswith(".") for p in parts[:-1]):
|
|
85
|
+
return "auxiliary"
|
|
86
|
+
|
|
87
|
+
name = parts[-1]
|
|
88
|
+
|
|
89
|
+
if len(parts) == 1:
|
|
90
|
+
if name in _CONFIG_MANIFESTS:
|
|
91
|
+
return "config"
|
|
92
|
+
if name in _APPLICATION_MANIFESTS:
|
|
93
|
+
return "application"
|
|
94
|
+
return "application" # unknown root manifest → treat as application
|
|
95
|
+
|
|
96
|
+
if len(parts) == 2:
|
|
97
|
+
return "workspace" # depth-1 sub-package
|
|
98
|
+
|
|
99
|
+
return "auxiliary"
|
|
100
|
+
|
|
101
|
+
|
|
51
102
|
class FileScanner:
|
|
52
103
|
"""Escanea un directorio de proyecto y produce un arbol de ficheros filtrado.
|
|
53
104
|
|
|
@@ -172,10 +223,15 @@ class FileScanner:
|
|
|
172
223
|
candidate = self.root / name
|
|
173
224
|
if candidate.exists() and not candidate.is_symlink():
|
|
174
225
|
manifests.append(str(candidate))
|
|
175
|
-
# Profundidad 1: primer nivel
|
|
226
|
+
# Profundidad 1: primer nivel (excluir directorios ocultos — son tooling, no proyecto)
|
|
176
227
|
try:
|
|
177
228
|
for child in self.root.iterdir():
|
|
178
|
-
if
|
|
229
|
+
if (
|
|
230
|
+
child.is_dir()
|
|
231
|
+
and not child.is_symlink()
|
|
232
|
+
and child.name not in self._excludes
|
|
233
|
+
and not child.name.startswith(".")
|
|
234
|
+
):
|
|
179
235
|
for name in MANIFEST_NAMES:
|
|
180
236
|
candidate = child / name
|
|
181
237
|
if candidate.exists() and not candidate.is_symlink():
|
sourcecode/schema.py
CHANGED
sourcecode/serializer.py
CHANGED
|
@@ -345,6 +345,193 @@ def validate_cross_analyzer_consistency(
|
|
|
345
345
|
return findings
|
|
346
346
|
|
|
347
347
|
|
|
348
|
+
def agent_view(sm: SourceMap) -> dict[str, Any]:
|
|
349
|
+
"""Opinionated output for AI agents — structured, noise-free, gap-aware.
|
|
350
|
+
|
|
351
|
+
Output order:
|
|
352
|
+
project → what it is and what stack it uses
|
|
353
|
+
entry_points → where execution starts
|
|
354
|
+
architecture → how it's structured
|
|
355
|
+
key_dependencies → what runtime dependencies matter (when analyzed)
|
|
356
|
+
signals → compact operational signals (env, notes, tests)
|
|
357
|
+
gaps → what's uncertain or missing
|
|
358
|
+
|
|
359
|
+
Never includes: file_tree, file_paths, schema internals, empty sections,
|
|
360
|
+
null fields, raw dependency lists, or low-signal metadata.
|
|
361
|
+
"""
|
|
362
|
+
# ── 1. Identity ──────────────────────────────────────────────────────────
|
|
363
|
+
primary = next((s for s in sm.stacks if s.primary), sm.stacks[0] if sm.stacks else None)
|
|
364
|
+
|
|
365
|
+
project: dict[str, Any] = {
|
|
366
|
+
"type": sm.project_type,
|
|
367
|
+
"summary": sm.project_summary,
|
|
368
|
+
}
|
|
369
|
+
if primary:
|
|
370
|
+
project["primary_stack"] = primary.stack
|
|
371
|
+
if primary.frameworks:
|
|
372
|
+
project["frameworks"] = [f.name for f in primary.frameworks]
|
|
373
|
+
if primary.package_manager:
|
|
374
|
+
project["package_manager"] = primary.package_manager
|
|
375
|
+
if primary.root and primary.root != ".":
|
|
376
|
+
project["root"] = primary.root
|
|
377
|
+
|
|
378
|
+
secondary = [s for s in sm.stacks if not s.primary and s.stack != (primary.stack if primary else "")]
|
|
379
|
+
if secondary:
|
|
380
|
+
project["secondary_stacks"] = sorted({s.stack for s in secondary})
|
|
381
|
+
|
|
382
|
+
result: dict[str, Any] = {"project": project}
|
|
383
|
+
|
|
384
|
+
# ── 2. Entry points ───────────────────────────────────────────────────────
|
|
385
|
+
if sm.entry_points:
|
|
386
|
+
result["entry_points"] = [
|
|
387
|
+
{k: v for k, v in asdict(ep).items() if v is not None and v != ""}
|
|
388
|
+
for ep in sm.entry_points
|
|
389
|
+
]
|
|
390
|
+
|
|
391
|
+
# ── 3. Architecture ───────────────────────────────────────────────────────
|
|
392
|
+
if sm.architecture_summary:
|
|
393
|
+
result["architecture"] = sm.architecture_summary
|
|
394
|
+
|
|
395
|
+
# ── 4. Key dependencies (role-sorted, already computed) ───────────────────
|
|
396
|
+
if sm.dependency_summary and sm.dependency_summary.requested and sm.key_dependencies:
|
|
397
|
+
_skip = {"parent", "manifest_path", "workspace", "source", "ecosystem"}
|
|
398
|
+
result["key_dependencies"] = [
|
|
399
|
+
{k: v for k, v in asdict(d).items() if v is not None and k not in _skip}
|
|
400
|
+
for d in sm.key_dependencies
|
|
401
|
+
]
|
|
402
|
+
|
|
403
|
+
# ── 5. Signals — compact operational context ─────────────────────────────
|
|
404
|
+
signals: dict[str, Any] = {}
|
|
405
|
+
|
|
406
|
+
if sm.env_summary and sm.env_summary.requested and sm.env_summary.total > 0:
|
|
407
|
+
signals["env_vars"] = {
|
|
408
|
+
"total": sm.env_summary.total,
|
|
409
|
+
"required": sm.env_summary.required_count,
|
|
410
|
+
}
|
|
411
|
+
if sm.env_summary.categories:
|
|
412
|
+
signals["env_vars"]["categories"] = sm.env_summary.categories
|
|
413
|
+
|
|
414
|
+
if sm.code_notes_summary and sm.code_notes_summary.requested and sm.code_notes_summary.total > 0:
|
|
415
|
+
by_kind = {k: v for k, v in sm.code_notes_summary.by_kind.items() if v > 0}
|
|
416
|
+
if by_kind:
|
|
417
|
+
signals["code_notes"] = {"total": sm.code_notes_summary.total, "by_kind": by_kind}
|
|
418
|
+
if sm.code_notes_summary.adr_count > 0:
|
|
419
|
+
signals["adrs"] = sm.code_notes_summary.adr_count
|
|
420
|
+
|
|
421
|
+
has_tests = any(
|
|
422
|
+
"/test" in p or "/tests" in p or "/spec" in p or p.startswith("test")
|
|
423
|
+
for p in sm.file_paths
|
|
424
|
+
)
|
|
425
|
+
if has_tests:
|
|
426
|
+
signals["has_tests"] = True
|
|
427
|
+
|
|
428
|
+
if signals:
|
|
429
|
+
result["signals"] = signals
|
|
430
|
+
|
|
431
|
+
# ── 6. Gaps — what's uncertain or missing ────────────────────────────────
|
|
432
|
+
gaps: list[str] = []
|
|
433
|
+
|
|
434
|
+
if not sm.entry_points:
|
|
435
|
+
gaps.append("No entry point detected — project structure may be non-standard")
|
|
436
|
+
|
|
437
|
+
if primary and primary.confidence == "low":
|
|
438
|
+
gaps.append(f"Low-confidence stack detection for '{primary.stack}' — no manifest found")
|
|
439
|
+
|
|
440
|
+
heuristic_stacks = [s for s in sm.stacks if s.detection_method == "heuristic"]
|
|
441
|
+
if heuristic_stacks:
|
|
442
|
+
gaps.append(
|
|
443
|
+
f"Heuristic-only detection (no manifest): {', '.join(s.stack for s in heuristic_stacks)}"
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
if sm.dependency_summary and sm.dependency_summary.requested:
|
|
447
|
+
for limitation in sm.dependency_summary.limitations[:3]:
|
|
448
|
+
gaps.append(limitation)
|
|
449
|
+
elif not sm.dependency_summary or not sm.dependency_summary.requested:
|
|
450
|
+
gaps.append("Dependencies not analyzed — add --dependencies for full context")
|
|
451
|
+
|
|
452
|
+
if gaps:
|
|
453
|
+
result["gaps"] = gaps
|
|
454
|
+
|
|
455
|
+
return result
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def standard_view(sm: SourceMap, *, include_tree: bool = False) -> dict[str, Any]:
|
|
459
|
+
"""Default output — three signal layers.
|
|
460
|
+
|
|
461
|
+
Layer A (always):
|
|
462
|
+
metadata, project_type, project_summary, architecture_summary,
|
|
463
|
+
stacks, entry_points.
|
|
464
|
+
|
|
465
|
+
Layer B (when the corresponding flag was passed):
|
|
466
|
+
dependency_summary + key_dependencies, env_summary + env_map,
|
|
467
|
+
code_notes_summary + code_notes, git_context.
|
|
468
|
+
|
|
469
|
+
Layer C (only when the flag was explicitly passed, checked via *.requested):
|
|
470
|
+
module_graph, docs, semantic_*, file_metrics, architecture inference.
|
|
471
|
+
|
|
472
|
+
file_tree / file_paths only when include_tree=True.
|
|
473
|
+
Full dependencies list is never included — use key_dependencies instead.
|
|
474
|
+
Empty unrequested analyzer fields are omitted entirely.
|
|
475
|
+
"""
|
|
476
|
+
result: dict[str, Any] = {
|
|
477
|
+
"metadata": asdict(sm.metadata),
|
|
478
|
+
"project_type": sm.project_type,
|
|
479
|
+
"project_summary": sm.project_summary,
|
|
480
|
+
"architecture_summary": sm.architecture_summary,
|
|
481
|
+
"stacks": [asdict(s) for s in sm.stacks],
|
|
482
|
+
"entry_points": [asdict(ep) for ep in sm.entry_points],
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
# Layer B — signals (only when the corresponding analyzer ran)
|
|
486
|
+
if sm.dependency_summary is not None and sm.dependency_summary.requested:
|
|
487
|
+
dep_dict = asdict(sm.dependency_summary)
|
|
488
|
+
dep_dict.pop("dependencies", None) # avoid duplication with key_dependencies
|
|
489
|
+
result["dependency_summary"] = dep_dict
|
|
490
|
+
result["key_dependencies"] = [asdict(d) for d in sm.key_dependencies]
|
|
491
|
+
|
|
492
|
+
if sm.env_summary is not None and sm.env_summary.requested:
|
|
493
|
+
result["env_summary"] = asdict(sm.env_summary)
|
|
494
|
+
result["env_map"] = [asdict(e) for e in sm.env_map]
|
|
495
|
+
|
|
496
|
+
if sm.code_notes_summary is not None and sm.code_notes_summary.requested:
|
|
497
|
+
result["code_notes_summary"] = asdict(sm.code_notes_summary)
|
|
498
|
+
if sm.code_notes:
|
|
499
|
+
result["code_notes"] = [asdict(n) for n in sm.code_notes]
|
|
500
|
+
if sm.code_adrs:
|
|
501
|
+
result["code_adrs"] = [asdict(a) for a in sm.code_adrs]
|
|
502
|
+
|
|
503
|
+
if sm.git_context is not None and sm.git_context.requested:
|
|
504
|
+
result["git_context"] = asdict(sm.git_context)
|
|
505
|
+
|
|
506
|
+
# Layer C — deep-dive (flag must have been explicitly passed)
|
|
507
|
+
if sm.module_graph is not None and sm.module_graph.summary.requested:
|
|
508
|
+
result["module_graph"] = asdict(sm.module_graph)
|
|
509
|
+
result["module_graph_summary"] = asdict(sm.module_graph.summary)
|
|
510
|
+
|
|
511
|
+
if sm.doc_summary is not None and sm.doc_summary.requested:
|
|
512
|
+
result["doc_summary"] = asdict(sm.doc_summary)
|
|
513
|
+
result["docs"] = [asdict(d) for d in sm.docs]
|
|
514
|
+
|
|
515
|
+
if sm.semantic_summary is not None and sm.semantic_summary.requested:
|
|
516
|
+
result["semantic_summary"] = asdict(sm.semantic_summary)
|
|
517
|
+
result["semantic_calls"] = [asdict(c) for c in sm.semantic_calls]
|
|
518
|
+
result["semantic_symbols"] = [asdict(s) for s in sm.semantic_symbols]
|
|
519
|
+
result["semantic_links"] = [asdict(lnk) for lnk in sm.semantic_links]
|
|
520
|
+
|
|
521
|
+
if sm.metrics_summary is not None and sm.metrics_summary.requested:
|
|
522
|
+
result["metrics_summary"] = asdict(sm.metrics_summary)
|
|
523
|
+
result["file_metrics"] = [asdict(m) for m in sm.file_metrics]
|
|
524
|
+
|
|
525
|
+
if sm.architecture is not None and sm.architecture.requested:
|
|
526
|
+
result["architecture"] = asdict(sm.architecture)
|
|
527
|
+
|
|
528
|
+
if include_tree:
|
|
529
|
+
result["file_tree"] = sm.file_tree
|
|
530
|
+
result["file_paths"] = sm.file_paths
|
|
531
|
+
|
|
532
|
+
return result
|
|
533
|
+
|
|
534
|
+
|
|
348
535
|
def write_output(content: str, output: Optional[Path]) -> None:
|
|
349
536
|
"""Escribe el contenido a stdout o a un fichero.
|
|
350
537
|
|
sourcecode/summarizer.py
CHANGED
|
@@ -174,29 +174,44 @@ class ProjectSummarizer:
|
|
|
174
174
|
return None
|
|
175
175
|
return " ".join(lines).strip()
|
|
176
176
|
|
|
177
|
+
_TYPE_LABELS: dict[str, str] = {
|
|
178
|
+
"cli": "CLI",
|
|
179
|
+
"api": "API",
|
|
180
|
+
"webapp": "Aplicación web",
|
|
181
|
+
"library": "Librería",
|
|
182
|
+
"monorepo": "Monorepo",
|
|
183
|
+
"fullstack": "Proyecto fullstack",
|
|
184
|
+
}
|
|
185
|
+
|
|
177
186
|
def _merge_description_with_structure(self, description: str, sm: SourceMap) -> str:
|
|
178
|
-
|
|
187
|
+
project_type = sm.project_type or ""
|
|
188
|
+
type_label = self._TYPE_LABELS.get(project_type, "")
|
|
179
189
|
|
|
180
|
-
|
|
181
|
-
|
|
190
|
+
desc = description.rstrip(".")
|
|
191
|
+
# Prepend type label when description doesn't already open with it
|
|
192
|
+
if type_label and not desc.upper().startswith(type_label.upper()):
|
|
193
|
+
lead = f"{type_label}: {desc}" if desc else type_label
|
|
194
|
+
else:
|
|
195
|
+
lead = desc
|
|
182
196
|
|
|
197
|
+
parts = [lead]
|
|
198
|
+
|
|
199
|
+
# Stack with frameworks — keep brief, skip internal module listings
|
|
183
200
|
non_tooling_stacks = self._filter_non_tooling_stacks(sm)
|
|
184
201
|
if non_tooling_stacks:
|
|
185
202
|
primary = self._select_summary_primary_stack(non_tooling_stacks)
|
|
186
203
|
frameworks = [fw.name for fw in primary.frameworks[:2]]
|
|
187
|
-
|
|
204
|
+
arch_pattern = self._detect_architecture_pattern(sm.file_paths)
|
|
188
205
|
arch_str = f" con arquitectura {arch_pattern}" if arch_pattern else ""
|
|
189
206
|
if frameworks:
|
|
190
|
-
parts.append(f"Stack: {
|
|
207
|
+
parts.append(f"Stack: {primary.stack.capitalize()} ({', '.join(frameworks)}){arch_str}")
|
|
191
208
|
else:
|
|
192
|
-
parts.append(f"Stack: {
|
|
209
|
+
parts.append(f"Stack: {primary.stack.capitalize()}{arch_str}")
|
|
193
210
|
|
|
211
|
+
# Business domains only — skip entry_points (too technical for product summary)
|
|
212
|
+
domains = self._extract_business_domains(sm.file_paths)
|
|
194
213
|
if domains:
|
|
195
214
|
parts.append(f"Dominios: {', '.join(domains)}")
|
|
196
|
-
else:
|
|
197
|
-
entry_points = [ep.path for ep in sm.entry_points if not self._is_tooling_path(ep.path)][:2]
|
|
198
|
-
if entry_points:
|
|
199
|
-
parts.append(f"Entry points: {', '.join(entry_points)}")
|
|
200
215
|
|
|
201
216
|
return ". ".join(parts) + "."
|
|
202
217
|
|
sourcecode/tree_utils.py
CHANGED
|
@@ -14,6 +14,20 @@ def flatten_file_tree(file_tree: dict[str, Any], prefix: str = "") -> list[str]:
|
|
|
14
14
|
return paths
|
|
15
15
|
|
|
16
16
|
|
|
17
|
+
def find_files_by_name(
|
|
18
|
+
file_tree: dict[str, Any], filename: str, prefix: str = ""
|
|
19
|
+
) -> list[str]:
|
|
20
|
+
"""Return all paths in the tree whose filename matches `filename`."""
|
|
21
|
+
paths: list[str] = []
|
|
22
|
+
for key, value in file_tree.items():
|
|
23
|
+
current = f"{prefix}/{key}" if prefix else key
|
|
24
|
+
if isinstance(value, dict):
|
|
25
|
+
paths.extend(find_files_by_name(value, filename, current))
|
|
26
|
+
elif key == filename:
|
|
27
|
+
paths.append(current)
|
|
28
|
+
return paths
|
|
29
|
+
|
|
30
|
+
|
|
17
31
|
def path_exists_in_tree(file_tree: dict[str, Any], target: str) -> bool:
|
|
18
32
|
"""Comprueba si un path relativo existe en el file_tree."""
|
|
19
33
|
normalized = target.strip("/").split("/")
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=fj7lPtxQ0hLFlBv6uWwpvAPQdgoXjKNjaM1Tpn2SFKg,100
|
|
2
2
|
sourcecode/architecture_analyzer.py,sha256=zxgqeqn2FhIUDn06X-whjGKiXbrHFkRlzZcr2zKd0Gw,16874
|
|
3
|
-
sourcecode/architecture_summary.py,sha256=
|
|
4
|
-
sourcecode/classifier.py,sha256=
|
|
5
|
-
sourcecode/cli.py,sha256=
|
|
3
|
+
sourcecode/architecture_summary.py,sha256=rr-Q2LLe6bXZ27NuV2pYb0_LZyEFn3sngKMdXCZSGbE,13263
|
|
4
|
+
sourcecode/classifier.py,sha256=Ft_RfYS-KOe0t7vjgUx04OoCJd1-DXK7k9-I0CFDSnU,6934
|
|
5
|
+
sourcecode/cli.py,sha256=x_ZjTyy9IaBqCFXOLxwTvopB3OeHVCv2mk1s1E2Hc_g,29446
|
|
6
6
|
sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
|
|
7
7
|
sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
|
|
8
|
-
sourcecode/dependency_analyzer.py,sha256=
|
|
8
|
+
sourcecode/dependency_analyzer.py,sha256=C-G2paetrBUCMTA2KlWrD04fl9Y4q75sk4X0M0utzpE,50863
|
|
9
9
|
sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
|
|
10
10
|
sourcecode/env_analyzer.py,sha256=JZxBOuIxnM0xw0IaJFL6LiPJBErL848L3XoTOHGBqZI,13554
|
|
11
11
|
sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
|
|
@@ -13,33 +13,33 @@ sourcecode/graph_analyzer.py,sha256=cxl7Xb88aGxIVOCsatZJAE1CtgXGzIbJfqytV-9kkFs,
|
|
|
13
13
|
sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
|
|
14
14
|
sourcecode/prepare_context.py,sha256=yPRohYZ3wvX156GQSo1vYuoCDBIG3uy3G-3DdNbGHKU,19271
|
|
15
15
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
16
|
-
sourcecode/scanner.py,sha256=
|
|
17
|
-
sourcecode/schema.py,sha256=
|
|
16
|
+
sourcecode/scanner.py,sha256=aM3h9-DCQ3xKpeHpHYdo2vX6T5P95HA_YwZbkAVNwmo,8288
|
|
17
|
+
sourcecode/schema.py,sha256=YJhzdn5C9_S1OPWoMgn7gdtY9jgWfiESN-m9znBTjIg,15833
|
|
18
18
|
sourcecode/semantic_analyzer.py,sha256=asQfJf-EhzYaOTA-iMuZsrVXtbW7SV2WEKCxgsxa88Y,79413
|
|
19
|
-
sourcecode/serializer.py,sha256=
|
|
20
|
-
sourcecode/summarizer.py,sha256=
|
|
21
|
-
sourcecode/tree_utils.py,sha256=
|
|
19
|
+
sourcecode/serializer.py,sha256=Yqn-AOZL0kSFwfY0U72yOvVuYi7quPjyq6xXHbW_vc0,22462
|
|
20
|
+
sourcecode/summarizer.py,sha256=YdIA5gGEc0XSctPgmTnFnuZI72bprrOYhkJNoDmvHwM,12114
|
|
21
|
+
sourcecode/tree_utils.py,sha256=Fj9OIuUksBvgibNd3feog0sMDjVypJzPexp5lvMoYWI,1424
|
|
22
22
|
sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
|
|
23
23
|
sourcecode/detectors/__init__.py,sha256=A0AACJFF6HWf_RgatNtWu3PUzstcKtIGM9f1PoFcJug,1987
|
|
24
|
-
sourcecode/detectors/base.py,sha256=
|
|
24
|
+
sourcecode/detectors/base.py,sha256=C2EqfZudQ1ITK4DID4M70nPxqoh9bl1zn_ta6XRaGWs,1168
|
|
25
25
|
sourcecode/detectors/dart.py,sha256=QbqaL5v18-_ort75HihVBt8MsKUfOcFDF8IpWFLiXpI,1432
|
|
26
26
|
sourcecode/detectors/dotnet.py,sha256=UC-74VW850r3admcl36R-rl3M0-nBFXw8ph5e9RSas8,1978
|
|
27
27
|
sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJE,1713
|
|
28
28
|
sourcecode/detectors/go.py,sha256=KehAyvC3H_s8zwwqFM0bAbztQbbUBDjpNbGvM2kQlk8,1836
|
|
29
|
-
sourcecode/detectors/heuristic.py,sha256=
|
|
29
|
+
sourcecode/detectors/heuristic.py,sha256=253JB1iD0IXC72AdGRivQtZNgGziyOJOJrJeztOzdJs,1953
|
|
30
30
|
sourcecode/detectors/java.py,sha256=qFVpMy5mu490ENhutO1LV2Gidms4ZZUfNX5Qv5Qxh2I,7188
|
|
31
31
|
sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
|
|
32
|
-
sourcecode/detectors/nodejs.py,sha256=
|
|
32
|
+
sourcecode/detectors/nodejs.py,sha256=qNJ1m0E_TsPiR_iNcSgK4igb4mm99h-HDKE7C8gME6o,4762
|
|
33
33
|
sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
|
|
34
34
|
sourcecode/detectors/php.py,sha256=W_AQD0WMVDdWHa9h_ilX6W8XSpz0X4ctpMK2WXfXf1I,1887
|
|
35
|
-
sourcecode/detectors/project.py,sha256=
|
|
36
|
-
sourcecode/detectors/python.py,sha256=
|
|
35
|
+
sourcecode/detectors/project.py,sha256=YCynNC8drJutPMiDGPutlYaL49IO8M-1Ms_RwVY09ss,6221
|
|
36
|
+
sourcecode/detectors/python.py,sha256=EGhHxvgjSVAZujOswdrK7awuKj79bfYRKXI38pqpJs8,9504
|
|
37
37
|
sourcecode/detectors/ruby.py,sha256=Q4B5ePAw6-T4DLfanKJiuLHLqUigTPVrzylcXJMei3M,1591
|
|
38
38
|
sourcecode/detectors/rust.py,sha256=V23NO6Y8NsrhrUlGC2feUEcNK63hf3gc1y3jI4tes1Q,2264
|
|
39
39
|
sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
|
|
40
40
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
41
41
|
sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
|
|
42
|
-
sourcecode-0.
|
|
43
|
-
sourcecode-0.
|
|
44
|
-
sourcecode-0.
|
|
45
|
-
sourcecode-0.
|
|
42
|
+
sourcecode-0.22.0.dist-info/METADATA,sha256=R7dNRAnWzgeRSNAnDT78YOaThzkojXX0eKZXkyVD6Pw,30326
|
|
43
|
+
sourcecode-0.22.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
44
|
+
sourcecode-0.22.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
|
|
45
|
+
sourcecode-0.22.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|