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.
Files changed (111) hide show
  1. {sourcecode-0.21.0 → sourcecode-0.23.0}/PKG-INFO +1 -1
  2. {sourcecode-0.21.0 → sourcecode-0.23.0}/docs/schema.md +2 -2
  3. {sourcecode-0.21.0 → sourcecode-0.23.0}/pyproject.toml +1 -1
  4. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/architecture_summary.py +72 -23
  6. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/classifier.py +3 -1
  7. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/cli.py +67 -29
  8. sourcecode-0.23.0/src/sourcecode/confidence_analyzer.py +190 -0
  9. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/dependency_analyzer.py +121 -0
  10. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/base.py +2 -0
  11. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/heuristic.py +11 -1
  12. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/nodejs.py +6 -1
  13. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/project.py +10 -1
  14. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/python.py +69 -14
  15. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/prepare_context.py +216 -13
  16. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/scanner.py +58 -2
  17. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/schema.py +30 -0
  18. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/serializer.py +261 -19
  19. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/summarizer.py +25 -10
  20. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/tree_utils.py +14 -0
  21. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_architecture_summary.py +2 -2
  22. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_analyzer_node_python.py +2 -2
  23. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detectors_base.py +2 -0
  24. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration.py +17 -7
  25. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_dependencies.py +5 -5
  26. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_docs.py +2 -2
  27. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_graph_modules.py +2 -6
  28. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_lqn.py +6 -8
  29. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_metrics.py +7 -7
  30. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_real_projects.py +1 -1
  31. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_schema.py +13 -11
  32. sourcecode-0.23.0/tests/test_signal_hierarchy.py +524 -0
  33. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_summarizer.py +1 -2
  34. {sourcecode-0.21.0 → sourcecode-0.23.0}/.gitignore +0 -0
  35. {sourcecode-0.21.0 → sourcecode-0.23.0}/.ruff.toml +0 -0
  36. {sourcecode-0.21.0 → sourcecode-0.23.0}/README.md +0 -0
  37. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/architecture_analyzer.py +0 -0
  38. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  39. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/coverage_parser.py +0 -0
  40. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/__init__.py +0 -0
  41. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/dart.py +0 -0
  42. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/dotnet.py +0 -0
  43. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/elixir.py +0 -0
  44. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/go.py +0 -0
  45. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/java.py +0 -0
  46. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/parsers.py +0 -0
  48. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/php.py +0 -0
  49. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/env_analyzer.py +0 -0
  56. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/git_analyzer.py +0 -0
  57. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/graph_analyzer.py +0 -0
  58. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/metrics_analyzer.py +0 -0
  59. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/redactor.py +0 -0
  60. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/semantic_analyzer.py +0 -0
  61. {sourcecode-0.21.0 → sourcecode-0.23.0}/src/sourcecode/workspace.py +0 -0
  62. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/__init__.py +0 -0
  63. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/conftest.py +0 -0
  64. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/coverage.xml +0 -0
  65. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  66. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  67. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  68. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/go_service/go.mod +0 -0
  69. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/jacoco.xml +0 -0
  70. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/lcov.info +0 -0
  71. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  72. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/package.json +0 -0
  73. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  74. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  75. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  76. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  77. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  78. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  79. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_architecture_analyzer.py +0 -0
  80. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_classifier.py +0 -0
  81. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_cli.py +0 -0
  82. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_code_notes_analyzer.py +0 -0
  83. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_coverage_parser.py +0 -0
  84. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_cross_consistency.py +0 -0
  85. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  86. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_dependency_schema.py +0 -0
  87. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_go_rust_java.py +0 -0
  88. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_nodejs.py +0 -0
  89. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_php_ruby_dart.py +0 -0
  90. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_python.py +0 -0
  91. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_universal_managed.py +0 -0
  92. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_detector_universal_systems.py +0 -0
  93. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  94. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_doc_analyzer_python.py +0 -0
  95. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  96. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_analyzer_python_node.py +0 -0
  97. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_graph_schema.py +0 -0
  98. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_detection.py +0 -0
  99. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_multistack.py +0 -0
  100. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_semantics.py +0 -0
  101. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_integration_universal.py +0 -0
  102. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_metrics_analyzer.py +0 -0
  103. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_packaging.py +0 -0
  104. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_redactor.py +0 -0
  105. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_scanner.py +0 -0
  106. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_schema_normalization.py +0 -0
  107. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_analyzer_node.py +0 -0
  108. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_analyzer_python.py +0 -0
  109. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_import_resolution.py +0 -0
  110. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_semantic_schema.py +0 -0
  111. {sourcecode-0.21.0 → sourcecode-0.23.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.21.0
3
+ Version: 0.23.0
4
4
  Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -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.21.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.21.0",
648
+ "sourcecode_version": "0.23.0",
649
649
  "analyzed_path": "/abs/path/to/project"
650
650
  },
