sourcecode 1.57.0__tar.gz → 1.59.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.57.0 → sourcecode-1.59.0}/CHANGELOG.md +42 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/PKG-INFO +5 -3
- {sourcecode-1.57.0 → sourcecode-1.59.0}/README.md +4 -2
- {sourcecode-1.57.0 → sourcecode-1.59.0}/pyproject.toml +1 -1
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/repository_ir.py +106 -2
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_impact.py +60 -1
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/validation_surface.py +258 -12
- {sourcecode-1.57.0 → sourcecode-1.59.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/.gitignore +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/.ruff.toml +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/LICENSE +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/SECURITY.md +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/raw +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/cli.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/format_contract.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/integration_detector.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/openapi_surface.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.57.0 → sourcecode-1.59.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,5 +1,47 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.59.0] — 2026-06-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
- **Event topology now recognizes generic-wrapper event publishers.**
|
|
7
|
+
`publishEvent(new SaveServiceEvent<>(obj))` and explicit forms like
|
|
8
|
+
`new SaveServiceEvent<Order>(obj)` were silently dropped: the publisher-edge
|
|
9
|
+
regex required the constructor `(` to immediately follow the class name, so any
|
|
10
|
+
diamond `<>` or type argument broke the match. `repo-ir` / event-topology then
|
|
11
|
+
reported no producers for the whole generic `*ServiceEvent` family (e.g. OpenMRS
|
|
12
|
+
`SaveServiceEvent`/`VoidServiceEvent`/`RetireServiceEvent`). The inline and
|
|
13
|
+
two-step publish scans now skip an optional (incl. nested) generic argument list.
|
|
14
|
+
- **`impact-chain` now resolves intra-class method callers.** A query on a
|
|
15
|
+
private/helper method (e.g. `OrderServiceImpl#stopOrder`) found zero direct
|
|
16
|
+
callers and degraded to a wrong class-level expansion, because method-to-method
|
|
17
|
+
calls inside a single class produced no graph edge — only class-level `calls`
|
|
18
|
+
edges existed. The relation builder now emits method-level `calls` edges for
|
|
19
|
+
bare `m(...)` and `this.m(...)` invocations whose target is a sibling method of
|
|
20
|
+
the same class (string/comment-aware, overload-safe, no self-loops). The two
|
|
21
|
+
defects were found dogfooding the tool on OpenMRS issue #6197.
|
|
22
|
+
|
|
23
|
+
## [1.58.0] — 2026-06-19
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **`validation` recovers DTO constraints from source when no OpenAPI spec is
|
|
27
|
+
present.** Previously `validated_fields` read `0` for any repo without a spec
|
|
28
|
+
on disk, even when DTOs carried bean-validation annotations. The command now
|
|
29
|
+
locates each body-shaped handler's `@Valid`/`@Validated` parameter, resolves
|
|
30
|
+
the DTO in-repo, and reads its field constraints (following one in-repo
|
|
31
|
+
supertype so inherited constraints are kept). Recovered routes are tagged
|
|
32
|
+
`source="source-derived"` at medium confidence with a `binding` hint
|
|
33
|
+
(body vs form). The core symbol/endpoint extractor is untouched, so the
|
|
34
|
+
OpenAPI-driven path is unchanged. Verified: spring-petclinic 0 → 13 validated
|
|
35
|
+
fields; repos with no `@Valid` usage correctly stay at 0 (no false positives).
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
- **`impact-chain` output now matches the `impact` command's risk schema.**
|
|
39
|
+
`risk_score`, `confidence_score`, `confidence_level`, and `explanation` are
|
|
40
|
+
emitted at the top level (previously `risk_score` lived only in `metadata`
|
|
41
|
+
and there was no `explanation`). The legacy `confidence` string is retained
|
|
42
|
+
and equals `confidence_level`. Risk/confidence formulas are unchanged — only
|
|
43
|
+
the output contract was aligned so agents parse both commands with one shape.
|
|
44
|
+
|
|
3
45
|
## [1.57.0] — 2026-06-19
|
|
4
46
|
|
|
5
47
|
### Changed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.59.0
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.
|
|
117
|
+
# sourcecode 1.59.0
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -150,6 +150,8 @@ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
|
150
150
|
sourcecode endpoints /path/to/repo
|
|
151
151
|
|
|
152
152
|
# Request-body validation per endpoint: constraints + custom validators (free)
|
|
153
|
+
# Recovers constraints from the OpenAPI spec, or directly from Java DTO
|
|
154
|
+
# bean-validation annotations when no spec is present.
|
|
153
155
|
sourcecode validation /path/to/repo
|
|
154
156
|
|
|
155
157
|
# Onboard to an unfamiliar codebase
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|

|
|
7
7
|
|
|
8
8
|
---
|
|
@@ -76,7 +76,7 @@ pipx install sourcecode
|
|
|
76
76
|
|
|
77
77
|
```bash
|
|
78
78
|
sourcecode version
|
|
79
|
-
# sourcecode 1.
|
|
79
|
+
# sourcecode 1.59.0
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -112,6 +112,8 @@ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
|
112
112
|
sourcecode endpoints /path/to/repo
|
|
113
113
|
|
|
114
114
|
# Request-body validation per endpoint: constraints + custom validators (free)
|
|
115
|
+
# Recovers constraints from the OpenAPI spec, or directly from Java DTO
|
|
116
|
+
# bean-validation annotations when no spec is present.
|
|
115
117
|
sourcecode validation /path/to/repo
|
|
116
118
|
|
|
117
119
|
# Onboard to an unfamiliar codebase
|
|
@@ -347,12 +347,17 @@ _SPRING_OTHER: frozenset[str] = frozenset({
|
|
|
347
347
|
"@MatrixParam", "@CookieParam", "@Context",
|
|
348
348
|
})
|
|
349
349
|
|
|
350
|
-
|
|
350
|
+
# Optional generic type args between the event class name and its constructor
|
|
351
|
+
# parens — diamond `<>` or explicit `<Order>` / `<Map<String,Integer>>`.
|
|
352
|
+
# Required so `publishEvent(new SaveServiceEvent<>(obj))` (generic event wrappers,
|
|
353
|
+
# e.g. OpenMRS *ServiceEvent family) is recognised as a publisher edge.
|
|
354
|
+
_GENERIC_ARGS = r'(?:<[^<>;{}()]*(?:<[^<>;{}()]*>[^<>;{}()]*)*>)?'
|
|
355
|
+
_PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*' + _GENERIC_ARGS + r'\s*[(\{]')
|
|
351
356
|
|
|
352
357
|
# Two-step publish: SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)
|
|
353
358
|
# Used when event is created before passing to publishEvent (common pattern).
|
|
354
359
|
_PUBLISH_EVENT_CALL_RE = re.compile(r'\.publishEvent\s*\(')
|
|
355
|
-
_NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*[\({]')
|
|
360
|
+
_NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*' + _GENERIC_ARGS + r'\s*[\({]')
|
|
356
361
|
|
|
357
362
|
# Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
|
|
358
363
|
_FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
|
|
@@ -1122,6 +1127,102 @@ def _build_same_package_map(symbols: list[SymbolRecord]) -> dict[str, dict[str,
|
|
|
1122
1127
|
return result
|
|
1123
1128
|
|
|
1124
1129
|
|
|
1130
|
+
# Reserved words that read like calls (`if (`, `for (`, …). Can never be sibling
|
|
1131
|
+
# method names (Java reserves them), but guard the intra-class scan anyway.
|
|
1132
|
+
_CALL_KEYWORDS: frozenset[str] = frozenset({
|
|
1133
|
+
"if", "for", "while", "switch", "catch", "return", "synchronized",
|
|
1134
|
+
"new", "super", "this", "assert",
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _method_body(raw_lines: list[str], start_idx: int) -> str:
|
|
1139
|
+
"""Source text of the method/constructor declared at raw_lines[start_idx],
|
|
1140
|
+
from its first '{' to the matching '}' (brace-matched, string/char aware).
|
|
1141
|
+
|
|
1142
|
+
The signature prefix before '{' is dropped so the method's own name is not
|
|
1143
|
+
scanned as a call site. Returns "" for a bodyless declaration (abstract /
|
|
1144
|
+
interface method terminated by ';' before any '{').
|
|
1145
|
+
"""
|
|
1146
|
+
depth = 0
|
|
1147
|
+
started = False
|
|
1148
|
+
out: list[str] = []
|
|
1149
|
+
for i in range(start_idx, len(raw_lines)):
|
|
1150
|
+
line = raw_lines[i]
|
|
1151
|
+
if not started:
|
|
1152
|
+
if "{" in line:
|
|
1153
|
+
started = True
|
|
1154
|
+
line = line[line.index("{"):]
|
|
1155
|
+
elif ";" in line:
|
|
1156
|
+
return "" # bodyless declaration
|
|
1157
|
+
else:
|
|
1158
|
+
continue # multi-line signature continuation
|
|
1159
|
+
out.append(line)
|
|
1160
|
+
depth += _count_net_braces(line)
|
|
1161
|
+
if depth <= 0:
|
|
1162
|
+
break
|
|
1163
|
+
return "\n".join(out)
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _intra_class_call_edges(symbols: list[SymbolRecord], source: str) -> list[RelationEdge]:
|
|
1167
|
+
"""Method-level `calls` edges for intra-class invocations.
|
|
1168
|
+
|
|
1169
|
+
`discontinueOrder(){ stopOrder(...); }` →
|
|
1170
|
+
OrderServiceImpl#discontinueOrder --calls--> OrderServiceImpl#stopOrder.
|
|
1171
|
+
|
|
1172
|
+
Class-level call scans miss method-to-method calls within a single class, so
|
|
1173
|
+
impact-chain on a private/helper method (e.g. OrderServiceImpl#stopOrder) found
|
|
1174
|
+
zero method-level callers and degraded to a wrong class-level expansion. Resolves
|
|
1175
|
+
bare `m(...)` and `this.m(...)` calls whose target is a sibling METHOD of the same
|
|
1176
|
+
class. Overloads link to all same-name siblings (arity not resolved by regex).
|
|
1177
|
+
Deterministic, in-source only — no cross-file or runtime inference.
|
|
1178
|
+
"""
|
|
1179
|
+
callers = [s for s in symbols if s.symbol_kind in ("method", "constructor") and s.line]
|
|
1180
|
+
if not callers:
|
|
1181
|
+
return []
|
|
1182
|
+
|
|
1183
|
+
# Per-class sibling index: simple method name → [method FQNs]. Constructors are
|
|
1184
|
+
# not call targets (a `new X(...)` site is a different relation).
|
|
1185
|
+
siblings: dict[str, dict[str, list[str]]] = {}
|
|
1186
|
+
for s in symbols:
|
|
1187
|
+
if s.symbol_kind == "method" and "#" in s.symbol:
|
|
1188
|
+
cls = _enclosing_class(s.symbol)
|
|
1189
|
+
name = s.symbol.rsplit("#", 1)[1]
|
|
1190
|
+
siblings.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
|
|
1191
|
+
if not siblings:
|
|
1192
|
+
return []
|
|
1193
|
+
|
|
1194
|
+
raw_lines = source.splitlines()
|
|
1195
|
+
edges: list[RelationEdge] = []
|
|
1196
|
+
for caller in callers:
|
|
1197
|
+
cls = _enclosing_class(caller.symbol)
|
|
1198
|
+
sib = siblings.get(cls)
|
|
1199
|
+
if not sib:
|
|
1200
|
+
continue
|
|
1201
|
+
body = _method_body(raw_lines, caller.line - 1)
|
|
1202
|
+
if not body:
|
|
1203
|
+
continue
|
|
1204
|
+
body = _STRING_LITERAL_RE.sub('', _strip_java_comments(body))
|
|
1205
|
+
for name, fqns in sib.items():
|
|
1206
|
+
if name in _CALL_KEYWORDS:
|
|
1207
|
+
continue
|
|
1208
|
+
# bare `name(` (not preceded by word char or '.') OR explicit `this.name(`
|
|
1209
|
+
pat = (r'(?<![\w.])' + re.escape(name) + r'\s*\('
|
|
1210
|
+
+ r'|\bthis\s*\.\s*' + re.escape(name) + r'\s*\(')
|
|
1211
|
+
if not re.search(pat, body):
|
|
1212
|
+
continue
|
|
1213
|
+
for tgt in fqns:
|
|
1214
|
+
if tgt == caller.symbol:
|
|
1215
|
+
continue # skip self-recursion self-loop
|
|
1216
|
+
edges.append(RelationEdge(
|
|
1217
|
+
from_symbol=caller.symbol,
|
|
1218
|
+
to_symbol=tgt,
|
|
1219
|
+
type="calls",
|
|
1220
|
+
confidence="medium",
|
|
1221
|
+
evidence={"type": "method_call", "value": f"{name}(...)"},
|
|
1222
|
+
))
|
|
1223
|
+
return edges
|
|
1224
|
+
|
|
1225
|
+
|
|
1125
1226
|
def _build_relations(
|
|
1126
1227
|
symbols: list[SymbolRecord],
|
|
1127
1228
|
raw_imports: list[str],
|
|
@@ -1557,6 +1658,9 @@ def _build_relations(
|
|
|
1557
1658
|
evidence={"type": "method_call", "value": f"{_tgt.split('.')[-1]}.…(…)"},
|
|
1558
1659
|
))
|
|
1559
1660
|
|
|
1661
|
+
# ── Intra-class method calls: EnclosingMethod -> calls -> SameClass#sibling ──
|
|
1662
|
+
edges.extend(_intra_class_call_edges(symbols, source))
|
|
1663
|
+
|
|
1560
1664
|
seen: set[tuple[str, str, str]] = set()
|
|
1561
1665
|
unique: list[RelationEdge] = []
|
|
1562
1666
|
for e in edges:
|
|
@@ -70,6 +70,17 @@ _SEVERITY_WEIGHT: dict[str, float] = {
|
|
|
70
70
|
"low": 1.0,
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
# Maps the categorical confidence to a numeric score so impact-chain output
|
|
74
|
+
# carries the same `confidence_score`/`confidence_level` pair as the `impact`
|
|
75
|
+
# command. Deterministic — mirrors impact's confidence banding (high≥0.75,
|
|
76
|
+
# medium≥0.45).
|
|
77
|
+
_CONFIDENCE_SCORE: dict[str, float] = {
|
|
78
|
+
"high": 0.9,
|
|
79
|
+
"medium": 0.6,
|
|
80
|
+
"low": 0.3,
|
|
81
|
+
"unknown": 0.0,
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
|
|
74
85
|
# ---------------------------------------------------------------------------
|
|
75
86
|
# Output model
|
|
@@ -117,10 +128,16 @@ class ImpactChainResult:
|
|
|
117
128
|
impact_findings: list[dict] = field(default_factory=list) # SpringFinding.to_dict() filtered
|
|
118
129
|
analysis_warnings: list[str] = field(default_factory=list)
|
|
119
130
|
risk_level: str = "unknown" # "critical" | "high" | "medium" | "low" | "unknown"
|
|
120
|
-
|
|
131
|
+
risk_score: float = 0.0 # numeric score behind risk_level (parity with `impact`)
|
|
132
|
+
confidence: str = "high" # "high" | "medium" | "low" (legacy field, kept)
|
|
133
|
+
explanation: str = "" # human-readable rationale (parity with `impact`)
|
|
121
134
|
metadata: dict = field(default_factory=dict)
|
|
122
135
|
|
|
123
136
|
def to_dict(self) -> dict:
|
|
137
|
+
# `confidence_level`/`confidence_score` mirror the `impact` command's schema
|
|
138
|
+
# so AI agents parse both commands with one shape. `confidence` (string) is
|
|
139
|
+
# retained for backward compatibility — it equals `confidence_level`.
|
|
140
|
+
confidence_level = self.confidence
|
|
124
141
|
d: dict = {
|
|
125
142
|
"schema_version": self.schema_version,
|
|
126
143
|
"symbol": self.symbol,
|
|
@@ -134,7 +151,11 @@ class ImpactChainResult:
|
|
|
134
151
|
"impact_findings": self.impact_findings,
|
|
135
152
|
"analysis_warnings": self.analysis_warnings,
|
|
136
153
|
"risk_level": self.risk_level,
|
|
154
|
+
"risk_score": self.risk_score,
|
|
137
155
|
"confidence": self.confidence,
|
|
156
|
+
"confidence_level": confidence_level,
|
|
157
|
+
"confidence_score": _CONFIDENCE_SCORE.get(confidence_level, 0.0),
|
|
158
|
+
"explanation": self.explanation,
|
|
138
159
|
"metadata": self.metadata,
|
|
139
160
|
}
|
|
140
161
|
return d
|
|
@@ -680,6 +701,33 @@ def _compute_risk(
|
|
|
680
701
|
return level, round(total, 2)
|
|
681
702
|
|
|
682
703
|
|
|
704
|
+
def _build_chain_explanation(
|
|
705
|
+
risk_level: str,
|
|
706
|
+
direct_callers: int,
|
|
707
|
+
indirect_callers: int,
|
|
708
|
+
endpoints_affected: int,
|
|
709
|
+
findings: int,
|
|
710
|
+
confidence: str,
|
|
711
|
+
) -> str:
|
|
712
|
+
"""Human-readable rationale, phrased to match the `impact` command's
|
|
713
|
+
explanation so both surfaces read consistently."""
|
|
714
|
+
if not direct_callers and not indirect_callers and not endpoints_affected:
|
|
715
|
+
return "No callers or endpoints found in the impact chain. Low-risk isolated change."
|
|
716
|
+
parts: list[str] = []
|
|
717
|
+
if direct_callers:
|
|
718
|
+
parts.append(f"{direct_callers} direct caller{'s' if direct_callers != 1 else ''}")
|
|
719
|
+
if indirect_callers:
|
|
720
|
+
parts.append(f"{indirect_callers} indirect caller{'s' if indirect_callers != 1 else ''}")
|
|
721
|
+
if endpoints_affected:
|
|
722
|
+
parts.append(f"{endpoints_affected} endpoint{'s' if endpoints_affected != 1 else ''} exposed")
|
|
723
|
+
if findings:
|
|
724
|
+
parts.append(f"{findings} TX/SEC finding{'s' if findings != 1 else ''} in chain")
|
|
725
|
+
text = f"Risk={risk_level.upper()}: {'; '.join(parts)}."
|
|
726
|
+
if confidence != "high":
|
|
727
|
+
text += f" (confidence={confidence}: IR may be incomplete)"
|
|
728
|
+
return text
|
|
729
|
+
|
|
730
|
+
|
|
683
731
|
# ---------------------------------------------------------------------------
|
|
684
732
|
# Security surface aggregation
|
|
685
733
|
# ---------------------------------------------------------------------------
|
|
@@ -762,6 +810,7 @@ class ImpactOrchestrator:
|
|
|
762
810
|
analysis_warnings=warnings or [f"Symbol '{symbol}' not found in CIR."],
|
|
763
811
|
risk_level="unknown",
|
|
764
812
|
confidence="low",
|
|
813
|
+
explanation=f"Symbol {symbol!r} not found in the IR.",
|
|
765
814
|
metadata={"analysis_depth": depth},
|
|
766
815
|
)
|
|
767
816
|
|
|
@@ -1041,7 +1090,16 @@ class ImpactOrchestrator:
|
|
|
1041
1090
|
impact_findings=impact_findings,
|
|
1042
1091
|
analysis_warnings=warnings,
|
|
1043
1092
|
risk_level=risk_level,
|
|
1093
|
+
risk_score=risk_score,
|
|
1044
1094
|
confidence=confidence,
|
|
1095
|
+
explanation=_build_chain_explanation(
|
|
1096
|
+
risk_level,
|
|
1097
|
+
len(direct_callers),
|
|
1098
|
+
len(indirect_callers),
|
|
1099
|
+
len(endpoints_affected),
|
|
1100
|
+
len(impact_findings),
|
|
1101
|
+
confidence,
|
|
1102
|
+
),
|
|
1045
1103
|
metadata={
|
|
1046
1104
|
"analysis_depth": depth,
|
|
1047
1105
|
"callers_total": len(direct_callers) + len(indirect_callers),
|
|
@@ -1132,4 +1190,5 @@ def run_impact_chain(
|
|
|
1132
1190
|
analysis_warnings=[f"Internal error: {type(exc).__name__}: {exc}"],
|
|
1133
1191
|
risk_level="unknown",
|
|
1134
1192
|
confidence="low",
|
|
1193
|
+
explanation=f"Internal error during analysis: {type(exc).__name__}.",
|
|
1135
1194
|
)
|
|
@@ -180,6 +180,226 @@ def discover_custom_validators(root: Path) -> "dict[str, CustomConstraint]":
|
|
|
180
180
|
return catalog
|
|
181
181
|
|
|
182
182
|
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# Source-derived constraints (no OpenAPI spec)
|
|
185
|
+
#
|
|
186
|
+
# When a repo ships no OpenAPI spec, declarative DTO constraints still live in
|
|
187
|
+
# the Java source as bean-validation annotations. We recover them directly:
|
|
188
|
+
# locate the handler's validated body parameter (@Valid/@Validated), resolve
|
|
189
|
+
# that DTO class in-repo, and read its fields' constraint annotations. This is
|
|
190
|
+
# pure extraction — it never fabricates constraints, and it is reported with
|
|
191
|
+
# source="source-derived" + a lower confidence than spec-carried constraints.
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
|
|
194
|
+
# jakarta/javax bean-validation built-in constraints. @Valid marks nested
|
|
195
|
+
# validation, which still means "this field is validated".
|
|
196
|
+
_BEAN_CONSTRAINTS = frozenset({
|
|
197
|
+
"NotNull", "NotEmpty", "NotBlank", "Null", "AssertTrue", "AssertFalse",
|
|
198
|
+
"Min", "Max", "DecimalMin", "DecimalMax", "Digits", "Positive",
|
|
199
|
+
"PositiveOrZero", "Negative", "NegativeOrZero", "Size", "Pattern", "Email",
|
|
200
|
+
"Past", "PastOrPresent", "Future", "FutureOrPresent", "Valid",
|
|
201
|
+
})
|
|
202
|
+
_VALIDATE_MARKERS = frozenset({"Valid", "Validated"})
|
|
203
|
+
# A class-typed identifier (starts uppercase) — heuristic for "a DTO type".
|
|
204
|
+
_CLASS_TYPE_RE = re.compile(r"^[A-Z][\w]*$")
|
|
205
|
+
_ANN_TOKEN_RE = re.compile(r"@(\w+)\s*(?:\(([^)]*)\))?")
|
|
206
|
+
_FIELD_DECL_RE = re.compile(
|
|
207
|
+
r"^\s*(?:private|protected|public)\s+"
|
|
208
|
+
r"(?:final\s+|static\s+|transient\s+|volatile\s+)*"
|
|
209
|
+
r"[\w.$<>\[\], ]+?\s+(\w+)\s*[;=]"
|
|
210
|
+
)
|
|
211
|
+
_CLASS_EXTENDS_RE = re.compile(r"\bclass\s+\w+\s+extends\s+(\w+)")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _index_repo_classes(root: Path) -> "dict[str, Path]":
|
|
215
|
+
"""Map a class's simple name → its source file (first non-test match)."""
|
|
216
|
+
index: "dict[str, Path]" = {}
|
|
217
|
+
count = 0
|
|
218
|
+
for jf in root.rglob("*.java"):
|
|
219
|
+
rel = str(jf).replace("\\", "/")
|
|
220
|
+
if is_test_path(rel) or "/target/" in rel:
|
|
221
|
+
continue
|
|
222
|
+
count += 1
|
|
223
|
+
if count > _SCAN_CAP:
|
|
224
|
+
break
|
|
225
|
+
index.setdefault(jf.stem, jf)
|
|
226
|
+
return index
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _balanced_parens(src: str, open_idx: int) -> "Optional[str]":
|
|
230
|
+
"""Given the index of a '(', return the inner text up to its matching ')'."""
|
|
231
|
+
depth = 0
|
|
232
|
+
for i in range(open_idx, len(src)):
|
|
233
|
+
c = src[i]
|
|
234
|
+
if c == "(":
|
|
235
|
+
depth += 1
|
|
236
|
+
elif c == ")":
|
|
237
|
+
depth -= 1
|
|
238
|
+
if depth == 0:
|
|
239
|
+
return src[open_idx + 1:i]
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _split_top_level(params: str) -> "list[str]":
|
|
244
|
+
"""Split a parameter list on top-level commas (ignoring generics/parens)."""
|
|
245
|
+
out: "list[str]" = []
|
|
246
|
+
depth = 0
|
|
247
|
+
buf: "list[str]" = []
|
|
248
|
+
for c in params:
|
|
249
|
+
if c in "<(":
|
|
250
|
+
depth += 1
|
|
251
|
+
elif c in ">)":
|
|
252
|
+
depth -= 1
|
|
253
|
+
if c == "," and depth == 0:
|
|
254
|
+
out.append("".join(buf))
|
|
255
|
+
buf = []
|
|
256
|
+
else:
|
|
257
|
+
buf.append(c)
|
|
258
|
+
if buf:
|
|
259
|
+
out.append("".join(buf))
|
|
260
|
+
return out
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _handler_body_dto(controller_src: str, handler: str) -> "Optional[tuple[str, str]]":
|
|
264
|
+
"""Find ``handler``'s validated body parameter. Returns (dto_simple, binding)
|
|
265
|
+
where binding is 'body' (@RequestBody), 'form' (@ModelAttribute / implicit),
|
|
266
|
+
or None when the handler has no @Valid/@Validated DTO parameter."""
|
|
267
|
+
for m in re.finditer(r"\b" + re.escape(handler) + r"\s*\(", controller_src):
|
|
268
|
+
params = _balanced_parens(controller_src, m.end() - 1)
|
|
269
|
+
if params is None:
|
|
270
|
+
continue
|
|
271
|
+
for raw in _split_top_level(params):
|
|
272
|
+
raw = raw.strip()
|
|
273
|
+
if not raw:
|
|
274
|
+
continue
|
|
275
|
+
anns = {a.group(1) for a in _ANN_TOKEN_RE.finditer(raw)}
|
|
276
|
+
if not (anns & _VALIDATE_MARKERS):
|
|
277
|
+
continue
|
|
278
|
+
# Strip annotations, then read the parameter's declared type.
|
|
279
|
+
without_ann = _ANN_TOKEN_RE.sub("", raw).strip()
|
|
280
|
+
without_ann = re.sub(r"^final\s+", "", without_ann)
|
|
281
|
+
parts = without_ann.split()
|
|
282
|
+
if not parts:
|
|
283
|
+
continue
|
|
284
|
+
dto = re.sub(r"<.*", "", parts[0]).strip()
|
|
285
|
+
if not _CLASS_TYPE_RE.match(dto):
|
|
286
|
+
continue
|
|
287
|
+
binding = "body" if "RequestBody" in anns else "form"
|
|
288
|
+
return dto, binding
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _dto_field_constraints(
|
|
293
|
+
dto: str,
|
|
294
|
+
class_index: "dict[str, Path]",
|
|
295
|
+
catalog: "dict[str, CustomConstraint]",
|
|
296
|
+
_seen: "Optional[set[str]]" = None,
|
|
297
|
+
) -> "list[dict[str, Any]]":
|
|
298
|
+
"""Read a DTO's validated fields (own file + in-repo supertypes, depth-guarded)."""
|
|
299
|
+
if _seen is None:
|
|
300
|
+
_seen = set()
|
|
301
|
+
if dto in _seen or dto not in class_index:
|
|
302
|
+
return []
|
|
303
|
+
_seen.add(dto)
|
|
304
|
+
try:
|
|
305
|
+
src = class_index[dto].read_text(encoding="utf-8", errors="replace")
|
|
306
|
+
except OSError:
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
fields: "list[dict[str, Any]]" = []
|
|
310
|
+
pending: "list[tuple[str, str]]" = [] # (annotation, args)
|
|
311
|
+
for line in src.splitlines():
|
|
312
|
+
stripped = line.strip()
|
|
313
|
+
if stripped.startswith("@"):
|
|
314
|
+
mt = _ANN_TOKEN_RE.match(stripped)
|
|
315
|
+
if mt:
|
|
316
|
+
pending.append((mt.group(1), (mt.group(2) or "").strip()))
|
|
317
|
+
continue
|
|
318
|
+
fm = _FIELD_DECL_RE.match(line)
|
|
319
|
+
if fm:
|
|
320
|
+
rules: "list[dict[str, Any]]" = []
|
|
321
|
+
customs: "list[dict[str, Any]]" = []
|
|
322
|
+
for ann, args in pending:
|
|
323
|
+
if ann in _BEAN_CONSTRAINTS:
|
|
324
|
+
rule: "dict[str, Any]" = {"kind": ann}
|
|
325
|
+
if args:
|
|
326
|
+
rule["value"] = args
|
|
327
|
+
rules.append(rule)
|
|
328
|
+
elif ann in catalog:
|
|
329
|
+
customs.append({"annotation": ann, "resolved": True})
|
|
330
|
+
if rules or customs:
|
|
331
|
+
entry: "dict[str, Any]" = {"name": fm.group(1)}
|
|
332
|
+
if rules:
|
|
333
|
+
entry["rules"] = rules
|
|
334
|
+
if customs:
|
|
335
|
+
entry["customValidators"] = customs
|
|
336
|
+
fields.append(entry)
|
|
337
|
+
pending = []
|
|
338
|
+
elif stripped and not stripped.startswith("//") and not stripped.startswith("*"):
|
|
339
|
+
# Any other meaningful line breaks the annotation→field adjacency.
|
|
340
|
+
pending = []
|
|
341
|
+
|
|
342
|
+
# Follow a single in-repo supertype so inherited constraints are not lost.
|
|
343
|
+
ext = _CLASS_EXTENDS_RE.search(src)
|
|
344
|
+
if ext:
|
|
345
|
+
seen_names = {f["name"] for f in fields}
|
|
346
|
+
for inherited in _dto_field_constraints(ext.group(1), class_index, catalog, _seen):
|
|
347
|
+
if inherited["name"] not in seen_names:
|
|
348
|
+
fields.append(inherited)
|
|
349
|
+
return fields
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _recover_source_endpoints(
|
|
353
|
+
root: Path,
|
|
354
|
+
endpoints: "list[dict[str, Any]]",
|
|
355
|
+
catalog: "dict[str, CustomConstraint]",
|
|
356
|
+
) -> "tuple[list[dict[str, Any]], int]":
|
|
357
|
+
"""Build validation routes for source endpoints whose handler validates a
|
|
358
|
+
DTO. Returns (routes, validated_field_count). Only body-shaped verbs are
|
|
359
|
+
considered, matching the OpenAPI path's scope."""
|
|
360
|
+
class_index = _index_repo_classes(root)
|
|
361
|
+
controller_cache: "dict[str, Optional[str]]" = {}
|
|
362
|
+
routes: "list[dict[str, Any]]" = []
|
|
363
|
+
total = 0
|
|
364
|
+
for ep in endpoints:
|
|
365
|
+
if not _is_body_endpoint(ep):
|
|
366
|
+
continue
|
|
367
|
+
controller = ep.get("controller")
|
|
368
|
+
handler = ep.get("handler")
|
|
369
|
+
if not controller or not handler or controller not in class_index:
|
|
370
|
+
continue
|
|
371
|
+
if controller not in controller_cache:
|
|
372
|
+
try:
|
|
373
|
+
controller_cache[controller] = class_index[controller].read_text(
|
|
374
|
+
encoding="utf-8", errors="replace"
|
|
375
|
+
)
|
|
376
|
+
except OSError:
|
|
377
|
+
controller_cache[controller] = None
|
|
378
|
+
csrc = controller_cache[controller]
|
|
379
|
+
if csrc is None:
|
|
380
|
+
continue
|
|
381
|
+
dto_binding = _handler_body_dto(csrc, str(handler))
|
|
382
|
+
if dto_binding is None:
|
|
383
|
+
continue
|
|
384
|
+
dto, binding = dto_binding
|
|
385
|
+
validated = _dto_field_constraints(dto, class_index, catalog)
|
|
386
|
+
if not validated:
|
|
387
|
+
continue
|
|
388
|
+
total += len(validated)
|
|
389
|
+
routes.append({
|
|
390
|
+
"method": ep.get("method"),
|
|
391
|
+
"path": ep.get("path"),
|
|
392
|
+
"controller": controller,
|
|
393
|
+
"handler": handler,
|
|
394
|
+
"schema": dto,
|
|
395
|
+
"binding": binding,
|
|
396
|
+
"source": "source-derived",
|
|
397
|
+
"confidence": "medium",
|
|
398
|
+
"validatedFields": validated,
|
|
399
|
+
})
|
|
400
|
+
return routes, total
|
|
401
|
+
|
|
402
|
+
|
|
183
403
|
def _field_rules(fieldc: "dict[str, Any]") -> "list[dict[str, Any]]":
|
|
184
404
|
"""Render a constraint dict's built-in rules as a list of {kind, value}."""
|
|
185
405
|
rules: "list[dict[str, Any]]" = []
|
|
@@ -303,18 +523,44 @@ def build_validation_surface(
|
|
|
303
523
|
if spec_path:
|
|
304
524
|
result["openapi_spec"] = spec_path
|
|
305
525
|
else:
|
|
306
|
-
# No OpenAPI spec on disk / under target/generated-sources.
|
|
307
|
-
#
|
|
308
|
-
#
|
|
309
|
-
#
|
|
310
|
-
# "no validation anywhere".
|
|
526
|
+
# No OpenAPI spec on disk / under target/generated-sources. Recover
|
|
527
|
+
# declarative constraints directly from the Java DTOs that handlers
|
|
528
|
+
# validate (@Valid/@Validated body params), so a repo without a spec is
|
|
529
|
+
# no longer reported as a sea of zeros.
|
|
311
530
|
result["openapi_spec"] = None
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
"target/generated-sources). Declarative DTO constraints cannot be "
|
|
315
|
-
"recovered; only source-declared custom validators are reported. "
|
|
316
|
-
"Body-endpoint and validated-field counts will read zero unless an "
|
|
317
|
-
"OpenAPI spec is present — this is expected, not a missing-validation "
|
|
318
|
-
"finding."
|
|
531
|
+
source_routes, source_fields = _recover_source_endpoints(
|
|
532
|
+
root, endpoints_data.get("endpoints", []), catalog
|
|
319
533
|
)
|
|
534
|
+
if source_routes:
|
|
535
|
+
existing = {(r.get("method"), r.get("path")) for r in out_endpoints}
|
|
536
|
+
for r in source_routes:
|
|
537
|
+
if (r.get("method"), r.get("path")) not in existing:
|
|
538
|
+
out_endpoints.append(r)
|
|
539
|
+
total_validated_fields += source_fields
|
|
540
|
+
# Recompute the gaps that depended on the now-recovered routes.
|
|
541
|
+
recovered = {(r.get("method"), r.get("path")) for r in source_routes}
|
|
542
|
+
gaps = [
|
|
543
|
+
g for g in gaps
|
|
544
|
+
if (g.get("method"), g.get("path")) not in recovered
|
|
545
|
+
]
|
|
546
|
+
result["endpoints"] = out_endpoints
|
|
547
|
+
result["gaps"] = gaps
|
|
548
|
+
result["summary"]["endpoints_with_body"] = len(out_endpoints)
|
|
549
|
+
result["summary"]["validated_fields"] = total_validated_fields
|
|
550
|
+
result["summary"]["gaps"] = len(gaps)
|
|
551
|
+
result["summary"]["source_derived_routes"] = len(source_routes)
|
|
552
|
+
result["note"] = (
|
|
553
|
+
"No OpenAPI spec found; constraints were recovered from Java DTO "
|
|
554
|
+
"source (bean-validation annotations on @Valid/@Validated handler "
|
|
555
|
+
"bodies). Routes carry source=\"source-derived\" at medium "
|
|
556
|
+
"confidence. Custom-validator linkage and nested generics may be "
|
|
557
|
+
"partial; OpenAPI-carried constraints would be more complete."
|
|
558
|
+
)
|
|
559
|
+
else:
|
|
560
|
+
result["note"] = (
|
|
561
|
+
"No OpenAPI spec found and no source DTO constraints recovered "
|
|
562
|
+
"(no handler validates an in-repo DTO via @Valid/@Validated, or "
|
|
563
|
+
"the DTOs declare no bean-validation annotations). This is "
|
|
564
|
+
"expected for such repos, not a missing-validation finding."
|
|
565
|
+
)
|
|
320
566
|
return result
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|