sourcecode 2.5.16__tar.gz → 2.5.18__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-2.5.16 → sourcecode-2.5.18}/CHANGELOG.md +73 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/PKG-INFO +1 -1
- {sourcecode-2.5.16 → sourcecode-2.5.18}/pyproject.toml +1 -1
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/__init__.py +1 -1
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/adaptive_scanner.py +12 -36
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ast_extractor.py +0 -8
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/coverage_parser.py +0 -14
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/env_analyzer.py +0 -5
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/repository_ir.py +0 -39
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/scanner.py +55 -37
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/serializer.py +0 -61
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_tx_analyzer.py +0 -24
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/validation_surface.py +128 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/.gitignore +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/.ruff.toml +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/CONTRIBUTING.md +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/LICENSE +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/README.md +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/SECURITY.md +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/raw +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/compare_integration_engines.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/customer_smoke_test.sh +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/generate_jdk_exports.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/scripts/perf_harness.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/archetype.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cache.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/call_surface.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/caller_metrics.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/classifier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/cli.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_cache.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_graph.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/dynamic_argument_surface.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/endpoint_literals.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/endpoint_metrics.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/evidence_provider.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/explain.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/graph_evidence.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/hibernate_strat.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/jdk_exports.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/license.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/perf.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/progress.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/reconciliation.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/redactor.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/__init__.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/context.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/errors.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/executor.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/planner.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/query.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/request.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/resolution.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/result.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/retriever.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/runtime.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_endpoint.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_graph.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_impact.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_intf.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_struct.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/retrieval/steps_txsec.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/ris.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/schema.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/security_config.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/security_posture.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_impact_engine.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_integration_engine.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/semantic_services.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/type_usage_surface.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/validation_inference.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/version_check.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/src/sourcecode/workspace.py +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/README.md +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/licensing_schema.sql +0 -0
- {sourcecode-2.5.16 → sourcecode-2.5.18}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.5.18] — 2026-07-20
|
|
4
|
+
|
|
5
|
+
**Engineering Decision Support — Part A, F1: standalone OpenAPI specs now reach the
|
|
6
|
+
validation surface.** First correctness-foundation fix of the capability milestone
|
|
7
|
+
(`docs/DEVELOPMENT-ROADMAP.md`): trust-critical, stability-permitted (consumer-wiring
|
|
8
|
+
over an existing producer, no new inference). A repo shipping a plain `openapi.yml`
|
|
9
|
+
with no openapi-generator interface-controller was reported "No OpenAPI spec found" —
|
|
10
|
+
a confirmed false negative that would launder into any downstream decision. Full suite
|
|
11
|
+
(3538 passed, 5 skipped) confirms confinement.
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **F1 — standalone-spec discovery reaches the validation surface.** `extract_java_endpoints`
|
|
16
|
+
only links a spec when an `@RestController implements XxxApi` interface-controller
|
|
17
|
+
matches it; a repo with a plain `openapi.yml` and no such controller fell through to
|
|
18
|
+
"No OpenAPI spec found". The validation surface now consults the existing
|
|
19
|
+
`openapi_surface.build_openapi_surface` producer directly in that branch: a discovered
|
|
20
|
+
spec's body operations (POST/PUT/PATCH) contribute their declared constraints as
|
|
21
|
+
`source="openapi-spec"` routes (deduped against source-annotated routes by
|
|
22
|
+
method+path), the `openapi_spec` claim flips from `null` to the spec path, and the
|
|
23
|
+
note explains the standalone discovery. Genuinely absent specs are unchanged — the
|
|
24
|
+
`null` claim and source-DTO recovery path stand. New `TestStandaloneSpecRecovery`
|
|
25
|
+
regression class (4 tests).
|
|
26
|
+
- **F1 — `zero_result_reason` on genuine absence.** When no spec is on disk and no
|
|
27
|
+
in-repo DTO constraints are recoverable, the validation surface now carries a
|
|
28
|
+
machine-readable `zero_result_reason = "no_openapi_spec_and_no_source_dto_constraints"`
|
|
29
|
+
beside the prose note, so a consumer can distinguish an attested negative ("looked,
|
|
30
|
+
found nothing") from an un-run analysis. Absent on any positive result.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
|
|
34
|
+
- **Reconciliation rule deliberately NOT added for F1.** The roadmap's F1 strategy
|
|
35
|
+
suggested a "spec-present vs spec-absent" reconciliation rule as the honesty guard.
|
|
36
|
+
The flip was instead achieved at the true source (the validation surface now reads
|
|
37
|
+
the spec), and the two sibling surfaces that also report spec presence
|
|
38
|
+
(`serializer` arch_reasons, `confidence_analyzer` api_contract gap) were verified to
|
|
39
|
+
already detect a standalone `openapi.yml` via their filename check — so **no surface
|
|
40
|
+
makes the false "no spec" claim**, leaving the reconciliation layer no provable
|
|
41
|
+
contradiction to fire on. Adding a rule that never fires on any real repo violates
|
|
42
|
+
the layer's own contract (fire only on contradiction, EPV). Recorded here so the
|
|
43
|
+
roadmap item is closed by decision, not oversight.
|
|
44
|
+
|
|
45
|
+
## [2.5.17] — 2026-07-19
|
|
46
|
+
|
|
47
|
+
**Semantic Stability cleanup: dead code removed and a scanner fork collapsed, with
|
|
48
|
+
zero behavioural change.** Per `docs/DEVELOPMENT-ROADMAP.md` the 2.5.x line is scoped
|
|
49
|
+
to documentation, cleanup, small bugs and profiling — no new inference. This release
|
|
50
|
+
is pure internal hygiene; every user-facing surface is byte-for-byte unchanged and the
|
|
51
|
+
full suite (3534 passed, 5 skipped) confirms it.
|
|
52
|
+
|
|
53
|
+
### Removed
|
|
54
|
+
|
|
55
|
+
- **8 dead module-level private helpers** (−151 lines), each verified to have zero
|
|
56
|
+
references across `src` + `tests` (call sites, string names, and dynamic lookups) and
|
|
57
|
+
confirmed long-orphaned (all last touched May 2026, superseded since):
|
|
58
|
+
`repository_ir._resolve_type`, `repository_ir._common_package_prefix`,
|
|
59
|
+
`ast_extractor._find_all`, `ast_extractor._named_children`,
|
|
60
|
+
`coverage_parser._decode_numbits`, `spring_tx_analyzer._extract_method_body`,
|
|
61
|
+
`serializer._tiered_display_score`, `env_analyzer._replace_required`.
|
|
62
|
+
Kept `perf._reset_for_tests` and `migrate_check._find_xml_config_files` — both live
|
|
63
|
+
via tests.
|
|
64
|
+
|
|
65
|
+
### Changed
|
|
66
|
+
|
|
67
|
+
- **Collapsed the `FileScanner` / `AdaptiveScanner` fork duplication.** AdaptiveScanner
|
|
68
|
+
was forked from FileScanner and carried three byte-identical method bodies
|
|
69
|
+
(`find_manifests`, `_get_or_create_node`, `_is_excluded_by_gitignore`) — one of which
|
|
70
|
+
documented "Identical logic to FileScanner.find_manifests()". The shared logic now
|
|
71
|
+
lives as three module-level free functions in `scanner.py`
|
|
72
|
+
(`find_manifest_paths` / `get_or_create_tree_node` / `match_gitignore`) and both
|
|
73
|
+
classes delegate. No new coupling (AdaptiveScanner already imported from `scanner`),
|
|
74
|
+
no circular import; dropped its now-unused `cast` / `MANIFEST_NAMES` imports.
|
|
75
|
+
|
|
3
76
|
## [2.5.16] — 2026-07-19
|
|
4
77
|
|
|
5
78
|
**Performance campaign close-out: one real algorithmic win, cost-per-symbol
|
|
@@ -14,12 +14,18 @@ interface, same output format (None = file, dict = directory).
|
|
|
14
14
|
|
|
15
15
|
import os
|
|
16
16
|
from pathlib import Path
|
|
17
|
-
from typing import Any, Optional
|
|
17
|
+
from typing import Any, Optional
|
|
18
18
|
|
|
19
19
|
from pathspec import GitIgnoreSpec
|
|
20
20
|
|
|
21
21
|
from sourcecode.repo_classifier import RepoTopology
|
|
22
|
-
from sourcecode.scanner import
|
|
22
|
+
from sourcecode.scanner import (
|
|
23
|
+
DEFAULT_EXCLUDES,
|
|
24
|
+
EXCLUDED_FILE_SUFFIXES,
|
|
25
|
+
find_manifest_paths,
|
|
26
|
+
get_or_create_tree_node,
|
|
27
|
+
match_gitignore,
|
|
28
|
+
)
|
|
23
29
|
|
|
24
30
|
|
|
25
31
|
class AdaptiveScanner:
|
|
@@ -100,9 +106,7 @@ class AdaptiveScanner:
|
|
|
100
106
|
return self._gitignore_spec
|
|
101
107
|
|
|
102
108
|
def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
|
|
103
|
-
|
|
104
|
-
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
105
|
-
return spec.match_file(path_to_match)
|
|
109
|
+
return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
|
|
106
110
|
|
|
107
111
|
# ------------------------------------------------------------------
|
|
108
112
|
# Depth budget computation — the core of adaptive traversal
|
|
@@ -224,40 +228,12 @@ class AdaptiveScanner:
|
|
|
224
228
|
def _get_or_create_node(
|
|
225
229
|
self, tree: dict[str, Any], parts: tuple[str, ...]
|
|
226
230
|
) -> dict[str, Any]:
|
|
227
|
-
|
|
228
|
-
for part in parts:
|
|
229
|
-
if part not in node or node[part] is None:
|
|
230
|
-
node[part] = {}
|
|
231
|
-
node = cast(dict[str, Any], node[part])
|
|
232
|
-
return node
|
|
231
|
+
return get_or_create_tree_node(tree, parts)
|
|
233
232
|
|
|
234
233
|
# ------------------------------------------------------------------
|
|
235
234
|
# Manifest discovery — same interface as FileScanner
|
|
236
235
|
# ------------------------------------------------------------------
|
|
237
236
|
|
|
238
237
|
def find_manifests(self) -> list[str]:
|
|
239
|
-
"""Find manifest files at depth 0-1.
|
|
240
|
-
|
|
241
|
-
Identical logic to FileScanner.find_manifests() — depth-0 root
|
|
242
|
-
manifests plus depth-1 sub-package manifests, hidden dirs excluded.
|
|
243
|
-
"""
|
|
244
|
-
manifests: list[str] = []
|
|
245
|
-
for name in MANIFEST_NAMES:
|
|
246
|
-
candidate = self.root / name
|
|
247
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
248
|
-
manifests.append(str(candidate))
|
|
249
|
-
try:
|
|
250
|
-
for child in self.root.iterdir():
|
|
251
|
-
if (
|
|
252
|
-
child.is_dir()
|
|
253
|
-
and not child.is_symlink()
|
|
254
|
-
and child.name not in self._excludes
|
|
255
|
-
and not child.name.startswith(".")
|
|
256
|
-
):
|
|
257
|
-
for name in MANIFEST_NAMES:
|
|
258
|
-
candidate = child / name
|
|
259
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
260
|
-
manifests.append(str(candidate))
|
|
261
|
-
except PermissionError:
|
|
262
|
-
pass
|
|
263
|
-
return manifests
|
|
238
|
+
"""Find manifest files at depth 0-1 (delegates to the shared helper)."""
|
|
239
|
+
return find_manifest_paths(self.root, self._excludes)
|
|
@@ -176,18 +176,10 @@ def _find_child(node: Any, *types: str) -> Optional[Any]:
|
|
|
176
176
|
return None
|
|
177
177
|
|
|
178
178
|
|
|
179
|
-
def _find_all(node: Any, *types: str) -> list[Any]:
|
|
180
|
-
return [child for child in node.children if child.type in types]
|
|
181
|
-
|
|
182
|
-
|
|
183
179
|
def _text(node: Any, src: bytes) -> str:
|
|
184
180
|
return src[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
185
181
|
|
|
186
182
|
|
|
187
|
-
def _named_children(node: Any) -> list[Any]:
|
|
188
|
-
return [c for c in node.children if c.is_named]
|
|
189
|
-
|
|
190
|
-
|
|
191
183
|
# ---------------------------------------------------------------------------
|
|
192
184
|
# Tree-sitter TS/JS extraction
|
|
193
185
|
# ---------------------------------------------------------------------------
|
|
@@ -39,20 +39,6 @@ def _safe_int(value: Optional[str]) -> Optional[int]:
|
|
|
39
39
|
return None
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def _decode_numbits(blob: bytes) -> list[int]:
|
|
43
|
-
"""Decodifica bitset little-endian de coverage.py .coverage SQLite.
|
|
44
|
-
|
|
45
|
-
Byte i, bit j set => linea (i*8 + j + 1) ejecutada.
|
|
46
|
-
Ejemplo: bytes([0b00000101]) => [1, 3]
|
|
47
|
-
"""
|
|
48
|
-
return [
|
|
49
|
-
i * 8 + j + 1
|
|
50
|
-
for i, byte in enumerate(blob)
|
|
51
|
-
for j in range(8)
|
|
52
|
-
if byte & (1 << j)
|
|
53
|
-
]
|
|
54
|
-
|
|
55
|
-
|
|
56
42
|
# ---------------------------------------------------------------------------
|
|
57
43
|
# CoverageParser
|
|
58
44
|
# ---------------------------------------------------------------------------
|
|
@@ -534,8 +534,3 @@ class EnvAnalyzer:
|
|
|
534
534
|
def _replace_description(record, description: str):
|
|
535
535
|
from dataclasses import replace
|
|
536
536
|
return replace(record, description=description)
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
def _replace_required(record, required: bool, default: Optional[str]):
|
|
540
|
-
from dataclasses import replace
|
|
541
|
-
return replace(record, required=required, default=default if not record.default else record.default)
|
|
@@ -807,11 +807,6 @@ def _pop_closed(class_stack: list[tuple[str, int]], depth: int) -> None:
|
|
|
807
807
|
class_stack.pop()
|
|
808
808
|
|
|
809
809
|
|
|
810
|
-
def _resolve_type(simple: str, import_map: dict[str, str]) -> Optional[str]:
|
|
811
|
-
base = re.sub(r'<.*', '', simple).strip().split('.')[-1]
|
|
812
|
-
return import_map.get(base)
|
|
813
|
-
|
|
814
|
-
|
|
815
810
|
def _resolve_types_from_text(text: str, import_map: dict[str, str]) -> list[str]:
|
|
816
811
|
resolved = []
|
|
817
812
|
for token in re.findall(r'\b([A-Z]\w*)\b', text):
|
|
@@ -3597,40 +3592,6 @@ def _build_evidence_bundles(
|
|
|
3597
3592
|
return bundles
|
|
3598
3593
|
|
|
3599
3594
|
|
|
3600
|
-
def _common_package_prefix(fqns: list[str]) -> str:
|
|
3601
|
-
"""Longest common dot-separated package prefix across a list of FQNs."""
|
|
3602
|
-
if not fqns:
|
|
3603
|
-
return ""
|
|
3604
|
-
# Strip class/method suffix — keep only package parts (lowercase segments)
|
|
3605
|
-
def pkg_parts(fqn: str) -> list[str]:
|
|
3606
|
-
parts = fqn.split(".")
|
|
3607
|
-
# Drop trailing class/method names (PascalCase or after '#')
|
|
3608
|
-
result = []
|
|
3609
|
-
for p in parts:
|
|
3610
|
-
if "#" in p:
|
|
3611
|
-
break
|
|
3612
|
-
if p and p[0].isupper():
|
|
3613
|
-
break
|
|
3614
|
-
result.append(p)
|
|
3615
|
-
return result
|
|
3616
|
-
|
|
3617
|
-
segs = [pkg_parts(f) for f in fqns if pkg_parts(f)]
|
|
3618
|
-
if not segs:
|
|
3619
|
-
return fqns[0].rsplit(".", 1)[0] if "." in fqns[0] else fqns[0]
|
|
3620
|
-
common = segs[0]
|
|
3621
|
-
for s in segs[1:]:
|
|
3622
|
-
new_common = []
|
|
3623
|
-
for a, b in zip(common, s):
|
|
3624
|
-
if a == b:
|
|
3625
|
-
new_common.append(a)
|
|
3626
|
-
else:
|
|
3627
|
-
break
|
|
3628
|
-
common = new_common
|
|
3629
|
-
if not common:
|
|
3630
|
-
break
|
|
3631
|
-
return ".".join(common) if common else fqns[0].rsplit(".", 1)[0]
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
3595
|
def _subsystem_label(package_prefix: str) -> str:
|
|
3635
3596
|
"""Derive short human label enforcing minimum meaningful depth.
|
|
3636
3597
|
|
|
@@ -115,6 +115,57 @@ def classify_manifest(manifest_path: str, root: Path) -> str:
|
|
|
115
115
|
return "auxiliary"
|
|
116
116
|
|
|
117
117
|
|
|
118
|
+
# ── Shared scan primitives (used by both FileScanner and AdaptiveScanner) ────
|
|
119
|
+
# AdaptiveScanner was forked from FileScanner and carried byte-identical copies of
|
|
120
|
+
# these three; they live here as the single source of truth, each class method a
|
|
121
|
+
# thin delegate.
|
|
122
|
+
|
|
123
|
+
def find_manifest_paths(root: Path, excludes: frozenset[str]) -> list[str]:
|
|
124
|
+
"""Manifest files at depth 0–1 (SCAN-04): root manifests plus depth-1
|
|
125
|
+
sub-package manifests, with hidden/excluded dirs and symlinks skipped."""
|
|
126
|
+
manifests: list[str] = []
|
|
127
|
+
# Depth 0: root
|
|
128
|
+
for name in MANIFEST_NAMES:
|
|
129
|
+
candidate = root / name
|
|
130
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
131
|
+
manifests.append(str(candidate))
|
|
132
|
+
# Depth 1: first level (hidden dirs excluded — tooling, not project)
|
|
133
|
+
try:
|
|
134
|
+
for child in root.iterdir():
|
|
135
|
+
if (
|
|
136
|
+
child.is_dir()
|
|
137
|
+
and not child.is_symlink()
|
|
138
|
+
and child.name not in excludes
|
|
139
|
+
and not child.name.startswith(".")
|
|
140
|
+
):
|
|
141
|
+
for name in MANIFEST_NAMES:
|
|
142
|
+
candidate = child / name
|
|
143
|
+
if candidate.exists() and not candidate.is_symlink():
|
|
144
|
+
manifests.append(str(candidate))
|
|
145
|
+
except PermissionError:
|
|
146
|
+
pass
|
|
147
|
+
return manifests
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def get_or_create_tree_node(
|
|
151
|
+
tree: dict[str, Any], parts: tuple[str, ...]
|
|
152
|
+
) -> dict[str, Any]:
|
|
153
|
+
"""Navigate/create the nested-dict tree node for a path's parts."""
|
|
154
|
+
node = tree
|
|
155
|
+
for part in parts:
|
|
156
|
+
if part not in node or node[part] is None:
|
|
157
|
+
node[part] = {}
|
|
158
|
+
node = cast(dict[str, Any], node[part])
|
|
159
|
+
return node
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def match_gitignore(spec: GitIgnoreSpec, rel_path: str, is_dir: bool) -> bool:
|
|
163
|
+
"""True if a root-relative path is excluded by ``spec``. GitIgnoreSpec expects
|
|
164
|
+
a trailing '/' for directories."""
|
|
165
|
+
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
166
|
+
return spec.match_file(path_to_match)
|
|
167
|
+
|
|
168
|
+
|
|
118
169
|
class FileScanner:
|
|
119
170
|
"""Escanea un directorio de proyecto y produce un arbol de ficheros filtrado.
|
|
120
171
|
|
|
@@ -148,10 +199,7 @@ class FileScanner:
|
|
|
148
199
|
|
|
149
200
|
def _is_excluded_by_gitignore(self, rel_path: str, is_dir: bool) -> bool:
|
|
150
201
|
"""Comprueba si una ruta relativa (a self.root) esta excluida por .gitignore."""
|
|
151
|
-
|
|
152
|
-
# GitIgnoreSpec espera rutas con / al final para directorios
|
|
153
|
-
path_to_match = rel_path + "/" if is_dir else rel_path
|
|
154
|
-
return spec.match_file(path_to_match)
|
|
202
|
+
return match_gitignore(self._load_gitignore_spec(), rel_path, is_dir)
|
|
155
203
|
|
|
156
204
|
def scan_tree(self) -> dict[str, Any]:
|
|
157
205
|
"""Construye el arbol JSON anidado del proyecto.
|
|
@@ -223,38 +271,8 @@ class FileScanner:
|
|
|
223
271
|
self, tree: dict[str, Any], parts: tuple[str, ...]
|
|
224
272
|
) -> dict[str, Any]:
|
|
225
273
|
"""Navega/crea el nodo del arbol para la ruta indicada."""
|
|
226
|
-
|
|
227
|
-
for part in parts:
|
|
228
|
-
if part not in node or node[part] is None:
|
|
229
|
-
node[part] = {}
|
|
230
|
-
node = cast(dict[str, Any], node[part])
|
|
231
|
-
return node
|
|
274
|
+
return get_or_create_tree_node(tree, parts)
|
|
232
275
|
|
|
233
276
|
def find_manifests(self) -> list[str]:
|
|
234
|
-
"""Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04).
|
|
235
|
-
|
|
236
|
-
Retorna:
|
|
237
|
-
Lista de paths absolutos de manifiestos encontrados.
|
|
238
|
-
"""
|
|
239
|
-
manifests: list[str] = []
|
|
240
|
-
# Profundidad 0: raiz
|
|
241
|
-
for name in MANIFEST_NAMES:
|
|
242
|
-
candidate = self.root / name
|
|
243
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
244
|
-
manifests.append(str(candidate))
|
|
245
|
-
# Profundidad 1: primer nivel (excluir directorios ocultos — son tooling, no proyecto)
|
|
246
|
-
try:
|
|
247
|
-
for child in self.root.iterdir():
|
|
248
|
-
if (
|
|
249
|
-
child.is_dir()
|
|
250
|
-
and not child.is_symlink()
|
|
251
|
-
and child.name not in self._excludes
|
|
252
|
-
and not child.name.startswith(".")
|
|
253
|
-
):
|
|
254
|
-
for name in MANIFEST_NAMES:
|
|
255
|
-
candidate = child / name
|
|
256
|
-
if candidate.exists() and not candidate.is_symlink():
|
|
257
|
-
manifests.append(str(candidate))
|
|
258
|
-
except PermissionError:
|
|
259
|
-
pass
|
|
260
|
-
return manifests
|
|
277
|
+
"""Encuentra ficheros de manifiesto en profundidad 0-1 (SCAN-04)."""
|
|
278
|
+
return find_manifest_paths(self.root, self._excludes)
|
|
@@ -913,67 +913,6 @@ def _jndi_datasources(sm: "SourceMap") -> "Optional[list[dict[str, Any]]]":
|
|
|
913
913
|
return datasources
|
|
914
914
|
|
|
915
915
|
|
|
916
|
-
def _tiered_display_score(
|
|
917
|
-
pre_bonus_combined: float,
|
|
918
|
-
file_class: Any,
|
|
919
|
-
path: str,
|
|
920
|
-
entry_paths: set,
|
|
921
|
-
has_structural_signals: bool = False,
|
|
922
|
-
) -> float:
|
|
923
|
-
"""Evidence-tiered display score [0.0, 1.0].
|
|
924
|
-
|
|
925
|
-
Tiers enforce: strong evidence > medium evidence > filesystem/path only.
|
|
926
|
-
M3 sort bonuses must NOT be included in pre_bonus_combined — they are for
|
|
927
|
-
ordering only and must not inflate the displayed score.
|
|
928
|
-
|
|
929
|
-
Tier ceilings:
|
|
930
|
-
T1 confirmed production entrypoint 0.92–1.00
|
|
931
|
-
T2 entrypoint (weaker category) 0.80–0.91
|
|
932
|
-
T3 annotation-confirmed stereotype 0.40–0.90 (table-calibrated)
|
|
933
|
-
T4 framework import evidence 0.55–0.79
|
|
934
|
-
T5 code definitions + imports 0.38–0.54
|
|
935
|
-
T6 build manifest / tooling / test 0.25–0.45
|
|
936
|
-
T7 path/filesystem signal only 0.10–0.39
|
|
937
|
-
"""
|
|
938
|
-
from sourcecode.file_classifier import JAVA_STEREOTYPE_CATEGORIES
|
|
939
|
-
|
|
940
|
-
cat = file_class.category if file_class else None
|
|
941
|
-
base_rel = file_class.relevance if file_class else 0.0
|
|
942
|
-
|
|
943
|
-
# T1: confirmed production entrypoint
|
|
944
|
-
if path in entry_paths and cat in ("runtime_core", "cli_entrypoint"):
|
|
945
|
-
return round(min(1.0, max(0.92, base_rel)), 3)
|
|
946
|
-
|
|
947
|
-
# T2: in entry_paths but weaker evidence category
|
|
948
|
-
if path in entry_paths:
|
|
949
|
-
return round(min(0.91, max(0.80, pre_bonus_combined / 2.0)), 3)
|
|
950
|
-
|
|
951
|
-
# T3: annotation-confirmed stereotype — table values are already calibrated
|
|
952
|
-
if file_class and cat in JAVA_STEREOTYPE_CATEGORIES:
|
|
953
|
-
return round(base_rel, 3)
|
|
954
|
-
|
|
955
|
-
# T4: framework import evidence (medium strength)
|
|
956
|
-
if cat in ("api_layer", "database_layer", "infrastructure"):
|
|
957
|
-
return round(min(0.79, max(0.55, pre_bonus_combined / 2.0)), 3)
|
|
958
|
-
|
|
959
|
-
# T5: code definitions with imports (medium-low)
|
|
960
|
-
if cat in ("application_logic", "domain_model"):
|
|
961
|
-
return round(min(0.54, max(0.38, pre_bonus_combined / 2.0)), 3)
|
|
962
|
-
|
|
963
|
-
# T6: build manifest / tooling / test
|
|
964
|
-
if cat == "build_system":
|
|
965
|
-
return round(min(0.45, base_rel), 3)
|
|
966
|
-
if cat in ("tests", "tooling"):
|
|
967
|
-
return round(min(0.35, base_rel), 3)
|
|
968
|
-
|
|
969
|
-
# T7: no content classification — filesystem/structural signals only
|
|
970
|
-
# has_structural_signals: fan_in, churn, export — allows up to 0.54
|
|
971
|
-
# pure path/filename only — hard cap 0.39
|
|
972
|
-
if has_structural_signals:
|
|
973
|
-
return round(min(0.54, max(0.10, pre_bonus_combined / 2.0)), 3)
|
|
974
|
-
return round(min(0.39, max(0.10, pre_bonus_combined / 2.0)), 3)
|
|
975
|
-
|
|
976
|
-
|
|
977
916
|
def _build_file_signals(
|
|
978
917
|
file_class: Any,
|
|
979
918
|
path: str,
|
|
@@ -112,30 +112,6 @@ def _caught_types(catch_header_inner: str) -> list[str]:
|
|
|
112
112
|
return types
|
|
113
113
|
|
|
114
114
|
|
|
115
|
-
def _extract_method_body(source: str, method_name: str) -> str:
|
|
116
|
-
"""Extract the first method body matching method_name using brace counting.
|
|
117
|
-
|
|
118
|
-
Returns the text from '{' to the matching '}', or empty string if not found.
|
|
119
|
-
Needed to scope TX-005 regex to the specific method instead of the whole file.
|
|
120
|
-
"""
|
|
121
|
-
pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
|
|
122
|
-
for m in pattern.finditer(source):
|
|
123
|
-
brace_pos = source.find('{', m.end())
|
|
124
|
-
if brace_pos < 0:
|
|
125
|
-
continue
|
|
126
|
-
depth = 1
|
|
127
|
-
i = brace_pos + 1
|
|
128
|
-
while i < len(source) and depth > 0:
|
|
129
|
-
c = source[i]
|
|
130
|
-
if c == '{':
|
|
131
|
-
depth += 1
|
|
132
|
-
elif c == '}':
|
|
133
|
-
depth -= 1
|
|
134
|
-
i += 1
|
|
135
|
-
return source[brace_pos:i]
|
|
136
|
-
return ""
|
|
137
|
-
|
|
138
|
-
|
|
139
115
|
# ---------------------------------------------------------------------------
|
|
140
116
|
# Pattern protocol
|
|
141
117
|
# ---------------------------------------------------------------------------
|
|
@@ -363,6 +363,98 @@ def _recover_source_endpoints(
|
|
|
363
363
|
return routes, total
|
|
364
364
|
|
|
365
365
|
|
|
366
|
+
def _recover_spec_endpoints(
|
|
367
|
+
root: Path,
|
|
368
|
+
existing_keys: "set[tuple[Any, Any]]",
|
|
369
|
+
catalog: "dict[str, Any]",
|
|
370
|
+
custom_used: "set[str]",
|
|
371
|
+
) -> "tuple[Optional[str], list[dict[str, Any]], int]":
|
|
372
|
+
"""Recover validation routes from a standalone OpenAPI spec on disk.
|
|
373
|
+
|
|
374
|
+
F1: ``extract_java_endpoints`` only links a spec when an openapi-generator
|
|
375
|
+
interface-controller (``@RestController implements XxxApi``) matches it. A
|
|
376
|
+
repo that ships a plain ``openapi.yml`` with no such controller therefore
|
|
377
|
+
reported "No OpenAPI spec found" — a confirmed false negative. This consults
|
|
378
|
+
the existing :func:`build_openapi_surface` producer directly so a discovered
|
|
379
|
+
spec's body operations contribute their declared constraints and flip the
|
|
380
|
+
"no spec" claim.
|
|
381
|
+
|
|
382
|
+
Returns ``(spec_path, routes, validated_field_count)``; ``spec_path`` is None
|
|
383
|
+
when no parseable spec with operations is present.
|
|
384
|
+
"""
|
|
385
|
+
from sourcecode.openapi_surface import build_openapi_surface
|
|
386
|
+
|
|
387
|
+
surface = build_openapi_surface(root)
|
|
388
|
+
if surface is None or not surface.operations:
|
|
389
|
+
return None, [], 0
|
|
390
|
+
|
|
391
|
+
routes: "list[dict[str, Any]]" = []
|
|
392
|
+
total = 0
|
|
393
|
+
for op in surface.operations:
|
|
394
|
+
if str(op.method).upper() not in ("POST", "PUT", "PATCH"):
|
|
395
|
+
continue
|
|
396
|
+
key = (op.method, op.path)
|
|
397
|
+
if key in existing_keys:
|
|
398
|
+
continue # already surfaced from source — dedupe by method+path
|
|
399
|
+
schema = surface.schemas.get(op.request_body_schema) if op.request_body_schema else None
|
|
400
|
+
constraints = [f.to_dict() for f in schema.fields] if schema is not None else []
|
|
401
|
+
validated = _validated_fields_from_constraints(constraints, catalog, custom_used)
|
|
402
|
+
total += len(validated)
|
|
403
|
+
route: "dict[str, Any]" = {
|
|
404
|
+
"method": op.method,
|
|
405
|
+
"path": op.path,
|
|
406
|
+
"controller": None,
|
|
407
|
+
"handler": op.operation_id or "(operation)",
|
|
408
|
+
"schema": op.request_body_schema,
|
|
409
|
+
"source": "openapi-spec",
|
|
410
|
+
"confidence": "high",
|
|
411
|
+
"validatedFields": validated,
|
|
412
|
+
}
|
|
413
|
+
routes.append(route)
|
|
414
|
+
existing_keys.add(key)
|
|
415
|
+
return surface.spec_path, routes, total
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _validated_fields_from_constraints(
|
|
419
|
+
constraints: "list[Any]",
|
|
420
|
+
catalog: "dict[str, Any]",
|
|
421
|
+
custom_used: "set[str]",
|
|
422
|
+
) -> "list[dict[str, Any]]":
|
|
423
|
+
"""Render a schema's constraint dicts into ``validatedFields`` entries.
|
|
424
|
+
|
|
425
|
+
Mirrors the built-in-rule + custom-validator linkage the main loop applies to
|
|
426
|
+
OpenAPI request bodies; shared so the standalone-spec path (F1) stays byte-
|
|
427
|
+
consistent with the interface-controller path."""
|
|
428
|
+
out: "list[dict[str, Any]]" = []
|
|
429
|
+
for fc in constraints:
|
|
430
|
+
if not isinstance(fc, dict):
|
|
431
|
+
continue
|
|
432
|
+
rules = _field_rules(fc)
|
|
433
|
+
customs: "list[dict[str, Any]]" = []
|
|
434
|
+
for ann in fc.get("extraAnnotations", []) or []:
|
|
435
|
+
cc = catalog.get(ann)
|
|
436
|
+
entry: "dict[str, Any]" = {"annotation": ann}
|
|
437
|
+
if cc is not None:
|
|
438
|
+
custom_used.add(ann)
|
|
439
|
+
if cc.validators:
|
|
440
|
+
entry["validators"] = cc.validators
|
|
441
|
+
if cc.message is not None:
|
|
442
|
+
entry["message"] = cc.message
|
|
443
|
+
entry["resolved"] = True
|
|
444
|
+
else:
|
|
445
|
+
entry["resolved"] = False
|
|
446
|
+
customs.append(entry)
|
|
447
|
+
if not rules and not customs:
|
|
448
|
+
continue
|
|
449
|
+
field_entry: "dict[str, Any]" = {"name": fc.get("name")}
|
|
450
|
+
if rules:
|
|
451
|
+
field_entry["rules"] = rules
|
|
452
|
+
if customs:
|
|
453
|
+
field_entry["customValidators"] = customs
|
|
454
|
+
out.append(field_entry)
|
|
455
|
+
return out
|
|
456
|
+
|
|
457
|
+
|
|
366
458
|
def _field_rules(fieldc: "dict[str, Any]") -> "list[dict[str, Any]]":
|
|
367
459
|
"""Render a constraint dict's built-in rules as a list of {kind, value}."""
|
|
368
460
|
rules: "list[dict[str, Any]]" = []
|
|
@@ -491,6 +583,36 @@ def build_validation_surface(
|
|
|
491
583
|
if spec_path:
|
|
492
584
|
result["openapi_spec"] = spec_path
|
|
493
585
|
else:
|
|
586
|
+
# No spec linked by extract_java_endpoints (no openapi-generator
|
|
587
|
+
# interface-controller matched). F1: consult the standalone-spec
|
|
588
|
+
# producer directly — a plain openapi.yml shipped in the repo is a real
|
|
589
|
+
# spec, so its body operations contribute constraints and flip the
|
|
590
|
+
# "no spec found" claim instead of reporting a false negative.
|
|
591
|
+
existing_keys = {(r.get("method"), r.get("path")) for r in out_endpoints}
|
|
592
|
+
spec_path2, spec_routes, spec_fields = _recover_spec_endpoints(
|
|
593
|
+
root, existing_keys, catalog, custom_used
|
|
594
|
+
)
|
|
595
|
+
if spec_path2 is not None:
|
|
596
|
+
result["openapi_spec"] = spec_path2
|
|
597
|
+
out_endpoints.extend(spec_routes)
|
|
598
|
+
total_validated_fields += spec_fields
|
|
599
|
+
spec_keys = {(r.get("method"), r.get("path")) for r in spec_routes}
|
|
600
|
+
gaps = [g for g in gaps if (g.get("method"), g.get("path")) not in spec_keys]
|
|
601
|
+
result["endpoints"] = out_endpoints
|
|
602
|
+
result["gaps"] = gaps
|
|
603
|
+
result["custom_validators"] = [catalog[k].to_dict() for k in sorted(catalog)]
|
|
604
|
+
result["summary"]["endpoints_with_body"] = len(out_endpoints)
|
|
605
|
+
result["summary"]["validated_fields"] = total_validated_fields
|
|
606
|
+
result["summary"]["custom_validators_linked"] = len(custom_used)
|
|
607
|
+
result["summary"]["gaps"] = len(gaps)
|
|
608
|
+
result["summary"]["spec_derived_routes"] = len(spec_routes)
|
|
609
|
+
result["note"] = (
|
|
610
|
+
"OpenAPI spec discovered on disk (standalone, not linked to a "
|
|
611
|
+
"generated interface-controller); body operations contributed "
|
|
612
|
+
"their declared constraints. Response shapes and non-body "
|
|
613
|
+
"operations are not modeled here."
|
|
614
|
+
)
|
|
615
|
+
return result
|
|
494
616
|
# No OpenAPI spec on disk / under target/generated-sources. Recover
|
|
495
617
|
# declarative constraints from the graph's DTO field nodes (bean-
|
|
496
618
|
# validation annotations on @Valid/@Validated handler bodies), so a
|
|
@@ -525,6 +647,12 @@ def build_validation_surface(
|
|
|
525
647
|
"partial; OpenAPI-carried constraints would be more complete."
|
|
526
648
|
)
|
|
527
649
|
else:
|
|
650
|
+
# Genuine absence — no spec on disk AND no in-repo DTO constraints.
|
|
651
|
+
# Emit a machine-readable zero_result_reason (F1) so a consumer can
|
|
652
|
+
# tell "we looked and found nothing" from "we did not look": an empty
|
|
653
|
+
# surface here is an attested negative, not a missing-validation
|
|
654
|
+
# finding.
|
|
655
|
+
result["zero_result_reason"] = "no_openapi_spec_and_no_source_dto_constraints"
|
|
528
656
|
result["note"] = (
|
|
529
657
|
"No OpenAPI spec found and no source DTO constraints recovered "
|
|
530
658
|
"(no handler validates an in-repo DTO via @Valid/@Validated, or "
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|