651
651
  "file_tree": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.21.0"
7
+ version = "0.23.0"
8
8
  description = "Genera un mapa de contexto estructurado de proyectos de software para agentes IA"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.21.0"
3
+ __version__ = "0.23.0"
@@ -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
- lines.extend(self._summarize_python_entry(entry_point.path, content))
70
+ lang_lines = self._summarize_python_entry(entry_point.path, content)
58
71
  elif suffix in _NODE_EXTENSIONS:
59
- lines.extend(self._summarize_node_entry(entry_point.path, content))
72
+ lang_lines = self._summarize_node_entry(entry_point.path, content)
60
73
  elif suffix in _GO_EXTENSIONS:
61
- lines.extend(self._summarize_go_entry(entry_point.path, content))
74
+ lang_lines = self._summarize_go_entry(entry_point.path, content)
62
75
  elif suffix in _JAVA_EXTENSIONS:
63
- lines.extend(self._summarize_java_entry(entry_point.path, content, sm.stacks))
76
+ lang_lines = self._summarize_java_entry(entry_point.path, content, sm.stacks)
64
77
  else:
65
- lines.append("Orquesta modulos internos no detallados por el analisis estatico disponible.")
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 ["Orquesta modulos Python, pero el entry point no pudo parsearse por completo."]
102
+ return []
82
103
 
83
104
  package_prefix = self._python_package_prefix(path)
84
105
  imported_modules: list[str] = []
85
- optional_analyzers: list[str] = []
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 == "sourcecode.serializer":
114
+ if node.module.endswith(".serializer") or "serializer" in node.module:
94
115
  uses_serializer = True
95
- if node.module == "sourcecode.redactor":
116
+ if node.module.endswith(".redactor") or "redactor" in node.module:
96
117
  uses_redactor = True
97
118
  elif isinstance(node, ast.Assign):
98
- optional = self._extract_optional_analyzer(node)
99
- if optional:
100
- optional_analyzers.append(optional)
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
- core_modules = self._format_module_list(imported_modules)
104
- if core_modules:
105
- lines.append(f"Orquesta modulos internos: {core_modules}.")
106
- if optional_analyzers:
107
- lines.append(f"Activa analisis opcionales: {', '.join(optional_analyzers)}.")
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
- output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta."
135
+ out = "Produce un SourceMap serializable en JSON/YAML"
110
136
  if uses_redactor:
111
- output_line = "Produce un SourceMap serializado en JSON/YAML y una vista compacta con redaccion de secretos."
112
- lines.append(output_line)
113
- elif not lines:
114
- lines.append("Orquesta modulos Python internos sin un patron de salida claramente inferible.")
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 or stack.stack == "nodejs":
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 (ideal con --dependencies en proyectos grandes)",
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="Incluir metricas de calidad: LOC, simbolos, complejidad, tests y cobertura por fichero",
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: selecciona automaticamente --compact --dependencies --env-map --code-notes y activa --no-tree en proyectos Java/Gradle o grandes",
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: auto-select flags based on project characteristics
243
+ # --agent: enable signal analyzers; output via agent_view (not compact)
239
244
  if agent:
240
- compact = True
241
- if _is_java:
242
- no_tree = True
243
- _agent_flags = ["--compact", "--dependencies", "--env-map", "--code-notes"]
244
- if no_tree:
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
- return (0 if d.ecosystem == primary_ecosystem else 1, d.name.lower())
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
- # 4. Serializar (con o sin modo compact)
620
- if compact:
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
- # Redactar sobre el dict serializado (SEC-01, SEC-03)
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
- """Prepare task-aware context optimized for LLM reasoning.
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
- Note: PATH must be provided before the subcommand (default: '.'):
716
+ Examples:
690
717
  sourcecode . prepare-context explain
691
718
  sourcecode . prepare-context fix-bug --path /my/project
692
- sourcecode . prepare-context generate-tests --llm-prompt
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))