sourcecode 0.41.0__tar.gz → 0.43.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 (148) hide show
  1. {sourcecode-0.41.0 → sourcecode-0.43.0}/PKG-INFO +1 -1
  2. {sourcecode-0.41.0 → sourcecode-0.43.0}/pyproject.toml +1 -1
  3. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/architecture_analyzer.py +94 -8
  5. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/cli.py +28 -0
  6. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/contract_model.py +1 -0
  7. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/contract_pipeline.py +49 -14
  8. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/doc_analyzer.py +22 -0
  9. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/env_analyzer.py +110 -22
  10. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/git_analyzer.py +13 -2
  11. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/prepare_context.py +6 -2
  12. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/schema.py +29 -0
  13. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/semantic_analyzer.py +64 -0
  14. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/serializer.py +44 -7
  15. sourcecode-0.43.0/tests/test_block1_reliability.py +474 -0
  16. sourcecode-0.43.0/tests/test_block2_coverage.py +449 -0
  17. sourcecode-0.43.0/tests/test_block5_quality.py +302 -0
  18. {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  19. {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  20. {sourcecode-0.41.0 → sourcecode-0.43.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  21. {sourcecode-0.41.0 → sourcecode-0.43.0}/.gitignore +0 -0
  22. {sourcecode-0.41.0 → sourcecode-0.43.0}/.ruff.toml +0 -0
  23. {sourcecode-0.41.0 → sourcecode-0.43.0}/CONTRIBUTING.md +0 -0
  24. {sourcecode-0.41.0 → sourcecode-0.43.0}/LICENSE +0 -0
  25. {sourcecode-0.41.0 → sourcecode-0.43.0}/README.md +0 -0
  26. {sourcecode-0.41.0 → sourcecode-0.43.0}/SECURITY.md +0 -0
  27. {sourcecode-0.41.0 → sourcecode-0.43.0}/docs/privacy.md +0 -0
  28. {sourcecode-0.41.0 → sourcecode-0.43.0}/docs/schema.md +0 -0
  29. {sourcecode-0.41.0 → sourcecode-0.43.0}/raw +0 -0
  30. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/adaptive_scanner.py +0 -0
  31. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/architecture_summary.py +0 -0
  32. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/ast_extractor.py +0 -0
  33. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/classifier.py +0 -0
  34. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  35. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/confidence_analyzer.py +0 -0
  36. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/context_summarizer.py +0 -0
  37. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/coverage_parser.py +0 -0
  38. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/dependency_analyzer.py +0 -0
  39. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/__init__.py +0 -0
  40. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/base.py +0 -0
  41. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  42. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dart.py +0 -0
  43. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/dotnet.py +0 -0
  44. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/elixir.py +0 -0
  45. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/go.py +0 -0
  46. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/heuristic.py +0 -0
  47. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/hybrid.py +0 -0
  48. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/java.py +0 -0
  49. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  50. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/nodejs.py +0 -0
  51. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/parsers.py +0 -0
  52. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/php.py +0 -0
  53. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/project.py +0 -0
  54. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/python.py +0 -0
  55. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/ruby.py +0 -0
  56. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/rust.py +0 -0
  57. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/systems.py +0 -0
  58. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/terraform.py +0 -0
  59. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/detectors/tooling.py +0 -0
  60. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  61. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/file_classifier.py +0 -0
  62. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/graph_analyzer.py +0 -0
  63. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/metrics_analyzer.py +0 -0
  64. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/ranking_engine.py +0 -0
  65. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/redactor.py +0 -0
  66. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/relevance_scorer.py +0 -0
  67. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/repo_classifier.py +0 -0
  68. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/runtime_classifier.py +0 -0
  69. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/scanner.py +0 -0
  70. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/summarizer.py +0 -0
  71. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/__init__.py +0 -0
  72. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/config.py +0 -0
  73. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/consent.py +0 -0
  74. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/events.py +0 -0
  75. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/filters.py +0 -0
  76. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/telemetry/transport.py +0 -0
  77. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/tree_utils.py +0 -0
  78. {sourcecode-0.41.0 → sourcecode-0.43.0}/src/sourcecode/workspace.py +0 -0
  79. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/__init__.py +0 -0
  80. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/conftest.py +0 -0
  81. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/coverage.xml +0 -0
  82. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  83. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  84. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  85. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/go_service/go.mod +0 -0
  86. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/jacoco.xml +0 -0
  87. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/lcov.info +0 -0
  88. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  89. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/package.json +0 -0
  90. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  91. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  92. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  93. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  94. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  95. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  96. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_architecture_analyzer.py +0 -0
  97. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_architecture_summary.py +0 -0
  98. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_ast_extractor.py +0 -0
  99. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_classifier.py +0 -0
  100. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_cli.py +0 -0
  101. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_code_notes_analyzer.py +0 -0
  102. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_contract_pipeline.py +0 -0
  103. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_coverage_parser.py +0 -0
  104. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_cross_consistency.py +0 -0
  105. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  106. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  107. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_dependency_schema.py +0 -0
  108. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_dotnet.py +0 -0
  109. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_go_rust_java.py +0 -0
  110. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_nodejs.py +0 -0
  111. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_php_ruby_dart.py +0 -0
  112. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_python.py +0 -0
  113. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_universal_managed.py +0 -0
  114. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detector_universal_systems.py +0 -0
  115. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_detectors_base.py +0 -0
  116. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  117. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_doc_analyzer_python.py +0 -0
  118. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  119. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_analyzer_python_node.py +0 -0
  120. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_graph_schema.py +0 -0
  121. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_hybrid_inference.py +0 -0
  122. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration.py +0 -0
  123. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_dependencies.py +0 -0
  124. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_detection.py +0 -0
  125. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_docs.py +0 -0
  126. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_graph_modules.py +0 -0
  127. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_lqn.py +0 -0
  128. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_metrics.py +0 -0
  129. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_multistack.py +0 -0
  130. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_semantics.py +0 -0
  131. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_integration_universal.py +0 -0
  132. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_metrics_analyzer.py +0 -0
  133. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_packaging.py +0 -0
  134. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_phase1_improvements.py +0 -0
  135. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_pipeline_integrity.py +0 -0
  136. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_real_projects.py +0 -0
  137. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_redactor.py +0 -0
  138. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_scanner.py +0 -0
  139. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_schema.py +0 -0
  140. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_schema_normalization.py +0 -0
  141. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_node.py +0 -0
  142. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_analyzer_python.py +0 -0
  143. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_import_resolution.py +0 -0
  144. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_semantic_schema.py +0 -0
  145. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_signal_hierarchy.py +0 -0
  146. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_summarizer.py +0 -0
  147. {sourcecode-0.41.0 → sourcecode-0.43.0}/tests/test_telemetry.py +0 -0
  148. {sourcecode-0.41.0 → sourcecode-0.43.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.41.0
3
+ Version: 0.43.0
4
4
  Summary: Deterministic codebase context for AI coding agents
5
5
  License: Apache License
6
6
  Version 2.0, January 2004
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.41.0"
7
+ version = "0.43.0"
8
8
  description = "Deterministic codebase context for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "0.41.0"
3
+ __version__ = "0.43.0"
@@ -172,6 +172,7 @@ class ArchitectureAnalyzer:
172
172
  graph: Optional[ModuleGraph] = None,
173
173
  ) -> ArchitectureAnalysis:
174
174
  limitations: list[str] = []
175
+ evidence: list[dict] = []
175
176
 
176
177
  # Step 1: filter paths
177
178
  filtered = self._filter_paths(sm.file_paths)
@@ -180,6 +181,8 @@ class ArchitectureAnalyzer:
180
181
  requested=True,
181
182
  pattern="unknown",
182
183
  limitations=["Arquitectura no inferida: proyecto sin archivos de codigo suficientes"],
184
+ evidence=[{"type": "none", "paths": [], "reason": "insufficient source files", "confidence": "high"}],
185
+ tentative=False,
183
186
  )
184
187
 
185
188
  # Step 2: domain clustering
@@ -193,17 +196,32 @@ class ArchitectureAnalyzer:
193
196
  elif pattern == "unknown":
194
197
  limitations.append("Patron de capas no reconocido: estructura de directorios sin senales claras")
195
198
 
196
- # Step 3b: monorepo override — workspace config is hard evidence
197
- if self._has_workspace_config(sm.file_paths) and pattern not in (
199
+ # Step 3b: monorepo override — workspace config is hard evidence.
200
+ # Overrides all weak inferred patterns; only truly specialised patterns
201
+ # (cqrs, clean, onion, hexagonal) take precedence over workspace config.
202
+ has_workspace = self._has_workspace_config(sm.file_paths)
203
+ if has_workspace and pattern not in (
198
204
  "monorepo", "cqrs", "clean", "onion", "hexagonal"
199
205
  ):
200
206
  mono_layers = self._detect_monorepo_packages(filtered)
201
- if mono_layers or pattern in (None, "unknown", "flat", "modular", "layered"):
207
+ # Override whenever: monorepo packages detected, OR pattern is any weak/generic type.
208
+ # "fullstack", "layered", "mvc", "microservices", "modular", "flat", "unknown", None
209
+ # all yield to workspace config evidence.
210
+ _WEAK_PATTERNS = {None, "unknown", "flat", "modular", "layered",
211
+ "fullstack", "mvc", "microservices"}
212
+ if mono_layers or pattern in _WEAK_PATTERNS:
202
213
  pattern = "monorepo"
203
214
  layers = mono_layers
204
215
  limitations.append(
205
216
  "Workspace config detectado — arquitectura refleja topologia de paquetes"
206
217
  )
218
+ ws_files = [p for p in sm.file_paths if p.split("/")[-1] in _WORKSPACE_CONFIG_FILES]
219
+ evidence.append({
220
+ "type": "workspace_config",
221
+ "paths": ws_files[:4],
222
+ "reason": "Monorepo workspace config file(s) detected — hard evidence for monorepo topology",
223
+ "confidence": "high",
224
+ })
207
225
 
208
226
  # Step 4: bounded context inference
209
227
  bounded_contexts = self._infer_bounded_contexts(domains, graph)
@@ -212,25 +230,91 @@ class ArchitectureAnalyzer:
212
230
  confidence: Literal["high", "medium", "low"]
213
231
  strong_domains = [d for d in domains if d.confidence in ("high", "medium")]
214
232
  all_layers_weak = layers and all(l.confidence == "low" for l in layers)
233
+
234
+ method = "graph+structure" if graph is not None else "filesystem_inference"
235
+ # High-confidence evidence (workspace config) makes pattern non-tentative.
236
+ tentative = not any(e.get("confidence") == "high" for e in evidence)
237
+
238
+ # _hard_evidence: high-confidence evidence was already set (e.g. workspace_config).
239
+ # When True, tentative must stay False and confidence must stay at least "medium".
240
+ _hard_evidence = not tentative # tentative=False iff high-conf evidence present
241
+
215
242
  if pattern not in (None, "unknown", "flat"):
216
- if all_layers_weak:
243
+ if graph is not None:
244
+ # Import graph provided — structural validation available
245
+ confidence = "medium" if len(strong_domains) >= 3 else "low"
246
+ evidence.append({
247
+ "type": "import_graph",
248
+ "paths": [n.id for n in graph.nodes[:6]],
249
+ "reason": f"Module import graph with {len(graph.nodes)} nodes used for pattern validation",
250
+ "confidence": "medium",
251
+ })
252
+ elif all_layers_weak:
217
253
  # Layers came from file-naming heuristic only, not directory structure
218
254
  confidence = "low"
255
+ if not _hard_evidence:
256
+ tentative = True
219
257
  limitations.append(
220
258
  "Low confidence inference: pattern inferred from filenames only, without import graph confirmation"
221
259
  )
260
+ evidence.append({
261
+ "type": "filesystem_naming",
262
+ "paths": [l.files[0] for l in layers if l.files][:6],
263
+ "reason": (
264
+ f"Pattern '{pattern}' inferred from file stem naming conventions only "
265
+ "(e.g. *_controller.py, *_service.py). "
266
+ "No directory structure or import graph confirmation."
267
+ ),
268
+ "confidence": "low",
269
+ })
222
270
  else:
223
- confidence = "medium" if len(strong_domains) >= 3 else "low"
224
- if graph is None:
271
+ # Directory structure match (or monorepo/workspace override with no layers)
272
+ confidence = "medium" if (_hard_evidence or len(strong_domains) >= 3) else "low"
273
+ if confidence == "low" and not _hard_evidence:
274
+ tentative = True
275
+ if not _hard_evidence:
225
276
  limitations.append(
226
277
  "Pattern not confirmed by module import graph; run with --graph-modules for structural validation"
227
278
  )
279
+ if not _hard_evidence:
280
+ matched_dirs = sorted({
281
+ p.replace("\\", "/").split("/")[0]
282
+ for layer in layers for p in layer.files
283
+ })
284
+ evidence.append({
285
+ "type": "filesystem_naming",
286
+ "paths": matched_dirs[:8],
287
+ "reason": (
288
+ f"Pattern '{pattern}' inferred from directory names matching layer keywords. "
289
+ "Import graph not available — structural direction of dependencies unverified."
290
+ ),
291
+ "confidence": "low" if confidence == "low" else "medium",
292
+ })
228
293
  elif len(strong_domains) >= 1:
229
294
  confidence = "medium"
295
+ if not _hard_evidence:
296
+ tentative = True
297
+ evidence.append({
298
+ "type": "filesystem_naming",
299
+ "paths": [d.name for d in strong_domains[:6]],
300
+ "reason": "Domain clustering from directory names; no layer pattern confirmed",
301
+ "confidence": "low",
302
+ })
230
303
  else:
231
304
  confidence = "low"
232
-
233
- method = "graph+structure" if graph is not None else "filesystem_inference"
305
+ if not _hard_evidence:
306
+ tentative = True
307
+ if not evidence:
308
+ limitations.append(
309
+ "insufficient_evidence: no recognizable architectural signals found; "
310
+ "filesystem structure does not match known patterns"
311
+ )
312
+ evidence.append({
313
+ "type": "filesystem_naming",
314
+ "paths": filtered[:6],
315
+ "reason": "Only filesystem paths available; no pattern matched",
316
+ "confidence": "low",
317
+ })
234
318
 
235
319
  return ArchitectureAnalysis(
236
320
  requested=True,
@@ -241,6 +325,8 @@ class ArchitectureAnalyzer:
241
325
  confidence=confidence,
242
326
  method=method,
243
327
  limitations=limitations,
328
+ evidence=evidence,
329
+ tentative=tentative,
244
330
  )
245
331
 
246
332
  # ------------------------------------------------------------------
@@ -181,6 +181,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
181
181
  "--dependency-depth",
182
182
  "--rank-by",
183
183
  "--symbol",
184
+ "--max-importers",
184
185
  })
185
186
 
186
187
 
@@ -594,6 +595,17 @@ def main(
594
595
  "--symbol",
595
596
  help="Contract mode: extract localized context for a specific symbol name. Returns defining file + all importers.",
596
597
  ),
598
+ max_importers: int = typer.Option(
599
+ 50,
600
+ "--max-importers",
601
+ help=(
602
+ "Maximum importer files returned by --symbol (default: 50). "
603
+ "Popular symbols can have hundreds of importers — this prevents output explosion. "
604
+ "Defining files are never truncated. Override: --symbol Foo --max-importers 200."
605
+ ),
606
+ min=1,
607
+ max=10000,
608
+ ),
597
609
  copy: bool = typer.Option(
598
610
  False,
599
611
  "--copy",
@@ -770,6 +782,21 @@ def main(
770
782
  code_notes = True
771
783
  no_tree = True # agents never need the raw file tree
772
784
  typer.echo("[agent] dependencies env-map code-notes (no-tree)", err=True)
785
+ # Warn about flags that are computed but excluded from agent_view output
786
+ _agent_suppressed: list[str] = []
787
+ if full_metrics:
788
+ _agent_suppressed.append("--full-metrics")
789
+ if graph_modules:
790
+ _agent_suppressed.append("--graph-modules")
791
+ if docs:
792
+ _agent_suppressed.append("--docs")
793
+ if _agent_suppressed:
794
+ typer.echo(
795
+ f"[agent] warning: {', '.join(_agent_suppressed)} computed but excluded "
796
+ "from --agent output — agent_view does not include these sections. "
797
+ "Remove these flags to skip unnecessary computation.",
798
+ err=True,
799
+ )
773
800
 
774
801
  scanner = AdaptiveScanner(target, topology=_topology, base_depth=effective_depth)
775
802
  raw_tree = scanner.scan_tree()
@@ -1343,6 +1370,7 @@ def main(
1343
1370
  changed_only=changed_only,
1344
1371
  symbol=symbol,
1345
1372
  compress_types=compress_types,
1373
+ max_importers=max_importers,
1346
1374
  )
1347
1375
  sm = _replace(sm, file_contracts=_contracts, contract_summary=_contract_summary)
1348
1376
  if symbol is not None and len(_contracts) == 0:
@@ -109,3 +109,4 @@ class ContractSummary:
109
109
  method_breakdown: dict[str, int] = field(default_factory=dict)
110
110
  ranked_by: str = "relevance"
111
111
  limitations: list[str] = field(default_factory=list)
112
+ symbol_truncation: Optional[dict] = None # set when --symbol truncates importers
@@ -45,9 +45,10 @@ def _get_changed_files(root: Path) -> set[str]:
45
45
  ]:
46
46
  try:
47
47
  result = subprocess.run(
48
- cmd, cwd=root, capture_output=True, text=True, timeout=10
48
+ cmd, cwd=root, capture_output=True, text=True,
49
+ encoding="utf-8", errors="replace", timeout=10,
49
50
  )
50
- for line in result.stdout.splitlines():
51
+ for line in (result.stdout or "").splitlines():
51
52
  line = line.strip()
52
53
  if line:
53
54
  changed.add(line.replace("\\", "/"))
@@ -56,9 +57,10 @@ def _get_changed_files(root: Path) -> set[str]:
56
57
  try:
57
58
  result = subprocess.run(
58
59
  ["git", "status", "--porcelain"],
59
- cwd=root, capture_output=True, text=True, timeout=10
60
+ cwd=root, capture_output=True, text=True,
61
+ encoding="utf-8", errors="replace", timeout=10,
60
62
  )
61
- for line in result.stdout.splitlines():
63
+ for line in (result.stdout or "").splitlines():
62
64
  if len(line) > 3:
63
65
  changed.add(line[3:].strip().replace("\\", "/"))
64
66
  except Exception:
@@ -129,11 +131,12 @@ def _get_git_churn(root: Path, file_paths: list[str]) -> dict[str, int]:
129
131
  try:
130
132
  result = subprocess.run(
131
133
  ["git", "log", "--name-only", "--format=", "--since=90.days.ago"],
132
- cwd=root, capture_output=True, text=True, timeout=15,
134
+ cwd=root, capture_output=True, text=True,
135
+ encoding="utf-8", errors="replace", timeout=15,
133
136
  )
134
137
  path_set = set(file_paths)
135
138
  counter: Counter[str] = Counter()
136
- for line in result.stdout.splitlines():
139
+ for line in (result.stdout or "").splitlines():
137
140
  line = line.strip().replace("\\", "/")
138
141
  if line in path_set:
139
142
  counter[line] += 1
@@ -172,6 +175,7 @@ class ContractPipeline:
172
175
  changed_only: bool = False,
173
176
  symbol: Optional[str] = None,
174
177
  compress_types: bool = False,
178
+ max_importers: int = 50,
175
179
  ) -> tuple[list[FileContract], ContractSummary]:
176
180
  """Run the full extraction pipeline.
177
181
 
@@ -276,17 +280,19 @@ class ContractPipeline:
276
280
  contracts = self._rank(contracts, rank_by)
277
281
 
278
282
  # 8. Symbol filter — keep files that define or import the symbol
283
+ _symbol_truncation: Optional[dict] = None
279
284
  if symbol:
280
- contracts = _filter_by_symbol(contracts, symbol)
285
+ contracts, _symbol_truncation = _filter_by_symbol(contracts, symbol, max_importers=max_importers)
281
286
  # When shallow scan missed the defining file (deep monorepo), fall back
282
287
  # to a grep-based filesystem search over the full directory tree.
283
288
  if not contracts:
284
- contracts = self._symbol_deep_scan(
289
+ contracts, _symbol_truncation = self._symbol_deep_scan(
285
290
  root, symbol,
286
291
  known_paths=set(src_paths),
287
292
  entry_paths=entry_paths,
288
293
  changed_files=changed_files,
289
294
  engine=engine,
295
+ max_importers=max_importers,
290
296
  )
291
297
 
292
298
  # 9. Entrypoints-only filter
@@ -310,6 +316,7 @@ class ContractPipeline:
310
316
  method_breakdown=dict(method_counts),
311
317
  ranked_by=rank_by,
312
318
  limitations=limitations,
319
+ symbol_truncation=_symbol_truncation,
313
320
  )
314
321
  return contracts, summary
315
322
 
@@ -329,7 +336,8 @@ class ContractPipeline:
329
336
  entry_paths: set[str],
330
337
  changed_files: set[str],
331
338
  engine: RankingEngine,
332
- ) -> list[FileContract]:
339
+ max_importers: int = 50,
340
+ ) -> tuple[list[FileContract], dict]:
333
341
  """Grep-based fallback when the shallow scan missed the defining files.
334
342
 
335
343
  Searches the full directory tree for source files containing *symbol*,
@@ -353,7 +361,7 @@ class ContractPipeline:
353
361
  contract.ranking_reasons = fs.reasons
354
362
  extra.append(contract)
355
363
 
356
- return _filter_by_symbol(extra, symbol)
364
+ return _filter_by_symbol(extra, symbol, max_importers=max_importers)
357
365
 
358
366
 
359
367
  # ---------------------------------------------------------------------------
@@ -409,7 +417,11 @@ def _limit_symbols(contracts: list[FileContract], max_symbols: int) -> list[File
409
417
  # Symbol-aware filter
410
418
  # ---------------------------------------------------------------------------
411
419
 
412
- def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileContract]:
420
+ def _filter_by_symbol(
421
+ contracts: list[FileContract],
422
+ symbol: str,
423
+ max_importers: int = 50,
424
+ ) -> tuple[list[FileContract], dict]:
413
425
  """Return contracts that define, import, or structurally reference *symbol*.
414
426
 
415
427
  Four tiers applied in order:
@@ -420,6 +432,8 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
420
432
  function signatures (word-boundary). Only used when tiers 1-3 fail.
421
433
 
422
434
  Defining contracts are ranked first; importers and references follow.
435
+ max_importers caps tier 3 results to prevent output explosion on popular symbols.
436
+ Returns (contracts, truncation_metadata).
423
437
  """
424
438
  sym_l = symbol.lower()
425
439
  word_re = re.compile(
@@ -463,8 +477,14 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
463
477
 
464
478
  # Tier 3: import matching (case-insensitive when no definers found)
465
479
  ci_imports = len(defining) == 0
466
- importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
467
- importers = [c for c in contracts if c.path in importer_paths and c.path not in defining_paths]
480
+ all_importer_paths = {c.path for c in contracts if _imports_sym(c, case=ci_imports)}
481
+ all_importers = [c for c in contracts if c.path in all_importer_paths and c.path not in defining_paths]
482
+
483
+ # Apply importer cap — definers are never truncated
484
+ total_importers = len(all_importers)
485
+ truncated = total_importers > max_importers
486
+ importers = all_importers[:max_importers] if truncated else all_importers
487
+ importer_paths = {c.path for c in importers}
468
488
 
469
489
  # Tier 4: type-reference matching (only when tiers 1-3 yield nothing)
470
490
  references: list[FileContract] = []
@@ -480,12 +500,27 @@ def _filter_by_symbol(contracts: list[FileContract], symbol: str) -> list[FileCo
480
500
  seen.add(c.path)
481
501
  merged.append(c)
482
502
 
483
- return sorted(merged, key=lambda c: (
503
+ result = sorted(merged, key=lambda c: (
484
504
  c.path not in defining_paths,
485
505
  c.path not in importer_paths,
486
506
  -c.relevance_score,
487
507
  ))
488
508
 
509
+ truncation: dict = {
510
+ "symbol": symbol,
511
+ "definers_found": len(defining),
512
+ "importers_found": total_importers,
513
+ "importers_returned": len(importers),
514
+ "references_found": len(references),
515
+ "total_returned": len(result),
516
+ "truncated": truncated,
517
+ }
518
+ if truncated:
519
+ truncation["truncation_reason"] = "max_importers_limit"
520
+ truncation["override_hint"] = f"--symbol {symbol} --max-importers {total_importers}"
521
+
522
+ return result, truncation
523
+
489
524
 
490
525
  # ---------------------------------------------------------------------------
491
526
  # Deep symbol scan — grep-based fallback for shallow-scanned repos
@@ -132,6 +132,8 @@ class DocAnalyzer:
132
132
  records: list[DocRecord] = []
133
133
  limitations: list[str] = list(limitations_pre)
134
134
  languages: set[str] = set()
135
+ # Track per-language support status for honest reporting
136
+ unsupported_langs: set[str] = set()
135
137
 
136
138
  for relative_path in file_paths:
137
139
  abs_path = root / relative_path
@@ -176,8 +178,18 @@ class DocAnalyzer:
176
178
  # Unsupported language — D-04: no emitir DocRecord, solo registrar limitation
177
179
  limitations.append(f"docs_unavailable:{norm_path}:language={lang}")
178
180
  languages.add(lang)
181
+ unsupported_langs.add(lang)
179
182
  # NO records.append() here
180
183
 
184
+ # Build language_coverage: explicit per-language support status
185
+ _SUPPORTED_LANGS = {"python", "javascript", "typescript"}
186
+ lang_coverage: dict[str, str] = {}
187
+ for lang in languages:
188
+ if lang in _SUPPORTED_LANGS:
189
+ lang_coverage[lang] = "supported"
190
+ else:
191
+ lang_coverage[lang] = "unsupported"
192
+
181
193
  # Build summary
182
194
  symbol_count = sum(1 for r in records if r.kind != "module")
183
195
  total_count = len(records)
@@ -192,6 +204,15 @@ class DocAnalyzer:
192
204
  "no docstrings or JSDoc comments found"
193
205
  )
194
206
 
207
+ # Warn explicitly when unsupported languages are present — agents must not
208
+ # assume full coverage when Java/Go/Rust files are in scope but not analyzed.
209
+ if unsupported_langs:
210
+ sorted_unsupported = sorted(unsupported_langs)
211
+ limitations.append(
212
+ f"docs_not_extracted: language(s) {sorted_unsupported} present but not supported; "
213
+ "only Python and JS/TS docstrings are extracted"
214
+ )
215
+
195
216
  summary = DocSummary(
196
217
  requested=True,
197
218
  total_count=total_count,
@@ -200,6 +221,7 @@ class DocAnalyzer:
200
221
  depth=depth,
201
222
  truncated=truncated,
202
223
  limitations=limitations,
224
+ language_coverage=lang_coverage,
203
225
  )
204
226
  return records, summary
205
227