sourcecode 0.21.0__tar.gz → 0.22.0__tar.gz
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-0.21.0 → sourcecode-0.22.0}/PKG-INFO +1 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/docs/schema.md +2 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/pyproject.toml +1 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/architecture_summary.py +72 -23
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/classifier.py +3 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/cli.py +30 -24
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/dependency_analyzer.py +75 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/base.py +2 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/heuristic.py +2 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/nodejs.py +6 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/project.py +10 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/python.py +63 -14
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/scanner.py +58 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/schema.py +1 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/serializer.py +187 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/summarizer.py +25 -10
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/tree_utils.py +14 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_architecture_summary.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_dependency_analyzer_node_python.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration.py +12 -5
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_dependencies.py +5 -5
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_docs.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_graph_modules.py +2 -6
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_lqn.py +6 -8
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_metrics.py +7 -7
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_real_projects.py +1 -1
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_summarizer.py +1 -2
- {sourcecode-0.21.0 → sourcecode-0.22.0}/.gitignore +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/.ruff.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/README.md +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/__init__.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/conftest.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_cli.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.22.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -81,7 +81,7 @@ Campos:
|
|
|
81
81
|
{
|
|
82
82
|
"schema_version": "1.0",
|
|
83
83
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
84
|
-
"sourcecode_version": "0.
|
|
84
|
+
"sourcecode_version": "0.22.0",
|
|
85
85
|
"analyzed_path": "/abs/path/to/project"
|
|
86
86
|
}
|
|
87
87
|
```
|
|
@@ -645,7 +645,7 @@ Ejemplo de salida para un monorepo con web Node.js y API Python con `--dependenc
|
|
|
645
645
|
"metadata": {
|
|
646
646
|
"schema_version": "1.0",
|
|
647
647
|
"generated_at": "2026-04-07T19:41:05.686277+00:00",
|
|
648
|
-
"sourcecode_version": "0.
|
|
648
|
+
"sourcecode_version": "0.22.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -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]
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -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
|
|
@@ -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")
|
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -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():
|