sourcecode 1.38.0__tar.gz → 1.39.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.38.0 → sourcecode-1.39.0}/PKG-INFO +6 -3
- {sourcecode-1.38.0 → sourcecode-1.39.0}/README.md +5 -2
- {sourcecode-1.38.0 → sourcecode-1.39.0}/pyproject.toml +1 -1
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/cli.py +101 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/format_contract.py +1 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/registry.py +47 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/openapi_surface.py +32 -0
- sourcecode-1.39.0/src/sourcecode/validation_surface.py +305 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/.gitignore +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/.ruff.toml +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/CHANGELOG.md +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/CONTRIBUTING.md +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/LICENSE +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/SECURITY.md +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/raw +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/file_chunker.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/license.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/migrate_check.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/rename_refactor.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/security_config.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/version_check.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/.temp/cli-latest +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/functions/README.md +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/functions/get-license/index.ts +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/functions/telemetry/index.ts +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/sql/license_event_ordering.sql +0 -0
- {sourcecode-1.38.0 → sourcecode-1.39.0}/supabase/sql/telemetry_events.sql +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.39.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.39.0
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -149,6 +149,9 @@ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
|
149
149
|
# REST endpoint surface
|
|
150
150
|
sourcecode endpoints /path/to/repo
|
|
151
151
|
|
|
152
|
+
# Request-body validation per endpoint: constraints + custom validators (free)
|
|
153
|
+
sourcecode validation /path/to/repo
|
|
154
|
+
|
|
152
155
|
# Onboard to an unfamiliar codebase
|
|
153
156
|
sourcecode onboard /path/to/repo
|
|
154
157
|
|
|
@@ -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.39.0
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
---
|
|
@@ -111,6 +111,9 @@ sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
|
111
111
|
# REST endpoint surface
|
|
112
112
|
sourcecode endpoints /path/to/repo
|
|
113
113
|
|
|
114
|
+
# Request-body validation per endpoint: constraints + custom validators (free)
|
|
115
|
+
sourcecode validation /path/to/repo
|
|
116
|
+
|
|
114
117
|
# Onboard to an unfamiliar codebase
|
|
115
118
|
sourcecode onboard /path/to/repo
|
|
116
119
|
|
|
@@ -230,6 +230,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
230
230
|
"cold-start",
|
|
231
231
|
# Spring semantic audit
|
|
232
232
|
"spring-audit",
|
|
233
|
+
# Request-body validation surface
|
|
234
|
+
"validation",
|
|
233
235
|
# Spring impact chain
|
|
234
236
|
"impact-chain",
|
|
235
237
|
# PR blast-radius report
|
|
@@ -3915,6 +3917,105 @@ def endpoints_cmd(
|
|
|
3915
3917
|
_nudge()
|
|
3916
3918
|
|
|
3917
3919
|
|
|
3920
|
+
@app.command("validation")
|
|
3921
|
+
def validation_cmd(
|
|
3922
|
+
path: Path = typer.Argument(
|
|
3923
|
+
Path("."),
|
|
3924
|
+
help="Repository path to scan for request-body validation (default: current directory)",
|
|
3925
|
+
),
|
|
3926
|
+
output_path: Optional[Path] = typer.Option(
|
|
3927
|
+
None, "--output", "-o",
|
|
3928
|
+
help="Write output to a file instead of stdout.",
|
|
3929
|
+
),
|
|
3930
|
+
format: str = typer.Option(
|
|
3931
|
+
"json",
|
|
3932
|
+
"--format",
|
|
3933
|
+
"-f",
|
|
3934
|
+
help="Output format: json (default) or yaml.",
|
|
3935
|
+
show_default=True,
|
|
3936
|
+
),
|
|
3937
|
+
copy: bool = typer.Option(
|
|
3938
|
+
False,
|
|
3939
|
+
"--copy",
|
|
3940
|
+
"-c",
|
|
3941
|
+
help="Copy output to system clipboard after a successful run.",
|
|
3942
|
+
),
|
|
3943
|
+
path_prefix: Optional[str] = typer.Option(
|
|
3944
|
+
None, "--path-prefix", "-p",
|
|
3945
|
+
help="Filter endpoints whose URL path starts with this prefix.",
|
|
3946
|
+
),
|
|
3947
|
+
gaps_only: bool = typer.Option(
|
|
3948
|
+
False, "--gaps-only",
|
|
3949
|
+
help="Report only endpoints/fields with no declared validation (the gaps section).",
|
|
3950
|
+
),
|
|
3951
|
+
) -> None:
|
|
3952
|
+
"""Map request-body validation per endpoint (constraints + custom validators).
|
|
3953
|
+
|
|
3954
|
+
\b
|
|
3955
|
+
Aggregates two sources of bean-validation truth so an agent knows exactly
|
|
3956
|
+
what a request body must satisfy before touching it:
|
|
3957
|
+
* declarative constraints on the DTOs (@Pattern/@Size/@NotNull, min/max,
|
|
3958
|
+
enum), recovered from the OpenAPI spec even when the DTOs are generated
|
|
3959
|
+
under target/generated-sources (not scanned);
|
|
3960
|
+
* hand-written custom validators (@Constraint + ConstraintValidator, e.g.
|
|
3961
|
+
PetAgeValidator), linked to fields via x-field-extra-annotation.
|
|
3962
|
+
|
|
3963
|
+
\b
|
|
3964
|
+
Output (JSON): per-endpoint validatedFields with their rules + custom
|
|
3965
|
+
validators, the discovered custom-validator catalog, and the set of body
|
|
3966
|
+
endpoints with no declared validation (gaps).
|
|
3967
|
+
|
|
3968
|
+
\b
|
|
3969
|
+
Examples:
|
|
3970
|
+
sourcecode validation .
|
|
3971
|
+
sourcecode validation . --gaps-only
|
|
3972
|
+
sourcecode validation . --path-prefix /owners
|
|
3973
|
+
sourcecode validation . --format yaml
|
|
3974
|
+
"""
|
|
3975
|
+
_enforce_format("validation", format)
|
|
3976
|
+
|
|
3977
|
+
target = path.resolve()
|
|
3978
|
+
if not target.exists() or not target.is_dir():
|
|
3979
|
+
_emit_error_json(
|
|
3980
|
+
INVALID_INPUT_CODE,
|
|
3981
|
+
f"'{target}' is not a valid directory.",
|
|
3982
|
+
path=str(target),
|
|
3983
|
+
hint="Pass an existing repository directory.",
|
|
3984
|
+
expected="A directory path.",
|
|
3985
|
+
)
|
|
3986
|
+
raise typer.Exit(code=1)
|
|
3987
|
+
|
|
3988
|
+
from sourcecode.validation_surface import build_validation_surface
|
|
3989
|
+
data = build_validation_surface(target)
|
|
3990
|
+
|
|
3991
|
+
if path_prefix:
|
|
3992
|
+
data["endpoints"] = [
|
|
3993
|
+
e for e in data.get("endpoints", [])
|
|
3994
|
+
if str(e.get("path", "")).startswith(path_prefix)
|
|
3995
|
+
]
|
|
3996
|
+
data["gaps"] = [
|
|
3997
|
+
g for g in data.get("gaps", [])
|
|
3998
|
+
if str(g.get("path", "")).startswith(path_prefix)
|
|
3999
|
+
]
|
|
4000
|
+
if gaps_only:
|
|
4001
|
+
data = {
|
|
4002
|
+
"gaps": data.get("gaps", []),
|
|
4003
|
+
"summary": data.get("summary", {}),
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
output = _serialize_dict(data, format)
|
|
4007
|
+
_summary = data.get("summary", {})
|
|
4008
|
+
_emit_command_output(
|
|
4009
|
+
output, output_path, copy,
|
|
4010
|
+
success_msg=f"Validation surface written to {output_path} "
|
|
4011
|
+
f"({_summary.get('endpoints_with_body', 0)} body endpoints, "
|
|
4012
|
+
f"{_summary.get('gaps', 0)} gaps)",
|
|
4013
|
+
)
|
|
4014
|
+
|
|
4015
|
+
from sourcecode.mcp_nudge import nudge_mcp_if_needed as _nudge
|
|
4016
|
+
_nudge()
|
|
4017
|
+
|
|
4018
|
+
|
|
3918
4019
|
# ── Spring Semantic Audit ─────────────────────────────────────────────────────
|
|
3919
4020
|
|
|
3920
4021
|
|
|
@@ -27,6 +27,7 @@ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
|
|
|
27
27
|
"repo-ir": ("json", "yaml"),
|
|
28
28
|
"impact": ("json", "yaml"),
|
|
29
29
|
"endpoints": ("json", "yaml"),
|
|
30
|
+
"validation": ("json", "yaml"),
|
|
30
31
|
"impact-chain": ("json", "yaml"),
|
|
31
32
|
"pr-impact": ("text", "json"),
|
|
32
33
|
"migrate-check": ("json", "text"),
|
|
@@ -864,6 +864,31 @@ Returns: endpoints list with method, path, controller, handler fields;
|
|
|
864
864
|
"unknown" (no security signals detected).
|
|
865
865
|
Supports Spring MVC (@GetMapping etc.) and JAX-RS (@GET/@POST etc.).
|
|
866
866
|
repo_path: absolute path to the Java repository (default: current working directory).
|
|
867
|
+
"""
|
|
868
|
+
|
|
869
|
+
_GET_VALIDATION_DOC = """\
|
|
870
|
+
Request-body validation surface per endpoint. JAVA/SPRING ONLY.
|
|
871
|
+
|
|
872
|
+
Do NOT call this on non-Java repositories — it will return empty results.
|
|
873
|
+
|
|
874
|
+
Combines two sources of bean-validation truth so you know what a request body
|
|
875
|
+
must satisfy before generating a payload, a test, or reasoning about a 400:
|
|
876
|
+
* declarative constraints on the DTOs (@Pattern/@Size/@NotNull, minimum/maximum,
|
|
877
|
+
enum) — recovered from the OpenAPI spec even when DTOs are generated under
|
|
878
|
+
target/generated-sources (not scanned);
|
|
879
|
+
* hand-written custom validators (@Constraint + ConstraintValidator, e.g.
|
|
880
|
+
PetAgeValidator), linked to fields via x-field-extra-annotation.
|
|
881
|
+
|
|
882
|
+
Maps to: sourcecode validation <repo_path>
|
|
883
|
+
Returns: endpoints[] (method, path, controller, handler, schema, validatedFields[
|
|
884
|
+
{name, rules[{kind,value}], customValidators[{annotation,validators,message,resolved}]}]),
|
|
885
|
+
custom_validators[] (catalog: annotation, validators, message, validatedTypes, targets),
|
|
886
|
+
gaps[] (POST/PUT/PATCH endpoints with no declared validation),
|
|
887
|
+
summary, openapi_spec.
|
|
888
|
+
An unresolved custom annotation (referenced in the spec, no validator in source)
|
|
889
|
+
is reported with resolved=false.
|
|
890
|
+
repo_path: absolute path to the Java repository (default: current working directory).
|
|
891
|
+
gaps_only: when true, return only the gaps section (endpoints lacking validation).
|
|
867
892
|
"""
|
|
868
893
|
|
|
869
894
|
_CACHE_STATUS_DOC = """\
|
|
@@ -1083,6 +1108,27 @@ repo_path: absolute path to the repository (default: current working directory).
|
|
|
1083
1108
|
docstring_override=_GET_ENDPOINTS_DOC,
|
|
1084
1109
|
),
|
|
1085
1110
|
|
|
1111
|
+
# --- get_validation: clean alias replacing raw canonical (6 CLI params) ---
|
|
1112
|
+
_alias_spec(
|
|
1113
|
+
"get_validation",
|
|
1114
|
+
"Request-body validation surface per endpoint (constraints + custom validators). JAVA/SPRING ONLY.",
|
|
1115
|
+
("validation",),
|
|
1116
|
+
(
|
|
1117
|
+
ToolParamSpec("repo_path", "argument", str, required=False, default=".", is_path=True),
|
|
1118
|
+
ToolParamSpec("gaps_only", "option", bool, required=False, default=False,
|
|
1119
|
+
option_names=("--gaps-only",), is_flag=True,
|
|
1120
|
+
help="Return only endpoints/fields lacking validation."),
|
|
1121
|
+
),
|
|
1122
|
+
lambda inputs: (
|
|
1123
|
+
["validation", str(inputs.get("repo_path", "."))]
|
|
1124
|
+
+ (["--gaps-only"] if bool(inputs.get("gaps_only")) else [])
|
|
1125
|
+
),
|
|
1126
|
+
supported_targets=("repo_path",),
|
|
1127
|
+
unsupported_targets=("file_path",),
|
|
1128
|
+
validator=validate_repo_path,
|
|
1129
|
+
docstring_override=_GET_VALIDATION_DOC,
|
|
1130
|
+
),
|
|
1131
|
+
|
|
1086
1132
|
# --- cache management: curated aliases stripping CLI noise params ---
|
|
1087
1133
|
_alias_spec(
|
|
1088
1134
|
"cache_status",
|
|
@@ -1214,6 +1260,7 @@ _MCP_HIDDEN_CANONICAL_TOOLS: frozenset[str] = frozenset({
|
|
|
1214
1260
|
"modernize", # duplicate of modernize_context
|
|
1215
1261
|
# Raw CLI tools with output-format/noise params — clean alias with only repo_path exists
|
|
1216
1262
|
"endpoints", # 7 CLI params (output_path/format/copy/etc.); use get_endpoints
|
|
1263
|
+
"validation", # 6 CLI params (output_path/format/copy/path_prefix/gaps_only); use get_validation
|
|
1217
1264
|
"cache_status", # path + json_output flag; curated alias strips json_output, renames path→repo_path
|
|
1218
1265
|
"cache_warm", # path + compact/agent output flags; curated alias keeps only repo_path
|
|
1219
1266
|
"cache_clear", # path + yes/all_ destructive flags; curated alias keeps repo_path + include_ris only
|
|
@@ -68,6 +68,9 @@ class FieldConstraint:
|
|
|
68
68
|
fmt: Optional[str] = None
|
|
69
69
|
enum: Optional[list[Any]] = None
|
|
70
70
|
ref: Optional[str] = None # schema name when the field is an object/array ref
|
|
71
|
+
# Custom bean-validation annotations injected via openapi-generator's
|
|
72
|
+
# ``x-field-extra-annotation`` vendor extension (simple class names).
|
|
73
|
+
extra_annotations: "list[str]" = field(default_factory=list)
|
|
71
74
|
|
|
72
75
|
def to_dict(self) -> "dict[str, Any]":
|
|
73
76
|
out: "dict[str, Any]" = {"name": self.name, "required": self.required}
|
|
@@ -84,6 +87,8 @@ class FieldConstraint:
|
|
|
84
87
|
):
|
|
85
88
|
if val is not None:
|
|
86
89
|
out[key] = val
|
|
90
|
+
if self.extra_annotations:
|
|
91
|
+
out["extraAnnotations"] = self.extra_annotations
|
|
87
92
|
return out
|
|
88
93
|
|
|
89
94
|
|
|
@@ -255,6 +260,32 @@ def _ref_name(ref: Any) -> Optional[str]:
|
|
|
255
260
|
return None
|
|
256
261
|
|
|
257
262
|
|
|
263
|
+
def _extra_annotations(prop: "dict[str, Any]") -> "list[str]":
|
|
264
|
+
"""Extract simple class names from ``x-field-extra-annotation``.
|
|
265
|
+
|
|
266
|
+
openapi-generator injects custom bean-validation annotations on generated DTO
|
|
267
|
+
fields via this vendor extension, e.g.
|
|
268
|
+
``x-field-extra-annotation: "@com.x.PetAgeValidation"`` (string) or a list of
|
|
269
|
+
such entries. We keep the trailing simple name (``PetAgeValidation``).
|
|
270
|
+
"""
|
|
271
|
+
import re as _re
|
|
272
|
+
|
|
273
|
+
raw = prop.get("x-field-extra-annotation")
|
|
274
|
+
if raw is None:
|
|
275
|
+
return []
|
|
276
|
+
items = raw if isinstance(raw, list) else [raw]
|
|
277
|
+
names: "list[str]" = []
|
|
278
|
+
for item in items:
|
|
279
|
+
if not isinstance(item, str):
|
|
280
|
+
continue
|
|
281
|
+
# Each entry may carry several annotations; grab every @Name token.
|
|
282
|
+
for m in _re.finditer(r"@\s*([\w.]+)", item):
|
|
283
|
+
simple = m.group(1).rsplit(".", 1)[-1]
|
|
284
|
+
if simple and simple not in names:
|
|
285
|
+
names.append(simple)
|
|
286
|
+
return names
|
|
287
|
+
|
|
288
|
+
|
|
258
289
|
def _field_from_property(name: str, prop: Any, required: bool) -> FieldConstraint:
|
|
259
290
|
fc = FieldConstraint(name=name, required=required)
|
|
260
291
|
if not isinstance(prop, dict):
|
|
@@ -276,6 +307,7 @@ def _field_from_property(name: str, prop: Any, required: bool) -> FieldConstrain
|
|
|
276
307
|
enum = prop.get("enum")
|
|
277
308
|
if isinstance(enum, list):
|
|
278
309
|
fc.enum = list(enum)
|
|
310
|
+
fc.extra_annotations = _extra_annotations(prop)
|
|
279
311
|
if fc.type is None and prop.get("type") == "array":
|
|
280
312
|
items = prop.get("items")
|
|
281
313
|
if isinstance(items, dict):
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Validation surface extraction (Phase 20).
|
|
2
|
+
|
|
3
|
+
Combines the two sources of bean-validation truth in a Spring repo into one
|
|
4
|
+
per-endpoint view an agent can reason about before touching a request body:
|
|
5
|
+
|
|
6
|
+
1. **Declarative constraints** carried by the OpenAPI spec DTOs (``pattern``,
|
|
7
|
+
``minLength``/``maxLength``, ``required``, ``minimum``/``maximum``, ``enum``)
|
|
8
|
+
— recovered by :mod:`sourcecode.openapi_surface` (Phase 18). These map to
|
|
9
|
+
``@Pattern``/``@Size``/``@NotNull`` on the generated DTOs.
|
|
10
|
+
2. **Custom constraint validators** hand-written in ``src`` — a ``@Constraint``
|
|
11
|
+
meta-annotation plus its ``ConstraintValidator`` implementation (e.g.
|
|
12
|
+
``PetAgeValidator``) — linked to DTO fields through openapi-generator's
|
|
13
|
+
``x-field-extra-annotation`` vendor extension.
|
|
14
|
+
|
|
15
|
+
The output is a per-endpoint validation surface (which body fields are validated
|
|
16
|
+
and how), the custom-validator catalog discovered in source, and the set of
|
|
17
|
+
validation gaps (body endpoints with no declared constraint at all).
|
|
18
|
+
|
|
19
|
+
Design notes mirror :mod:`sourcecode.openapi_surface`: pure extraction (never a
|
|
20
|
+
conformance check), defensive (malformed input yields a partial surface, never an
|
|
21
|
+
exception), and deterministic ordering for stable JSON.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
from dataclasses import dataclass, field
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Optional
|
|
30
|
+
|
|
31
|
+
from sourcecode.path_filters import is_test_path
|
|
32
|
+
|
|
33
|
+
# Built-in constraint keys (as emitted by FieldConstraint.to_dict) that count as
|
|
34
|
+
# "this field is validated".
|
|
35
|
+
_BUILTIN_CONSTRAINT_KEYS = (
|
|
36
|
+
"pattern",
|
|
37
|
+
"minLength",
|
|
38
|
+
"maxLength",
|
|
39
|
+
"minimum",
|
|
40
|
+
"maximum",
|
|
41
|
+
"enum",
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# How many java files to scan for custom validators, to stay fast on big repos.
|
|
45
|
+
_SCAN_CAP = 5000
|
|
46
|
+
|
|
47
|
+
_CONSTRAINT_RE = re.compile(
|
|
48
|
+
r"@Constraint\s*\(\s*validatedBy\s*=\s*\{?([^)}]*)\}?\s*\)", re.DOTALL
|
|
49
|
+
)
|
|
50
|
+
_INTERFACE_RE = re.compile(r"@interface\s+(\w+)")
|
|
51
|
+
_MESSAGE_RE = re.compile(
|
|
52
|
+
r"String\s+message\s*\(\s*\)\s*default\s*\"([^\"]*)\"", re.DOTALL
|
|
53
|
+
)
|
|
54
|
+
_TARGET_RE = re.compile(r"@Target\s*\(\s*\{?([^)}]*)\}?\s*\)", re.DOTALL)
|
|
55
|
+
_VALIDATOR_IMPL_RE = re.compile(
|
|
56
|
+
r"class\s+(\w+)\s+implements\s+ConstraintValidator\s*<\s*(\w+)\s*,\s*([\w.<>\[\] ]+?)\s*>",
|
|
57
|
+
re.DOTALL,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class CustomConstraint:
|
|
63
|
+
"""A hand-written bean-validation constraint discovered in source."""
|
|
64
|
+
|
|
65
|
+
name: str # the @interface annotation, e.g. "PetAgeValidation"
|
|
66
|
+
validators: "list[str]" = field(default_factory=list) # ConstraintValidator impls
|
|
67
|
+
message: Optional[str] = None # default message template
|
|
68
|
+
validated_types: "list[str]" = field(default_factory=list) # T in <A, T>
|
|
69
|
+
targets: "list[str]" = field(default_factory=list) # @Target element types
|
|
70
|
+
source_file: Optional[str] = None
|
|
71
|
+
|
|
72
|
+
def to_dict(self) -> "dict[str, Any]":
|
|
73
|
+
out: "dict[str, Any]" = {"annotation": self.name}
|
|
74
|
+
if self.validators:
|
|
75
|
+
out["validators"] = self.validators
|
|
76
|
+
if self.message is not None:
|
|
77
|
+
out["message"] = self.message
|
|
78
|
+
if self.validated_types:
|
|
79
|
+
out["validatedTypes"] = self.validated_types
|
|
80
|
+
if self.targets:
|
|
81
|
+
out["targets"] = self.targets
|
|
82
|
+
if self.source_file:
|
|
83
|
+
out["sourceFile"] = self.source_file
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _simple(name: str) -> str:
|
|
88
|
+
return name.rsplit(".", 1)[-1].strip()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _split_class_list(raw: str) -> "list[str]":
|
|
92
|
+
"""Parse ``A.class, B.class`` (or a single entry) into simple class names."""
|
|
93
|
+
out: "list[str]" = []
|
|
94
|
+
for tok in raw.split(","):
|
|
95
|
+
tok = tok.strip()
|
|
96
|
+
if not tok:
|
|
97
|
+
continue
|
|
98
|
+
tok = re.sub(r"\.class\b", "", tok)
|
|
99
|
+
simple = _simple(tok)
|
|
100
|
+
if simple and simple not in out:
|
|
101
|
+
out.append(simple)
|
|
102
|
+
return out
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def discover_custom_validators(root: Path) -> "dict[str, CustomConstraint]":
|
|
106
|
+
"""Scan non-test Java source for custom ``@Constraint`` validators.
|
|
107
|
+
|
|
108
|
+
Returns a map keyed by the annotation's simple name. Never raises; an
|
|
109
|
+
unreadable or malformed file is skipped.
|
|
110
|
+
"""
|
|
111
|
+
root = Path(root)
|
|
112
|
+
catalog: "dict[str, CustomConstraint]" = {}
|
|
113
|
+
# annotation name -> validated types, harvested from ConstraintValidator impls.
|
|
114
|
+
impl_types: "dict[str, list[str]]" = {}
|
|
115
|
+
impl_validators: "dict[str, list[str]]" = {}
|
|
116
|
+
|
|
117
|
+
scanned = 0
|
|
118
|
+
for p in sorted(root.rglob("*.java")):
|
|
119
|
+
if scanned >= _SCAN_CAP:
|
|
120
|
+
break
|
|
121
|
+
norm = str(p).replace("\\", "/")
|
|
122
|
+
if is_test_path(norm) or "/target/" in norm or "/build/" in norm:
|
|
123
|
+
continue
|
|
124
|
+
try:
|
|
125
|
+
text = p.read_text(encoding="utf-8")
|
|
126
|
+
except (OSError, UnicodeDecodeError):
|
|
127
|
+
continue
|
|
128
|
+
scanned += 1
|
|
129
|
+
|
|
130
|
+
if "@Constraint" not in text and "ConstraintValidator" not in text:
|
|
131
|
+
continue
|
|
132
|
+
rel = norm[len(str(root).replace("\\", "/")) :].lstrip("/") or norm
|
|
133
|
+
|
|
134
|
+
# @interface declarations carrying @Constraint(validatedBy = ...).
|
|
135
|
+
for m in _CONSTRAINT_RE.finditer(text):
|
|
136
|
+
tail = text[m.end() :]
|
|
137
|
+
iface = _INTERFACE_RE.search(tail)
|
|
138
|
+
if not iface:
|
|
139
|
+
continue
|
|
140
|
+
ann_name = iface.group(1)
|
|
141
|
+
cc = catalog.setdefault(ann_name, CustomConstraint(name=ann_name))
|
|
142
|
+
cc.source_file = cc.source_file or rel
|
|
143
|
+
for v in _split_class_list(m.group(1)):
|
|
144
|
+
if v not in cc.validators:
|
|
145
|
+
cc.validators.append(v)
|
|
146
|
+
# Default message + @Target sit within this @interface body.
|
|
147
|
+
body = tail[iface.end() :]
|
|
148
|
+
msg = _MESSAGE_RE.search(body)
|
|
149
|
+
if msg and cc.message is None:
|
|
150
|
+
cc.message = msg.group(1)
|
|
151
|
+
tgt = _TARGET_RE.search(text[: m.start()][-400:] + tail[: iface.end()])
|
|
152
|
+
if tgt:
|
|
153
|
+
for t in tgt.group(1).split(","):
|
|
154
|
+
t = _simple(t)
|
|
155
|
+
if t and t not in cc.targets:
|
|
156
|
+
cc.targets.append(t)
|
|
157
|
+
|
|
158
|
+
# ConstraintValidator<Annotation, Type> implementations.
|
|
159
|
+
for m in _VALIDATOR_IMPL_RE.finditer(text):
|
|
160
|
+
validator_cls, ann_name, vtype = m.group(1), m.group(2), _simple(m.group(3))
|
|
161
|
+
impl_types.setdefault(ann_name, [])
|
|
162
|
+
if vtype and vtype not in impl_types[ann_name]:
|
|
163
|
+
impl_types[ann_name].append(vtype)
|
|
164
|
+
impl_validators.setdefault(ann_name, [])
|
|
165
|
+
if validator_cls not in impl_validators[ann_name]:
|
|
166
|
+
impl_validators[ann_name].append(validator_cls)
|
|
167
|
+
|
|
168
|
+
# Fold validator-impl findings back into the catalog (handles validators
|
|
169
|
+
# declared in a different file than the annotation).
|
|
170
|
+
for ann_name, types in impl_types.items():
|
|
171
|
+
cc = catalog.setdefault(ann_name, CustomConstraint(name=ann_name))
|
|
172
|
+
for t in types:
|
|
173
|
+
if t not in cc.validated_types:
|
|
174
|
+
cc.validated_types.append(t)
|
|
175
|
+
for ann_name, vals in impl_validators.items():
|
|
176
|
+
cc = catalog.setdefault(ann_name, CustomConstraint(name=ann_name))
|
|
177
|
+
for v in vals:
|
|
178
|
+
if v not in cc.validators:
|
|
179
|
+
cc.validators.append(v)
|
|
180
|
+
return catalog
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _field_rules(fieldc: "dict[str, Any]") -> "list[dict[str, Any]]":
|
|
184
|
+
"""Render a constraint dict's built-in rules as a list of {kind, value}."""
|
|
185
|
+
rules: "list[dict[str, Any]]" = []
|
|
186
|
+
if fieldc.get("required"):
|
|
187
|
+
rules.append({"kind": "required"})
|
|
188
|
+
for key in _BUILTIN_CONSTRAINT_KEYS:
|
|
189
|
+
if key in fieldc:
|
|
190
|
+
rules.append({"kind": key, "value": fieldc[key]})
|
|
191
|
+
return rules
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _is_body_endpoint(ep: "dict[str, Any]") -> bool:
|
|
195
|
+
method = str(ep.get("method", "")).upper()
|
|
196
|
+
return method in ("POST", "PUT", "PATCH")
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def build_validation_surface(
|
|
200
|
+
root: Path,
|
|
201
|
+
endpoints_data: "Optional[dict[str, Any]]" = None,
|
|
202
|
+
) -> "dict[str, Any]":
|
|
203
|
+
"""Build the per-endpoint validation surface for a repo.
|
|
204
|
+
|
|
205
|
+
``endpoints_data`` is the result of :func:`extract_java_endpoints`; when not
|
|
206
|
+
supplied it is computed (so the command can run standalone). Returns a JSON-
|
|
207
|
+
ready dict: ``endpoints`` (validated fields per route), ``custom_validators``
|
|
208
|
+
(catalog), ``gaps`` (body routes with no declared constraint), and ``summary``.
|
|
209
|
+
"""
|
|
210
|
+
root = Path(root)
|
|
211
|
+
if endpoints_data is None:
|
|
212
|
+
from sourcecode.repository_ir import extract_java_endpoints
|
|
213
|
+
|
|
214
|
+
endpoints_data = extract_java_endpoints(root)
|
|
215
|
+
|
|
216
|
+
catalog = discover_custom_validators(root)
|
|
217
|
+
|
|
218
|
+
out_endpoints: "list[dict[str, Any]]" = []
|
|
219
|
+
gaps: "list[dict[str, Any]]" = []
|
|
220
|
+
custom_used: "set[str]" = set()
|
|
221
|
+
total_validated_fields = 0
|
|
222
|
+
|
|
223
|
+
for ep in endpoints_data.get("endpoints", []):
|
|
224
|
+
body = ep.get("request_body")
|
|
225
|
+
if not isinstance(body, dict):
|
|
226
|
+
# A body-shaped verb with no recovered request body is a blind spot
|
|
227
|
+
# worth flagging, but only when we resolved the route from the spec.
|
|
228
|
+
if _is_body_endpoint(ep) and ep.get("source") == "openapi-spec":
|
|
229
|
+
gaps.append(
|
|
230
|
+
{
|
|
231
|
+
"method": ep.get("method"),
|
|
232
|
+
"path": ep.get("path"),
|
|
233
|
+
"controller": ep.get("controller"),
|
|
234
|
+
"reason": "no_request_body_constraints",
|
|
235
|
+
}
|
|
236
|
+
)
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
constraints = body.get("constraints") or []
|
|
240
|
+
validated_fields: "list[dict[str, Any]]" = []
|
|
241
|
+
for fc in constraints:
|
|
242
|
+
if not isinstance(fc, dict):
|
|
243
|
+
continue
|
|
244
|
+
rules = _field_rules(fc)
|
|
245
|
+
customs: "list[dict[str, Any]]" = []
|
|
246
|
+
for ann in fc.get("extraAnnotations", []) or []:
|
|
247
|
+
cc = catalog.get(ann)
|
|
248
|
+
entry: "dict[str, Any]" = {"annotation": ann}
|
|
249
|
+
if cc is not None:
|
|
250
|
+
custom_used.add(ann)
|
|
251
|
+
if cc.validators:
|
|
252
|
+
entry["validators"] = cc.validators
|
|
253
|
+
if cc.message is not None:
|
|
254
|
+
entry["message"] = cc.message
|
|
255
|
+
entry["resolved"] = True
|
|
256
|
+
else:
|
|
257
|
+
entry["resolved"] = False
|
|
258
|
+
customs.append(entry)
|
|
259
|
+
if not rules and not customs:
|
|
260
|
+
continue
|
|
261
|
+
field_entry: "dict[str, Any]" = {"name": fc.get("name")}
|
|
262
|
+
if rules:
|
|
263
|
+
field_entry["rules"] = rules
|
|
264
|
+
if customs:
|
|
265
|
+
field_entry["customValidators"] = customs
|
|
266
|
+
validated_fields.append(field_entry)
|
|
267
|
+
|
|
268
|
+
total_validated_fields += len(validated_fields)
|
|
269
|
+
route = {
|
|
270
|
+
"method": ep.get("method"),
|
|
271
|
+
"path": ep.get("path"),
|
|
272
|
+
"controller": ep.get("controller"),
|
|
273
|
+
"handler": ep.get("handler"),
|
|
274
|
+
"schema": body.get("schema"),
|
|
275
|
+
"validatedFields": validated_fields,
|
|
276
|
+
}
|
|
277
|
+
out_endpoints.append(route)
|
|
278
|
+
if _is_body_endpoint(ep) and not validated_fields:
|
|
279
|
+
gaps.append(
|
|
280
|
+
{
|
|
281
|
+
"method": ep.get("method"),
|
|
282
|
+
"path": ep.get("path"),
|
|
283
|
+
"controller": ep.get("controller"),
|
|
284
|
+
"schema": body.get("schema"),
|
|
285
|
+
"reason": "no_validated_fields",
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
custom_list = [catalog[k].to_dict() for k in sorted(catalog)]
|
|
290
|
+
result: "dict[str, Any]" = {
|
|
291
|
+
"endpoints": out_endpoints,
|
|
292
|
+
"custom_validators": custom_list,
|
|
293
|
+
"gaps": gaps,
|
|
294
|
+
"summary": {
|
|
295
|
+
"endpoints_with_body": len(out_endpoints),
|
|
296
|
+
"validated_fields": total_validated_fields,
|
|
297
|
+
"custom_validators_declared": len(custom_list),
|
|
298
|
+
"custom_validators_linked": len(custom_used),
|
|
299
|
+
"gaps": len(gaps),
|
|
300
|
+
},
|
|
301
|
+
}
|
|
302
|
+
spec_path = endpoints_data.get("openapi_spec")
|
|
303
|
+
if spec_path:
|
|
304
|
+
result["openapi_spec"] = spec_path
|
|
305
|
+
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
|