sourcecode 1.3.0__tar.gz → 1.5.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 (177) hide show
  1. {sourcecode-1.3.0 → sourcecode-1.5.0}/PKG-INFO +1 -1
  2. {sourcecode-1.3.0 → sourcecode-1.5.0}/pyproject.toml +1 -1
  3. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/architecture_analyzer.py +12 -1
  5. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/ast_extractor.py +178 -40
  6. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/cli.py +44 -10
  7. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/contract_model.py +7 -1
  8. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/contract_pipeline.py +18 -2
  9. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/dependency_analyzer.py +54 -1
  10. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/java.py +131 -10
  11. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/project.py +33 -5
  12. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/tooling.py +5 -0
  13. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/graph_analyzer.py +20 -67
  14. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/schema.py +11 -0
  15. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/serializer.py +91 -1
  16. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_contract_pipeline.py +4 -4
  17. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_analyzer_polyglot.py +4 -1
  18. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration.py +2 -1
  19. {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
  20. {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
  21. {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
  22. {sourcecode-1.3.0 → sourcecode-1.5.0}/.github/workflows/build-windows.yml +0 -0
  23. {sourcecode-1.3.0 → sourcecode-1.5.0}/.gitignore +0 -0
  24. {sourcecode-1.3.0 → sourcecode-1.5.0}/.ruff.toml +0 -0
  25. {sourcecode-1.3.0 → sourcecode-1.5.0}/CONTRIBUTING.md +0 -0
  26. {sourcecode-1.3.0 → sourcecode-1.5.0}/LICENSE +0 -0
  27. {sourcecode-1.3.0 → sourcecode-1.5.0}/README.md +0 -0
  28. {sourcecode-1.3.0 → sourcecode-1.5.0}/SECURITY.md +0 -0
  29. {sourcecode-1.3.0 → sourcecode-1.5.0}/docs/privacy.md +0 -0
  30. {sourcecode-1.3.0 → sourcecode-1.5.0}/docs/schema.md +0 -0
  31. {sourcecode-1.3.0 → sourcecode-1.5.0}/raw +0 -0
  32. {sourcecode-1.3.0 → sourcecode-1.5.0}/run_cli.py +0 -0
  33. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/adaptive_scanner.py +0 -0
  34. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/architecture_summary.py +0 -0
  35. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/classifier.py +0 -0
  36. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  37. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/confidence_analyzer.py +0 -0
  38. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/context_scorer.py +0 -0
  39. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/context_summarizer.py +0 -0
  40. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/coverage_parser.py +0 -0
  41. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/__init__.py +0 -0
  42. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/base.py +0 -0
  43. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  44. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/dart.py +0 -0
  45. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/dotnet.py +0 -0
  46. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/elixir.py +0 -0
  47. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/go.py +0 -0
  48. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/heuristic.py +0 -0
  49. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/hybrid.py +0 -0
  50. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  51. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/nodejs.py +0 -0
  52. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/parsers.py +0 -0
  53. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/php.py +0 -0
  54. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/python.py +0 -0
  55. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/ruby.py +0 -0
  56. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/rust.py +0 -0
  57. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/systems.py +0 -0
  58. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/terraform.py +0 -0
  59. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/doc_analyzer.py +0 -0
  60. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  61. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/env_analyzer.py +0 -0
  62. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/git_analyzer.py +0 -0
  64. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/metrics_analyzer.py +0 -0
  65. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/prepare_context.py +0 -0
  66. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/ranking_engine.py +0 -0
  67. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/redactor.py +0 -0
  68. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/relevance_scorer.py +0 -0
  69. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/repo_classifier.py +0 -0
  70. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/runtime_classifier.py +0 -0
  71. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/scanner.py +0 -0
  72. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/semantic_analyzer.py +0 -0
  73. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/summarizer.py +0 -0
  74. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/__init__.py +0 -0
  75. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/config.py +0 -0
  76. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/consent.py +0 -0
  77. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/events.py +0 -0
  78. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/filters.py +0 -0
  79. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/transport.py +0 -0
  80. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/tree_utils.py +0 -0
  81. {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/workspace.py +0 -0
  82. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/__init__.py +0 -0
  83. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/conftest.py +0 -0
  84. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/coverage.xml +0 -0
  85. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  86. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  87. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  88. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/go_service/go.mod +0 -0
  89. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/jacoco.xml +0 -0
  90. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/lcov.info +0 -0
  91. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  92. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/package.json +0 -0
  93. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  94. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  95. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  96. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  97. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  98. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  99. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
  100. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/application/service/FindAusenteService.java +0 -0
  101. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/domain/entities/Ausente.java +0 -0
  102. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/ausente/infrastructure/rest/AusenteRestController.java +0 -0
  103. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/application/service/FindAutocoberturasService.java +0 -0
  104. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/domain/entities/Autocoberturas.java +0 -0
  105. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/autocoberturas/infrastructure/rest/AutocoberturasRestController.java +0 -0
  106. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/application/service/FindCalendarioTrabajadorService.java +0 -0
  107. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/domain/entities/CalendarioTrabajador.java +0 -0
  108. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/calendarioTrabajador/infrastructure/rest/CalendarioTrabajadorRestController.java +0 -0
  109. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/application/service/FindDepartamentoService.java +0 -0
  110. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/domain/entities/Departamento.java +0 -0
  111. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/departamento/infrastructure/rest/DepartamentoRestController.java +0 -0
  112. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/application/service/FindEmpleadoService.java +0 -0
  113. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/domain/entities/Empleado.java +0 -0
  114. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/ddd/empleado/infrastructure/rest/EmpleadoRestController.java +0 -0
  115. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
  116. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
  117. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
  118. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
  119. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
  120. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
  121. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
  122. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
  123. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_architecture_analyzer.py +0 -0
  124. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_architecture_summary.py +0 -0
  125. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_ast_extractor.py +0 -0
  126. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block1_reliability.py +0 -0
  127. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block2_coverage.py +0 -0
  128. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block5_quality.py +0 -0
  129. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_classifier.py +0 -0
  130. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_cli.py +0 -0
  131. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_code_notes_analyzer.py +0 -0
  132. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_context_scorer.py +0 -0
  133. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_coverage_parser.py +0 -0
  134. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_cross_consistency.py +0 -0
  135. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_analyzer_node_python.py +0 -0
  136. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
  137. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_schema.py +0 -0
  138. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_dotnet.py +0 -0
  139. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_go_rust_java.py +0 -0
  140. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_nodejs.py +0 -0
  141. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_php_ruby_dart.py +0 -0
  142. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_python.py +0 -0
  143. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_universal_managed.py +0 -0
  144. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_universal_systems.py +0 -0
  145. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detectors_base.py +0 -0
  146. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  147. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_doc_analyzer_python.py +0 -0
  148. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_analyzer_python_node.py +0 -0
  149. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_schema.py +0 -0
  150. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_hybrid_inference.py +0 -0
  151. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_dependencies.py +0 -0
  152. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_detection.py +0 -0
  153. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_docs.py +0 -0
  154. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_graph_modules.py +0 -0
  155. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_lqn.py +0 -0
  156. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_metrics.py +0 -0
  157. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_multistack.py +0 -0
  158. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_semantics.py +0 -0
  159. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_universal.py +0 -0
  160. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_java_spring_integration.py +0 -0
  161. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_metrics_analyzer.py +0 -0
  162. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_packaging.py +0 -0
  163. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_phase1_improvements.py +0 -0
  164. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_pipeline_integrity.py +0 -0
  165. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_real_projects.py +0 -0
  166. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_redactor.py +0 -0
  167. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_scanner.py +0 -0
  168. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_schema.py +0 -0
  169. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_schema_normalization.py +0 -0
  170. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_analyzer_node.py +0 -0
  171. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_analyzer_python.py +0 -0
  172. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_import_resolution.py +0 -0
  173. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_schema.py +0 -0
  174. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_signal_hierarchy.py +0 -0
  175. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_summarizer.py +0 -0
  176. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_telemetry.py +0 -0
  177. {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_workspace_analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.3.0
3
+ Version: 1.5.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 = "1.3.0"
7
+ version = "1.5.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__ = "1.3.0"
3
+ __version__ = "1.5.0"
@@ -182,8 +182,19 @@ class ArchitectureAnalyzer:
182
182
  ddd_result = self._detect_ddd(sm.file_paths)
183
183
  if ddd_result is not None:
184
184
  ddd_pattern, ddd_layers, ddd_contexts, ddd_layer_names = ddd_result
185
- domains_for_ddd = self._cluster_domains(filtered) if len(filtered) >= 2 else []
186
185
  module_files = self._build_ddd_module_files(sm.file_paths, ddd_contexts)
186
+ # Use DDD bounded context names as domains so --architecture shows each
187
+ # context as a distinct domain instead of collapsing all files under
188
+ # the Maven path segment (e.g. "java").
189
+ domains_for_ddd = [
190
+ ArchitectureDomain(
191
+ name=n,
192
+ files=module_files.get(n, []),
193
+ role="DDD bounded context",
194
+ confidence="high",
195
+ )
196
+ for n in ddd_contexts
197
+ ]
187
198
  bc_list = [
188
199
  BoundedContext(name=n, modules=module_files.get(n, []), confidence="high")
189
200
  for n in ddd_contexts
@@ -940,19 +940,51 @@ def _extract_python(path: str, source: str) -> FileContract:
940
940
 
941
941
 
942
942
  # ---------------------------------------------------------------------------
943
- # Minimal Java extraction (regex-based, no AST)
943
+ # ---------------------------------------------------------------------------
944
+ # Enhanced Java extraction (regex-based, annotation-aware)
944
945
  # ---------------------------------------------------------------------------
945
946
 
946
- _JAVA_CLASS_DECL_RE = re.compile(
947
- r'public\s+(?:(?:abstract|final|static)\s+)*(class|interface|enum)\s+(\w+)'
948
- r'(?:\s+extends\s+([\w.]+))?(?:\s+implements\s+([\w.,\s]+?))?(?=\s*[\{<])',
949
- re.MULTILINE,
947
+ _JAVA_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([^;\s]+)\s*;', re.MULTILINE)
948
+
949
+ # Single annotation on one line (captures name + optional parens args)
950
+ _JAVA_ANNO_LINE_RE = re.compile(r'^\s*(@[\w.]+(?:\s*\([^)]*\))?)\s*$')
951
+ # Class/interface/enum declaration line (public or package-private)
952
+ _JAVA_CLASS_LINE_RE = re.compile(
953
+ r'(?:public\s+)?(?:(?:abstract|final|static|sealed)\s+)*'
954
+ r'(class|interface|enum|@interface)\s+(\w+)'
955
+ r'(?:\s+extends\s+([\w.]+))?'
956
+ r'(?:\s+implements\s+([\w.,\s<>]+?))?'
957
+ r'(?=\s*[\{<])'
950
958
  )
951
- _JAVA_METHOD_SIG_RE = re.compile(
952
- r'^\s{0,12}public\s+[^\{]+\(',
959
+ # Public method: up to 12 leading spaces, return type, name, open paren
960
+ _JAVA_PUB_METHOD_LINE_RE = re.compile(
961
+ r'^\s{0,12}public\s+(?:(?:static|final|synchronized|abstract|default)\s+)*'
962
+ r'[\w<>\[\]?,\s]+?\s+(\w+)\s*\(',
953
963
  re.MULTILINE,
954
964
  )
955
- _JAVA_IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([^;\s]+)\s*;', re.MULTILINE)
965
+ # @Autowired or @Inject field
966
+ _JAVA_FIELD_DECL_RE = re.compile(
967
+ r'^\s*(?:private|protected|public)?\s*'
968
+ r'([\w<>.,\[\]? ]+?)\s+(\w+)\s*[;=]'
969
+ )
970
+
971
+
972
+ def _java_collect_preceding_annotations(lines: list[str], decl_idx: int) -> list[str]:
973
+ """Walk back from decl_idx and collect @annotation lines immediately before it."""
974
+ annotations: list[str] = []
975
+ i = decl_idx - 1
976
+ while i >= 0:
977
+ stripped = lines[i].strip()
978
+ if not stripped or stripped.startswith("//") or stripped.startswith("*"):
979
+ i -= 1
980
+ continue
981
+ m = re.match(r'(@[\w.]+(?:\s*\([^)]*\))?)', stripped)
982
+ if m:
983
+ annotations.insert(0, m.group(1))
984
+ i -= 1
985
+ else:
986
+ break
987
+ return annotations
956
988
 
957
989
 
958
990
  def _extract_java(path: str, source: str) -> FileContract:
@@ -960,35 +992,93 @@ def _extract_java(path: str, source: str) -> FileContract:
960
992
  types: list[TypeDefinition] = []
961
993
  functions: list[FunctionSignature] = []
962
994
  imports: list[ImportRecord] = []
995
+ autowired_fields: list[dict] = []
963
996
 
964
- # Class / interface / enum declarations
965
- for m in _JAVA_CLASS_DECL_RE.finditer(source):
966
- name = m.group(2)
967
- extends_str = m.group(3)
968
- implements_str = m.group(4)
969
- all_extends: list[str] = []
970
- if extends_str:
971
- all_extends.append(extends_str.strip())
972
- if implements_str:
973
- all_extends.extend(i.strip() for i in implements_str.split(",") if i.strip())
974
- types.append(TypeDefinition(name=name, kind="class", fields=[], extends=all_extends))
975
- exports.append(ExportRecord(name=name, kind="class"))
976
-
977
- class_names = {t.name for t in types}
978
-
979
- # Public method signatures (one-line heuristic)
997
+ lines = source.splitlines()
998
+
999
+ # Pass 1: collect imports
1000
+ seen_sources: set[str] = set()
1001
+ for m in _JAVA_IMPORT_RE.finditer(source):
1002
+ full_import = m.group(1).strip()
1003
+ if full_import not in seen_sources:
1004
+ seen_sources.add(full_import)
1005
+ imports.append(ImportRecord(source=full_import, kind="named", symbols=[]))
1006
+
1007
+ # Pass 2: line-by-line scan for classes, methods, @Autowired fields
1008
+ class_names: set[str] = set()
980
1009
  seen_methods: set[str] = set()
981
- for m in _JAVA_METHOD_SIG_RE.finditer(source):
982
- sig_text = m.group(0).strip()
983
- name_match = re.search(r'(\w+)\s*\($', sig_text)
984
- if not name_match:
985
- name_match = re.search(r'(\w+)\s*\(', sig_text)
986
- if not name_match:
1010
+ autowired_pending = False
1011
+
1012
+ for idx, line in enumerate(lines):
1013
+ stripped = line.strip()
1014
+
1015
+ # Detect @Autowired / @Inject annotations
1016
+ if stripped.startswith("@Autowired") or stripped.startswith("@Inject"):
1017
+ autowired_pending = True
987
1018
  continue
988
- mname = name_match.group(1)
989
- if mname in class_names or mname in seen_methods or mname in {"if", "for", "while", "switch"}:
1019
+
1020
+ # Capture autowired field on next non-annotation, non-blank line
1021
+ if autowired_pending and stripped and not stripped.startswith("@"):
1022
+ autowired_pending = False
1023
+ fm = _JAVA_FIELD_DECL_RE.match(line)
1024
+ if fm:
1025
+ type_name = fm.group(1).strip().split()[-1] # last word = simple type
1026
+ field_name = fm.group(2).strip()
1027
+ if field_name and type_name and field_name not in {"class", "interface"}:
1028
+ autowired_fields.append({"type": type_name, "name": field_name})
1029
+ elif stripped and not stripped.startswith("@"):
1030
+ autowired_pending = False
1031
+
1032
+ # Class/interface/enum declaration
1033
+ cm = _JAVA_CLASS_LINE_RE.search(stripped)
1034
+ if cm and ("class " in stripped or "interface " in stripped or "enum " in stripped):
1035
+ kind_kw = cm.group(1) # class | interface | enum | @interface
1036
+ name = cm.group(2)
1037
+ extends_str = cm.group(3)
1038
+ implements_str = cm.group(4)
1039
+
1040
+ annotations = _java_collect_preceding_annotations(lines, idx)
1041
+ all_extends: list[str] = []
1042
+ if extends_str:
1043
+ all_extends.append(extends_str.strip())
1044
+
1045
+ implements_list: list[str] = []
1046
+ if implements_str:
1047
+ implements_list = [i.strip() for i in implements_str.split(",") if i.strip()]
1048
+
1049
+ export_kind = "class" if kind_kw in ("class", "@interface") else kind_kw
1050
+ exports.append(ExportRecord(
1051
+ name=name,
1052
+ kind=export_kind,
1053
+ annotations=annotations,
1054
+ extends=extends_str.strip() if extends_str else None,
1055
+ implements=implements_list,
1056
+ ))
1057
+ types.append(TypeDefinition(
1058
+ name=name,
1059
+ kind=export_kind,
1060
+ fields=[],
1061
+ extends=all_extends,
1062
+ ))
1063
+ class_names.add(name)
1064
+
1065
+ # Pass 3: public methods with their preceding annotations
1066
+ for m in _JAVA_PUB_METHOD_LINE_RE.finditer(source):
1067
+ sig_text = m.group(0).strip()
1068
+ mname = m.group(1)
1069
+ if (mname in class_names or mname in seen_methods
1070
+ or mname in {"if", "for", "while", "switch", "return", "new"}):
990
1071
  continue
991
1072
  seen_methods.add(mname)
1073
+ # Find line index for this match to collect preceding annotations
1074
+ line_start = source.count("\n", 0, m.start())
1075
+ annotations = _java_collect_preceding_annotations(lines, line_start)
1076
+ exports.append(ExportRecord(
1077
+ name=mname,
1078
+ kind="method",
1079
+ annotations=annotations,
1080
+ signature=sig_text,
1081
+ ))
992
1082
  functions.append(FunctionSignature(
993
1083
  name=mname,
994
1084
  signature=sig_text,
@@ -997,14 +1087,6 @@ def _extract_java(path: str, source: str) -> FileContract:
997
1087
  return_type=None,
998
1088
  ))
999
1089
 
1000
- # Import statements
1001
- seen_sources: set[str] = set()
1002
- for m in _JAVA_IMPORT_RE.finditer(source):
1003
- full_import = m.group(1).strip()
1004
- if full_import not in seen_sources:
1005
- seen_sources.add(full_import)
1006
- imports.append(ImportRecord(source=full_import, kind="named", symbols=[]))
1007
-
1008
1090
  # External deps: top-2 package segments, skip java.* / javax.*
1009
1091
  deps = sorted({
1010
1092
  ".".join(imp.source.split(".")[:2])
@@ -1021,6 +1103,7 @@ def _extract_java(path: str, source: str) -> FileContract:
1021
1103
  functions=sorted(functions, key=lambda f: f.name)[:20],
1022
1104
  types=sorted(types, key=lambda t: t.name),
1023
1105
  dependencies=deps[:20],
1106
+ autowired_fields=autowired_fields[:20],
1024
1107
  extraction_method="heuristic",
1025
1108
  )
1026
1109
 
@@ -1089,6 +1172,51 @@ def _detect_role(path: str, contract: FileContract) -> str:
1089
1172
  return "util"
1090
1173
 
1091
1174
 
1175
+ # ---------------------------------------------------------------------------
1176
+ # MyBatis XML mapper extractor
1177
+ # ---------------------------------------------------------------------------
1178
+
1179
+ def _extract_mybatis_xml(rel_path: str, source: str) -> FileContract:
1180
+ """Extract namespace and SQL operations from a MyBatis *Mapper.xml file."""
1181
+ import re as _re
1182
+ from xml.etree import ElementTree
1183
+
1184
+ _NS_STRIP = _re.compile(r"\{[^}]+\}")
1185
+ _SQL_OPS = frozenset({"select", "insert", "update", "delete"})
1186
+
1187
+ exports: list[ExportRecord] = []
1188
+ namespace: str | None = None
1189
+
1190
+ try:
1191
+ root_elem = ElementTree.fromstring(source.encode("utf-8"))
1192
+ namespace = root_elem.get("namespace") or None
1193
+ for elem in root_elem:
1194
+ tag = _NS_STRIP.sub("", elem.tag).lower()
1195
+ if tag in _SQL_OPS:
1196
+ op_id = (elem.get("id") or "").strip()
1197
+ if op_id:
1198
+ # type_ref carries select/insert/update/delete for the serializer
1199
+ exports.append(ExportRecord(kind="query", name=op_id, type_ref=tag))
1200
+ except Exception:
1201
+ return FileContract(
1202
+ path=rel_path,
1203
+ language="mybatis-xml",
1204
+ role="mybatis-mapper",
1205
+ extraction_method="heuristic",
1206
+ limitations=["xml_parse_error: failed to parse mapper XML"],
1207
+ )
1208
+
1209
+ deps = [f"namespace:{namespace}"] if namespace else []
1210
+ return FileContract(
1211
+ path=rel_path,
1212
+ language="mybatis-xml",
1213
+ role="mybatis-mapper",
1214
+ exports=exports,
1215
+ dependencies=deps,
1216
+ extraction_method="heuristic",
1217
+ )
1218
+
1219
+
1092
1220
  # ---------------------------------------------------------------------------
1093
1221
  # AstExtractor public class
1094
1222
  # ---------------------------------------------------------------------------
@@ -1108,6 +1236,16 @@ class AstExtractor:
1108
1236
  return self._ts_ok
1109
1237
 
1110
1238
  def extract(self, path: Path, root: Optional[Path] = None) -> Optional[FileContract]:
1239
+ # MyBatis mapper XML — handled before the language map lookup so .xml
1240
+ # files are only processed when they match the mapper naming convention.
1241
+ if path.suffix.lower() == ".xml" and path.name.endswith("Mapper.xml"):
1242
+ try:
1243
+ source = path.read_text(encoding="utf-8", errors="replace")
1244
+ except OSError:
1245
+ return None
1246
+ rel_path = str(path.relative_to(root)).replace("\\", "/") if root else path.name
1247
+ return _extract_mybatis_xml(rel_path, source)
1248
+
1111
1249
  ext = path.suffix.lower()
1112
1250
  language = _LANGUAGE_MAP.get(ext)
1113
1251
  if language is None:
@@ -728,15 +728,13 @@ def main(
728
728
  mode = "contract" # unknown → safe default
729
729
 
730
730
  # Legacy flags imply raw mode unless --mode was explicitly overridden.
731
- # These flags produce standard_view-only output sections not in contract_view.
732
- # Preserves backward compat: callers using any legacy flag get their previous format.
733
- # New callers opt into contract mode via --mode contract (or bare invocation).
734
- # Legacy flags that produce output sections incompatible with contract_view
735
- # force mode to raw. --agent is excluded: it now runs the contract pipeline
736
- # and enriches contract_view with auto-enabled analyzers (deps, env, notes).
731
+ # --format yaml and --graph-modules are now compatible with contract_view:
732
+ # yaml is a serialization format (not an output-section flag)
733
+ # graph-modules output is included in contract_view when available
734
+ # Other flags that produce sections exclusive to standard_view still force raw.
737
735
  _legacy_flags_active = (
738
- compact or tree or format == "yaml" or trace_pipeline
739
- or docs or semantics or graph_modules or full_metrics or architecture
736
+ compact or tree or trace_pipeline
737
+ or docs or semantics or full_metrics or architecture
740
738
  )
741
739
  if mode in ("contract", "standard") and _legacy_flags_active:
742
740
  mode = "raw"
@@ -1106,6 +1104,17 @@ def main(
1106
1104
  metrics_summary=metrics_summary,
1107
1105
  )
1108
1106
 
1107
+ # Populate Java-specific root fields from java stack detection (FIX-6, 7, 8)
1108
+ _java_stack = next((s for s in stacks if s.stack == "java"), None)
1109
+ if _java_stack is not None:
1110
+ from dataclasses import replace as _dc_replace
1111
+ sm = _dc_replace(sm,
1112
+ packaging=getattr(_java_stack, "packaging", None) or None,
1113
+ language_version=getattr(_java_stack, "language_version", None) or None,
1114
+ spring_profiles=getattr(_java_stack, "spring_profiles", []) or [],
1115
+ app_server_hint=getattr(_java_stack, "app_server_hint", None) or None,
1116
+ )
1117
+
1109
1118
  # Semantic analysis (--semantics flag)
1110
1119
  if semantic_analyzer is not None:
1111
1120
  if workspace_analysis.workspaces:
@@ -1402,7 +1411,16 @@ def main(
1402
1411
  if _is_contract_mode:
1403
1412
  from sourcecode.contract_pipeline import ContractPipeline
1404
1413
  from sourcecode.contract_model import ContractSummary as _ContractSummary
1405
- _cp = ContractPipeline()
1414
+ # FIX-1: Java projects need higher caps — many files, comprehensive coverage required
1415
+ _jvm_stacks = {"java", "kotlin", "scala", "groovy"}
1416
+ _is_jvm = any(s.stack in _jvm_stacks for s in sm.stacks)
1417
+ # FIX-1: Java projects need higher caps and no relevance threshold
1418
+ _max_files_cp = 2500 if _is_jvm else 500
1419
+ _cp = ContractPipeline(max_files=_max_files_cp)
1420
+ _java_pipeline_kwargs: dict = {}
1421
+ if _is_jvm:
1422
+ _java_pipeline_kwargs["max_contracts"] = 500
1423
+ _java_pipeline_kwargs["min_score"] = 0.0
1406
1424
  try:
1407
1425
  _contracts, _contract_summary = _cp.run(
1408
1426
  target,
@@ -1420,6 +1438,7 @@ def main(
1420
1438
  max_importers=max_importers,
1421
1439
  semantic_calls=sm.semantic_calls or None,
1422
1440
  code_notes=sm.code_notes or None,
1441
+ **_java_pipeline_kwargs,
1423
1442
  )
1424
1443
  except Exception as _exc:
1425
1444
  typer.echo(f"[error] contract pipeline failed: {_exc}", err=True)
@@ -1462,7 +1481,22 @@ def main(
1462
1481
  data = _contract_view(sm, emit_graph=emit_graph, depth=_depth)
1463
1482
  if not no_redact:
1464
1483
  data = redact_dict(data)
1465
- content = json.dumps(data, indent=2, ensure_ascii=False)
1484
+ if format == "yaml":
1485
+ from io import StringIO
1486
+ from ruamel.yaml import YAML as _YAML
1487
+ _yaml = _YAML()
1488
+ _yaml.default_flow_style = False
1489
+ _yaml.representer.add_representer(
1490
+ type(None),
1491
+ lambda dumper, data_val: dumper.represent_scalar(
1492
+ "tag:yaml.org,2002:null", "null"
1493
+ ),
1494
+ )
1495
+ _stream = StringIO()
1496
+ _yaml.dump(data, _stream)
1497
+ content = _stream.getvalue()
1498
+ else:
1499
+ content = json.dumps(data, indent=2, ensure_ascii=False)
1466
1500
  elif agent:
1467
1501
  data = agent_view(sm)
1468
1502
  # When contract pipeline ran (mode=contract, no legacy flags), include
@@ -25,9 +25,13 @@ class ExportRecord:
25
25
  """Exported symbol."""
26
26
 
27
27
  name: str
28
- kind: str = "unknown" # function | class | const | type | default | react_component | enum | interface
28
+ kind: str = "unknown" # function | class | const | type | default | react_component | enum | interface | method
29
29
  type_ref: Optional[str] = None
30
30
  async_: bool = False
31
+ annotations: list[str] = field(default_factory=list) # Java: ["@Controller", "@Transactional"]
32
+ extends: Optional[str] = None # Java: parent class
33
+ implements: list[str] = field(default_factory=list) # Java: interfaces
34
+ signature: Optional[str] = None # Java method: full signature
31
35
 
32
36
 
33
37
  @dataclass
@@ -96,6 +100,8 @@ class FileContract:
96
100
  # Extraction quality
97
101
  extraction_method: str = "heuristic" # ast | tree_sitter | heuristic
98
102
  limitations: list[str] = field(default_factory=list)
103
+ # Java-specific (FIX-1)
104
+ autowired_fields: list[dict] = field(default_factory=list) # [{"type": "...", "name": "..."}]
99
105
 
100
106
 
101
107
  @dataclass
@@ -182,6 +182,7 @@ class ContractPipeline:
182
182
  semantic_calls: Optional[list] = None,
183
183
  code_notes: Optional[list] = None,
184
184
  max_contracts: Optional[int] = _MAX_CONTRACTS,
185
+ min_score: Optional[float] = None,
185
186
  ) -> tuple[list[FileContract], ContractSummary]:
186
187
  """Run the full extraction pipeline.
187
188
 
@@ -218,9 +219,18 @@ class ContractPipeline:
218
219
  fname = Path(pn).name
219
220
  return any(fname.startswith(pat) or f".{pat.strip('.')}" in fname for pat in _TEST_PATTERNS)
220
221
 
222
+ def _is_extractable(p: str) -> bool:
223
+ suf = Path(p).suffix.lower()
224
+ if suf in _SRC_EXTENSIONS:
225
+ return True
226
+ # MyBatis mapper XML files — only *Mapper.xml, not all XML
227
+ if suf == ".xml" and p.endswith("Mapper.xml"):
228
+ return True
229
+ return False
230
+
221
231
  src_paths = [
222
232
  p for p in file_paths
223
- if Path(p).suffix.lower() in _SRC_EXTENSIONS
233
+ if _is_extractable(p)
224
234
  and not scorer.is_noise(p)
225
235
  and (symbol is not None or changed_only or not _is_test(p))
226
236
  ]
@@ -317,11 +327,17 @@ class ContractPipeline:
317
327
  # 10. Top-N cap — enforce max_contracts when not in symbol-search mode.
318
328
  # Symbol searches must return all matching files; budget applies only to
319
329
  # the default architectural briefing use case.
330
+ _effective_min_score = min_score if min_score is not None else _MIN_CONTRACT_SCORE
320
331
  if symbol is None and max_contracts is not None:
321
332
  contracts = [
322
333
  c for c in contracts
323
- if c.relevance_score >= _MIN_CONTRACT_SCORE or c.is_entrypoint
334
+ if c.relevance_score >= _effective_min_score or c.is_entrypoint
324
335
  ][:max_contracts]
336
+ elif symbol is None and max_contracts is None:
337
+ contracts = [
338
+ c for c in contracts
339
+ if c.relevance_score >= _effective_min_score or c.is_entrypoint
340
+ ]
325
341
 
326
342
  # 11. Compress types if requested
327
343
  if compress_types:
@@ -128,6 +128,8 @@ def _infer_role(name: str, ecosystem: str, scope: str) -> str:
128
128
  return "runtime"
129
129
 
130
130
  if ecosystem == "java":
131
+ if scope == "provided":
132
+ return "provided"
131
133
  artifact = n.split(":")[-1] if ":" in n else n
132
134
  if any(x in artifact for x in ("spring-boot", "spring-security")):
133
135
  return "runtime"
@@ -1120,6 +1122,23 @@ class DependencyAnalyzer:
1120
1122
  properties = self._parse_maven_properties(root_elem, ns)
1121
1123
  dm_versions = self._parse_dependency_management(root_elem, ns, properties)
1122
1124
 
1125
+ # FIX-9: extract parent version for BOM resolution
1126
+ parent_elem = root_elem.find(f"{ns}parent")
1127
+ parent_version: Optional[str] = None
1128
+ parent_group: str = ""
1129
+ if parent_elem is not None:
1130
+ parent_version = (parent_elem.findtext(f"{ns}version") or "").strip() or None
1131
+ parent_group = (parent_elem.findtext(f"{ns}groupId") or "").strip()
1132
+ parent_artifact = (parent_elem.findtext(f"{ns}artifactId") or "").strip()
1133
+ # Propagate parent version into properties for ${project.parent.version}
1134
+ if parent_version:
1135
+ properties.setdefault("project.parent.version", parent_version)
1136
+ properties.setdefault("revision", parent_version)
1137
+
1138
+ # Infer packaging for FIX-6 (used by scope hint for provided)
1139
+ packaging_elem = root_elem.find(f"{ns}packaging")
1140
+ is_war = packaging_elem is not None and (packaging_elem.text or "").strip().lower() == "war"
1141
+
1123
1142
  records: list[DependencyRecord] = []
1124
1143
  deps_elem = root_elem.find(f"{ns}dependencies")
1125
1144
  if deps_elem is None:
@@ -1134,14 +1153,36 @@ class DependencyAnalyzer:
1134
1153
  declared = self._resolve_maven_version(version_raw, properties)
1135
1154
  if declared is None:
1136
1155
  declared = dm_versions.get(f"{group_id}:{artifact_id}")
1156
+
1157
+ # FIX-4: proper maven scope mapping
1137
1158
  scope_text = (dep.findtext(f"{ns}scope") or "compile").strip().lower()
1138
- scope = "dev" if scope_text == "test" else "direct"
1159
+ if scope_text == "test":
1160
+ scope = "dev"
1161
+ elif scope_text == "provided":
1162
+ scope = "provided"
1163
+ else:
1164
+ scope = "direct" # compile, runtime, system, import
1165
+
1166
+ # FIX-4: infer provided for embedded tomcat in WAR projects
1167
+ if (is_war and scope == "direct"
1168
+ and artifact_id in ("spring-boot-starter-tomcat", "tomcat-embed-core")):
1169
+ scope = "provided"
1170
+
1171
+ # FIX-9: resolve BOM version for Spring Boot / Spring Security starters
1172
+ resolved_version: Optional[str] = None
1173
+ if declared is None and parent_version:
1174
+ if group_id == "org.springframework.boot":
1175
+ resolved_version = parent_version
1176
+ elif group_id == "org.springframework.security" and "spring-security.version" in properties:
1177
+ resolved_version = properties["spring-security.version"]
1178
+
1139
1179
  records.append(
1140
1180
  DependencyRecord(
1141
1181
  name=f"{group_id}:{artifact_id}",
1142
1182
  ecosystem="java",
1143
1183
  scope=scope,
1144
1184
  declared_version=declared,
1185
+ resolved_version=resolved_version,
1145
1186
  source="manifest",
1146
1187
  manifest_path="pom.xml",
1147
1188
  )
@@ -1150,6 +1191,18 @@ class DependencyAnalyzer:
1150
1191
  limitations: list[str] = []
1151
1192
  if not records:
1152
1193
  limitations.append("java: pom.xml sin dependencias parseables (puede usar BOM o propiedades)")
1194
+
1195
+ # Warn when Spring Boot BOM manages transitive deps — they can't be resolved statically.
1196
+ parent_artifact_local = (
1197
+ root_elem.findtext(f"{ns}parent/{ns}artifactId") or ""
1198
+ ).strip() if parent_elem is not None else ""
1199
+ if parent_artifact_local == "spring-boot-starter-parent" and parent_version:
1200
+ limitations.append(
1201
+ f"spring_boot_bom_detected: transitive deps managed by Spring Boot BOM "
1202
+ f"v{parent_version}, not resolved statically. "
1203
+ "Run 'mvn dependency:tree' for the full transitive tree."
1204
+ )
1205
+
1153
1206
  return records, limitations
1154
1207
 
1155
1208
  def _analyze_gradle(self, root: Path) -> tuple[list[DependencyRecord], list[str]]: