sourcecode 0.21.0__tar.gz → 0.23.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.23.0}/PKG-INFO +1 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/docs/schema.md +2 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/pyproject.toml +1 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/architecture_summary.py +72 -23
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/classifier.py +3 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/cli.py +67 -29
- sourcecode-0.23.0/src/sourcecode/confidence_analyzer.py +190 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/dependency_analyzer.py +121 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/base.py +2 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/heuristic.py +11 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/nodejs.py +6 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/project.py +10 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/python.py +69 -14
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/prepare_context.py +216 -13
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/scanner.py +58 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/schema.py +30 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/serializer.py +261 -19
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/summarizer.py +25 -10
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/tree_utils.py +14 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_architecture_summary.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_analyzer_node_python.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detectors_base.py +2 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration.py +17 -7
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_dependencies.py +5 -5
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_docs.py +2 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_graph_modules.py +2 -6
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_lqn.py +6 -8
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_metrics.py +7 -7
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_real_projects.py +1 -1
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_schema.py +13 -11
- sourcecode-0.23.0/tests/test_signal_hierarchy.py +524 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_summarizer.py +1 -2
- {sourcecode-0.21.0 → sourcecode-0.23.0}/.gitignore +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/.ruff.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/README.md +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/__init__.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/conftest.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_cli.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.21.0 → sourcecode-0.23.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.23.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.23.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,29 @@ 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
|
-
#
|
|
620
|
-
|
|
626
|
+
# Build confidence summary + analysis gaps (always runs, lightweight)
|
|
627
|
+
from sourcecode.confidence_analyzer import ConfidenceAnalyzer
|
|
628
|
+
from dataclasses import replace as _replace
|
|
629
|
+
_conf_summary, _analysis_gaps = ConfidenceAnalyzer().analyze(sm)
|
|
630
|
+
sm = _replace(sm, confidence_summary=_conf_summary, analysis_gaps=_analysis_gaps)
|
|
631
|
+
|
|
632
|
+
# 4. Serializar
|
|
633
|
+
if agent:
|
|
634
|
+
data = agent_view(sm)
|
|
635
|
+
if not no_redact:
|
|
636
|
+
data = redact_dict(data)
|
|
637
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
638
|
+
elif compact:
|
|
621
639
|
data = compact_view(sm, no_tree=no_tree)
|
|
622
|
-
# Aplicar redaccion sobre el dict del compact view
|
|
623
640
|
if not no_redact:
|
|
624
641
|
data = redact_dict(data)
|
|
625
642
|
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
626
643
|
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)
|
|
644
|
+
raw_dict = standard_view(sm, include_tree=tree and not no_tree)
|
|
632
645
|
if not no_redact:
|
|
633
646
|
raw_dict = redact_dict(raw_dict)
|
|
634
647
|
|
|
635
648
|
if format == "yaml":
|
|
636
|
-
# Para YAML, serializar el dict directamente con ruamel.yaml
|
|
637
649
|
from io import StringIO
|
|
638
650
|
|
|
639
651
|
from ruamel.yaml import YAML
|
|
@@ -660,13 +672,18 @@ def main(
|
|
|
660
672
|
def prepare_context_cmd(
|
|
661
673
|
task: Optional[str] = typer.Argument(
|
|
662
674
|
None,
|
|
663
|
-
help="Task: explain | fix-bug | refactor | generate-tests",
|
|
675
|
+
help="Task: explain | fix-bug | refactor | generate-tests | onboard | review-pr | delta",
|
|
664
676
|
),
|
|
665
677
|
path: Path = typer.Option(
|
|
666
678
|
Path("."),
|
|
667
679
|
"--path", "-p",
|
|
668
680
|
help="Project directory to analyze (default: current directory)",
|
|
669
681
|
),
|
|
682
|
+
since: Optional[str] = typer.Option(
|
|
683
|
+
None,
|
|
684
|
+
"--since",
|
|
685
|
+
help="Git ref for delta task: show files changed since this ref (e.g. HEAD~3, main)",
|
|
686
|
+
),
|
|
670
687
|
llm_prompt: bool = typer.Option(
|
|
671
688
|
False,
|
|
672
689
|
"--llm-prompt",
|
|
@@ -683,13 +700,24 @@ def prepare_context_cmd(
|
|
|
683
700
|
help="Show what would be analyzed without running it",
|
|
684
701
|
),
|
|
685
702
|
) -> None:
|
|
686
|
-
"""
|
|
703
|
+
"""Compile task-aware context for AI coding agents.
|
|
704
|
+
|
|
705
|
+
\b
|
|
706
|
+
Tasks:
|
|
707
|
+
explain Project overview: structure, entry points, dependencies
|
|
708
|
+
fix-bug Risk-ranked files, suspected areas, code annotations
|
|
709
|
+
refactor Structural issues, improvement opportunities
|
|
710
|
+
generate-tests Untested source files, test gap analysis
|
|
711
|
+
onboard Full project context for a new agent or developer
|
|
712
|
+
review-pr PR review context: changed files + architecture
|
|
713
|
+
delta Incremental context: git-changed files only
|
|
687
714
|
|
|
688
715
|
\b
|
|
689
|
-
|
|
716
|
+
Examples:
|
|
690
717
|
sourcecode . prepare-context explain
|
|
691
718
|
sourcecode . prepare-context fix-bug --path /my/project
|
|
692
|
-
sourcecode . prepare-context
|
|
719
|
+
sourcecode . prepare-context delta --since main
|
|
720
|
+
sourcecode . prepare-context onboard --llm-prompt
|
|
693
721
|
sourcecode . prepare-context --task-help
|
|
694
722
|
"""
|
|
695
723
|
from sourcecode.prepare_context import TASKS, TaskContextBuilder
|
|
@@ -728,22 +756,28 @@ def prepare_context_cmd(
|
|
|
728
756
|
typer.echo(f"path: {target}")
|
|
729
757
|
typer.echo(f"analyzers: dependencies={'yes' if spec.enable_dependencies else 'no'}"
|
|
730
758
|
f", code_notes={'yes' if spec.enable_code_notes else 'no'}")
|
|
759
|
+
if since:
|
|
760
|
+
typer.echo(f"since: {since}")
|
|
731
761
|
typer.echo(f"output: {spec.output_hint}")
|
|
732
762
|
raise typer.Exit()
|
|
733
763
|
|
|
734
764
|
from dataclasses import asdict
|
|
735
765
|
|
|
736
766
|
builder = TaskContextBuilder(target)
|
|
737
|
-
output = builder.build(task)
|
|
767
|
+
output = builder.build(task, since=since)
|
|
738
768
|
|
|
739
769
|
out: dict[str, Any] = {
|
|
740
770
|
"task": output.task,
|
|
741
771
|
"goal": output.goal,
|
|
742
772
|
"project_summary": output.project_summary,
|
|
743
773
|
"architecture_summary": output.architecture_summary,
|
|
774
|
+
"confidence": output.confidence,
|
|
744
775
|
"relevant_files": [asdict(f) for f in output.relevant_files],
|
|
776
|
+
"why_these_files": output.why_these_files,
|
|
745
777
|
"key_dependencies": output.key_dependencies,
|
|
746
778
|
}
|
|
779
|
+
if output.gaps:
|
|
780
|
+
out["gaps"] = output.gaps
|
|
747
781
|
if output.suspected_areas:
|
|
748
782
|
out["suspected_areas"] = output.suspected_areas
|
|
749
783
|
if output.improvement_opportunities:
|
|
@@ -752,6 +786,10 @@ def prepare_context_cmd(
|
|
|
752
786
|
out["test_gaps"] = output.test_gaps
|
|
753
787
|
if output.code_notes_summary:
|
|
754
788
|
out["code_notes_summary"] = output.code_notes_summary
|
|
789
|
+
if output.changed_files:
|
|
790
|
+
out["changed_files"] = output.changed_files
|
|
791
|
+
if output.affected_entry_points:
|
|
792
|
+
out["affected_entry_points"] = output.affected_entry_points
|
|
755
793
|
if output.limitations:
|
|
756
794
|
out["limitations"] = output.limitations
|
|
757
795
|
if llm_prompt:
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""confidence_analyzer.py — Builds ConfidenceSummary and AnalysisGap list from SourceMap.
|
|
2
|
+
|
|
3
|
+
Analyzes detection quality post-facto:
|
|
4
|
+
- Classifies signals as hard (manifest/lockfile) vs soft (heuristic/extension)
|
|
5
|
+
- Identifies auxiliary paths that were found but correctly ignored
|
|
6
|
+
- Detects anomalies (conflicting signals, low-confidence detections)
|
|
7
|
+
- Produces structured analysis gaps for agent consumption
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import TYPE_CHECKING
|
|
14
|
+
|
|
15
|
+
from sourcecode.schema import AnalysisGap, ConfidenceSummary, SourceMap
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
_AUXILIARY_DIR_PREFIXES = (
|
|
21
|
+
".claude/", ".cursor/", ".vscode/", ".github/", ".idea/",
|
|
22
|
+
".devcontainer/", ".husky/",
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_FIXTURE_DIR_SEGMENTS = {"fixtures", "fixture", "testdata", "test_data", "__fixtures__"}
|
|
26
|
+
_TEST_DIR_SEGMENTS = {"tests", "test", "spec", "specs", "__tests__"}
|
|
27
|
+
_DOC_DIR_SEGMENTS = {"docs", "doc", "documentation", "wiki"}
|
|
28
|
+
_GENERATED_DIR_SEGMENTS = {"dist", "build", "target", "out", "output", ".next", "__pycache__"}
|
|
29
|
+
|
|
30
|
+
_HARD_SOURCES = {"manifest", "lockfile", "pyproject.toml", "package.json", "go.mod",
|
|
31
|
+
"Cargo.toml", "pom.xml", "build.gradle"}
|
|
32
|
+
_SOFT_SOURCES = {"heuristic", "code_signal", "convention"}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ConfidenceAnalyzer:
|
|
36
|
+
"""Analyzes SourceMap quality and produces confidence + gap metadata."""
|
|
37
|
+
|
|
38
|
+
def analyze(self, sm: SourceMap) -> tuple[ConfidenceSummary, list[AnalysisGap]]:
|
|
39
|
+
hard_signals: list[str] = []
|
|
40
|
+
soft_signals: list[str] = []
|
|
41
|
+
ignored_signals: list[str] = []
|
|
42
|
+
anomalies: list[str] = []
|
|
43
|
+
gaps: list[AnalysisGap] = []
|
|
44
|
+
|
|
45
|
+
# ── Stack signals ─────────────────────────────────────────────────────
|
|
46
|
+
for stack in sm.stacks:
|
|
47
|
+
if stack.detection_method == "manifest" and stack.confidence in ("high", "medium"):
|
|
48
|
+
for manifest in stack.manifests:
|
|
49
|
+
sig = f"stack:{stack.stack} via {manifest}"
|
|
50
|
+
if sig not in hard_signals:
|
|
51
|
+
hard_signals.append(sig)
|
|
52
|
+
elif stack.detection_method == "heuristic":
|
|
53
|
+
sig = f"stack:{stack.stack} (heuristic, no manifest)"
|
|
54
|
+
if sig not in soft_signals:
|
|
55
|
+
soft_signals.append(sig)
|
|
56
|
+
elif stack.detection_method == "lockfile":
|
|
57
|
+
sig = f"stack:{stack.stack} via lockfile"
|
|
58
|
+
if sig not in hard_signals:
|
|
59
|
+
hard_signals.append(sig)
|
|
60
|
+
|
|
61
|
+
# ── Entry point signals ───────────────────────────────────────────────
|
|
62
|
+
for ep in sm.entry_points:
|
|
63
|
+
if ep.source in _HARD_SOURCES or ep.reason == "console_script":
|
|
64
|
+
sig = f"entry:{ep.path} ({ep.reason or ep.source})"
|
|
65
|
+
if sig not in hard_signals:
|
|
66
|
+
hard_signals.append(sig)
|
|
67
|
+
else:
|
|
68
|
+
sig = f"entry:{ep.path} ({ep.reason or ep.source})"
|
|
69
|
+
if sig not in soft_signals:
|
|
70
|
+
soft_signals.append(sig)
|
|
71
|
+
|
|
72
|
+
# ── Ignored auxiliary paths ───────────────────────────────────────────
|
|
73
|
+
aux_dirs_found: set[str] = set()
|
|
74
|
+
for path in sm.file_paths:
|
|
75
|
+
norm = path.replace("\\", "/")
|
|
76
|
+
for prefix in _AUXILIARY_DIR_PREFIXES:
|
|
77
|
+
if norm.startswith(prefix):
|
|
78
|
+
top = prefix.rstrip("/")
|
|
79
|
+
aux_dirs_found.add(top)
|
|
80
|
+
break
|
|
81
|
+
|
|
82
|
+
for aux in sorted(aux_dirs_found):
|
|
83
|
+
ignored_signals.append(f"aux_dir:{aux} (tooling, not analyzed as project source)")
|
|
84
|
+
|
|
85
|
+
# ── Anomaly: multiple stacks, ambiguous primary ───────────────────────
|
|
86
|
+
primary_stacks = [s for s in sm.stacks if s.primary]
|
|
87
|
+
heuristic_only = [s for s in sm.stacks if s.detection_method == "heuristic"]
|
|
88
|
+
|
|
89
|
+
if len(primary_stacks) == 0 and sm.stacks:
|
|
90
|
+
anomalies.append("No primary stack marked — multiple stacks detected with equal weight")
|
|
91
|
+
if len(primary_stacks) > 1:
|
|
92
|
+
names = ", ".join(s.stack for s in primary_stacks)
|
|
93
|
+
anomalies.append(f"Multiple stacks marked as primary: {names}")
|
|
94
|
+
if heuristic_only and not any(s.detection_method != "heuristic" for s in sm.stacks):
|
|
95
|
+
anomalies.append("All stacks detected via heuristic only — no manifest found")
|
|
96
|
+
|
|
97
|
+
# ── Anomaly: entry points all low-confidence ──────────────────────────
|
|
98
|
+
if sm.entry_points and all(ep.confidence == "low" for ep in sm.entry_points):
|
|
99
|
+
anomalies.append("All entry points are low-confidence (heuristic/code_signal only)")
|
|
100
|
+
|
|
101
|
+
# ── Gaps ──────────────────────────────────────────────────────────────
|
|
102
|
+
if not sm.entry_points:
|
|
103
|
+
gaps.append(AnalysisGap(
|
|
104
|
+
area="entry_points",
|
|
105
|
+
reason="No entry point detected — project may use non-standard structure or be a library",
|
|
106
|
+
impact="high",
|
|
107
|
+
))
|
|
108
|
+
elif all(ep.confidence == "low" for ep in sm.entry_points):
|
|
109
|
+
gaps.append(AnalysisGap(
|
|
110
|
+
area="entry_points",
|
|
111
|
+
reason="Entry points inferred from code patterns only, no manifest declaration found",
|
|
112
|
+
impact="medium",
|
|
113
|
+
))
|
|
114
|
+
|
|
115
|
+
if not sm.stacks:
|
|
116
|
+
gaps.append(AnalysisGap(
|
|
117
|
+
area="stack",
|
|
118
|
+
reason="No stack detected — project may be infrastructure-only or use an unsupported language",
|
|
119
|
+
impact="high",
|
|
120
|
+
))
|
|
121
|
+
elif all(s.detection_method == "heuristic" for s in sm.stacks):
|
|
122
|
+
gaps.append(AnalysisGap(
|
|
123
|
+
area="stack",
|
|
124
|
+
reason="Stack inferred from file extensions only — no manifest or lockfile found",
|
|
125
|
+
impact="medium",
|
|
126
|
+
))
|
|
127
|
+
|
|
128
|
+
dep_summary = sm.dependency_summary
|
|
129
|
+
if dep_summary is None or not dep_summary.requested:
|
|
130
|
+
gaps.append(AnalysisGap(
|
|
131
|
+
area="dependencies",
|
|
132
|
+
reason="Dependencies not analyzed — run with --dependencies for full context",
|
|
133
|
+
impact="medium",
|
|
134
|
+
))
|
|
135
|
+
elif dep_summary.requested and dep_summary.total_count == 0:
|
|
136
|
+
gaps.append(AnalysisGap(
|
|
137
|
+
area="dependencies",
|
|
138
|
+
reason="No dependencies found — project may have no external dependencies or manifest is non-standard",
|
|
139
|
+
impact="low",
|
|
140
|
+
))
|
|
141
|
+
|
|
142
|
+
env_summary = sm.env_summary
|
|
143
|
+
if env_summary is None or not env_summary.requested:
|
|
144
|
+
gaps.append(AnalysisGap(
|
|
145
|
+
area="env",
|
|
146
|
+
reason="Environment variables not analyzed — run with --env-map for operational context",
|
|
147
|
+
impact="low",
|
|
148
|
+
))
|
|
149
|
+
|
|
150
|
+
# ── Compute overall confidence ─────────────────────────────────────────
|
|
151
|
+
# Stack: use best manifest-detected stack, fall back to min
|
|
152
|
+
manifest_stacks = [s for s in sm.stacks if s.detection_method != "heuristic"]
|
|
153
|
+
stack_conf = (
|
|
154
|
+
_max_confidence([s.confidence for s in manifest_stacks])
|
|
155
|
+
if manifest_stacks
|
|
156
|
+
else _min_confidence([s.confidence for s in sm.stacks] or ["low"])
|
|
157
|
+
)
|
|
158
|
+
# Entry points: use best available (highest-confidence EP wins)
|
|
159
|
+
ep_conf = _max_confidence([ep.confidence for ep in sm.entry_points] or ["low"])
|
|
160
|
+
overall = _min_confidence([stack_conf, ep_conf])
|
|
161
|
+
|
|
162
|
+
# Downgrade if gaps are severe
|
|
163
|
+
high_impact_gaps = [g for g in gaps if g.impact == "high"]
|
|
164
|
+
if high_impact_gaps:
|
|
165
|
+
overall = "low" if overall != "high" else "medium"
|
|
166
|
+
|
|
167
|
+
summary = ConfidenceSummary(
|
|
168
|
+
overall=overall, # type: ignore[arg-type]
|
|
169
|
+
stack_confidence=stack_conf, # type: ignore[arg-type]
|
|
170
|
+
entry_point_confidence=ep_conf, # type: ignore[arg-type]
|
|
171
|
+
hard_signals=hard_signals,
|
|
172
|
+
soft_signals=soft_signals,
|
|
173
|
+
ignored_signals=ignored_signals,
|
|
174
|
+
anomalies=anomalies,
|
|
175
|
+
)
|
|
176
|
+
return summary, gaps
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _min_confidence(values: list[str]) -> str:
|
|
180
|
+
rank = {"high": 2, "medium": 1, "low": 0}
|
|
181
|
+
if not values:
|
|
182
|
+
return "low"
|
|
183
|
+
return min(values, key=lambda v: rank.get(v, 0))
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _max_confidence(values: list[str]) -> str:
|
|
187
|
+
rank = {"high": 2, "medium": 1, "low": 0}
|
|
188
|
+
if not values:
|
|
189
|
+
return "low"
|
|
190
|
+
return max(values, key=lambda v: rank.get(v, 0))
|