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.
- {sourcecode-1.3.0 → sourcecode-1.5.0}/PKG-INFO +1 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/pyproject.toml +1 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/architecture_analyzer.py +12 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/ast_extractor.py +178 -40
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/cli.py +44 -10
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/contract_model.py +7 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/contract_pipeline.py +18 -2
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/dependency_analyzer.py +54 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/java.py +131 -10
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/project.py +33 -5
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/tooling.py +5 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/graph_analyzer.py +20 -67
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/schema.py +11 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/serializer.py +91 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_contract_pipeline.py +4 -4
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_analyzer_polyglot.py +4 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration.py +2 -1
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-join-discord/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-review-backlog/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.agents/skills/source-command-gsd-workstreams/SKILL.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.gitignore +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/.ruff.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/LICENSE +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/README.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/SECURITY.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/docs/privacy.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/docs/schema.md +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/raw +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/run_cli.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/__init__.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/conftest.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/pom.xml +0 -0
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {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
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/DemoApplication.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/domain/Health.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/mapper/HealthMapper.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/repository/HealthRepository.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/service/HealthService.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/java/com/example/demo/web/HealthRestController.java +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application-dev.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/fixtures/spring_boot_minimal/src/main/resources/application.yml +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_ast_extractor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block1_reliability.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block2_coverage.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_block5_quality.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_classifier.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_cli.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_context_scorer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_analyzer_node_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_analyzer_polyglot.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_dotnet.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_hybrid_inference.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_dependencies.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_lqn.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_java_spring_integration.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_packaging.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_phase1_improvements.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_pipeline_integrity.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_real_projects.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_redactor.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_scanner.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_signal_hierarchy.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_summarizer.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_telemetry.py +0 -0
- {sourcecode-1.3.0 → sourcecode-1.5.0}/tests/test_workspace_analyzer.py +0 -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
|
-
#
|
|
943
|
+
# ---------------------------------------------------------------------------
|
|
944
|
+
# Enhanced Java extraction (regex-based, annotation-aware)
|
|
944
945
|
# ---------------------------------------------------------------------------
|
|
945
946
|
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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
|
-
|
|
952
|
-
|
|
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
|
-
|
|
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
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
if
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
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
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
989
|
-
|
|
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
|
-
#
|
|
732
|
-
#
|
|
733
|
-
#
|
|
734
|
-
#
|
|
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
|
|
739
|
-
or docs or semantics or
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 >=
|
|
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
|
-
|
|
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]]:
|