sourcecode 1.36.5__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.
Files changed (121) hide show
  1. {sourcecode-1.36.5 → sourcecode-1.39.0}/PKG-INFO +6 -3
  2. {sourcecode-1.36.5 → sourcecode-1.39.0}/README.md +5 -2
  3. {sourcecode-1.36.5 → sourcecode-1.39.0}/pyproject.toml +1 -1
  4. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/cli.py +137 -89
  6. sourcecode-1.39.0/src/sourcecode/format_contract.py +87 -0
  7. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/registry.py +47 -0
  8. sourcecode-1.39.0/src/sourcecode/openapi_surface.py +463 -0
  9. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/repository_ir.py +75 -9
  10. sourcecode-1.39.0/src/sourcecode/validation_surface.py +305 -0
  11. {sourcecode-1.36.5 → sourcecode-1.39.0}/.github/workflows/build-windows.yml +0 -0
  12. {sourcecode-1.36.5 → sourcecode-1.39.0}/.gitignore +0 -0
  13. {sourcecode-1.36.5 → sourcecode-1.39.0}/.ruff.toml +0 -0
  14. {sourcecode-1.36.5 → sourcecode-1.39.0}/CHANGELOG.md +0 -0
  15. {sourcecode-1.36.5 → sourcecode-1.39.0}/CONTRIBUTING.md +0 -0
  16. {sourcecode-1.36.5 → sourcecode-1.39.0}/LICENSE +0 -0
  17. {sourcecode-1.36.5 → sourcecode-1.39.0}/SECURITY.md +0 -0
  18. {sourcecode-1.36.5 → sourcecode-1.39.0}/raw +0 -0
  19. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/adaptive_scanner.py +0 -0
  20. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/architecture_analyzer.py +0 -0
  21. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/architecture_summary.py +0 -0
  22. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/ast_extractor.py +0 -0
  23. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/cache.py +0 -0
  24. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/canonical_ir.py +0 -0
  25. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/cir_graphs.py +0 -0
  26. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/classifier.py +0 -0
  27. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  28. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/confidence_analyzer.py +0 -0
  29. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/context_scorer.py +0 -0
  30. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/context_summarizer.py +0 -0
  31. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/contract_model.py +0 -0
  32. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/contract_pipeline.py +0 -0
  33. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/coverage_parser.py +0 -0
  34. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/dependency_analyzer.py +0 -0
  35. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/__init__.py +0 -0
  36. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/base.py +0 -0
  37. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/csproj_parser.py +0 -0
  38. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/dart.py +0 -0
  39. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/dotnet.py +0 -0
  40. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/elixir.py +0 -0
  41. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/go.py +0 -0
  42. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/heuristic.py +0 -0
  43. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/hybrid.py +0 -0
  44. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/java.py +0 -0
  45. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  46. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/nodejs.py +0 -0
  47. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/parsers.py +0 -0
  48. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/php.py +0 -0
  49. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/project.py +0 -0
  50. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/python.py +0 -0
  51. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/ruby.py +0 -0
  52. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/rust.py +0 -0
  53. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/systems.py +0 -0
  54. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/terraform.py +0 -0
  55. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/detectors/tooling.py +0 -0
  56. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/doc_analyzer.py +0 -0
  57. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/entrypoint_classifier.py +0 -0
  58. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/env_analyzer.py +0 -0
  59. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/error_schema.py +0 -0
  60. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/explain.py +0 -0
  61. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/file_chunker.py +0 -0
  62. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/flow_analyzer.py +0 -0
  64. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/fqn_utils.py +0 -0
  65. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/git_analyzer.py +0 -0
  66. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/graph_analyzer.py +0 -0
  67. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/license.py +0 -0
  68. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/__init__.py +0 -0
  69. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  70. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  71. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  72. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  73. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  74. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/orchestrator.py +0 -0
  75. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/runner.py +0 -0
  76. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp/server.py +0 -0
  77. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/mcp_nudge.py +0 -0
  78. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/metrics_analyzer.py +0 -0
  79. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/migrate_check.py +0 -0
  80. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/output_budget.py +0 -0
  81. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/path_filters.py +0 -0
  82. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/pr_comment_renderer.py +0 -0
  83. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/pr_impact.py +0 -0
  84. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/prepare_context.py +0 -0
  85. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/progress.py +0 -0
  86. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/ranking_engine.py +0 -0
  87. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/redactor.py +0 -0
  88. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/relevance_scorer.py +0 -0
  89. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/rename_refactor.py +0 -0
  90. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/repo_classifier.py +0 -0
  91. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/ris.py +0 -0
  92. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/runtime_classifier.py +0 -0
  93. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/scanner.py +0 -0
  94. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/schema.py +0 -0
  95. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/security_config.py +0 -0
  96. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/semantic_analyzer.py +0 -0
  97. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/serializer.py +0 -0
  98. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_event_topology.py +0 -0
  99. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_findings.py +0 -0
  100. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_impact.py +0 -0
  101. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_model.py +0 -0
  102. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_security_audit.py +0 -0
  103. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_semantic.py +0 -0
  104. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/spring_tx_analyzer.py +0 -0
  105. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/summarizer.py +0 -0
  106. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/__init__.py +0 -0
  107. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/config.py +0 -0
  108. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/consent.py +0 -0
  109. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/events.py +0 -0
  110. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/filters.py +0 -0
  111. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/telemetry/transport.py +0 -0
  112. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/tree_utils.py +0 -0
  113. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/version_check.py +0 -0
  114. {sourcecode-1.36.5 → sourcecode-1.39.0}/src/sourcecode/workspace.py +0 -0
  115. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/.temp/cli-latest +0 -0
  116. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/functions/README.md +0 -0
  117. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/functions/get-license/index.ts +0 -0
  118. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/functions/lemonsqueezy-webhook/index.ts +0 -0
  119. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/functions/telemetry/index.ts +0 -0
  120. {sourcecode-1.36.5 → sourcecode-1.39.0}/supabase/sql/license_event_ordering.sql +0 -0
  121. {sourcecode-1.36.5 → 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.36.5
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
- ![Version](https://img.shields.io/badge/version-1.36.5-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.39.0-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.36.5
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
- ![Version](https://img.shields.io/badge/version-1.36.5-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.39.0-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.36.5
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
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.36.5"
7
+ version = "1.39.0"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.36.5"
3
+ __version__ = "1.39.0"
@@ -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
@@ -401,6 +403,28 @@ def _emit_error_json(error: str, message: str, **context: object) -> None:
401
403
  sys.stderr.flush()
402
404
 
403
405
 
406
+ def _enforce_format(command: str, fmt: str) -> None:
407
+ """Validate ``--format`` for ``command`` against the central contract.
408
+
409
+ Single validation path for every command's ``--format`` option (see
410
+ ``sourcecode.format_contract``). On an invalid value it emits the
411
+ homogeneous JSON error envelope to stderr and exits with code 2
412
+ (argument-validation convention). Valid values are a no-op.
413
+ """
414
+ from sourcecode.format_contract import (
415
+ FORMAT_ERROR_EXIT_CODE,
416
+ format_error_context,
417
+ is_valid_format,
418
+ )
419
+
420
+ if is_valid_format(command, fmt):
421
+ return
422
+ ctx = format_error_context(command, fmt)
423
+ message = str(ctx.pop("message"))
424
+ _emit_error_json(INVALID_INPUT_CODE, message, **ctx)
425
+ raise typer.Exit(code=FORMAT_ERROR_EXIT_CODE)
426
+
427
+
404
428
  def _safe_write_file(path: "Path", content: str) -> None:
405
429
  """Write content to path, emitting a clean JSON error on I/O failure."""
406
430
  try:
@@ -631,7 +655,8 @@ def _active_flags(
631
655
  if fmt != "json": flags.append("--format")
632
656
  return flags
633
657
 
634
- FORMAT_CHOICES = ["json", "yaml"]
658
+ # Per-command output-format contracts now live in sourcecode.format_contract
659
+ # (validated via _enforce_format). No module-level FORMAT_CHOICES here.
635
660
  GRAPH_DETAIL_CHOICES = ["high", "medium", "full"]
636
661
  GRAPH_EDGE_CHOICES = {"imports", "calls", "contains", "extends"}
637
662
  DOCS_DEPTH_CHOICES = ["module", "symbols", "full"]
@@ -1138,17 +1163,7 @@ def main(
1138
1163
  )
1139
1164
 
1140
1165
  # Validate format choices
1141
- if format not in FORMAT_CHOICES:
1142
- _emit_error_json(
1143
- INVALID_INPUT_CODE,
1144
- f"Invalid value '{format}' for --format. Valid values: {', '.join(FORMAT_CHOICES)}.",
1145
- flag="--format",
1146
- value=format,
1147
- valid_values=list(FORMAT_CHOICES),
1148
- hint="Choose one of the supported --format values.",
1149
- expected=f"One of: {', '.join(FORMAT_CHOICES)}",
1150
- )
1151
- raise typer.Exit(code=2) # FIX-P2-7: arg validation → exit 2
1166
+ _enforce_format("main", format)
1152
1167
  if graph_detail not in GRAPH_DETAIL_CHOICES:
1153
1168
  _emit_error_json(
1154
1169
  INVALID_INPUT_CODE,
@@ -2834,19 +2849,9 @@ def prepare_context_cmd(
2834
2849
  # Validate --format: only "json" and "github-comment" are valid for prepare-context.
2835
2850
  # "yaml" is intentionally NOT supported here (use main command for yaml output).
2836
2851
  # Invalid values must error loudly — silently falling through to JSON is a lie.
2837
- _PC_FORMAT_CHOICES = ("json", "github-comment")
2838
- if format is not None and format not in _PC_FORMAT_CHOICES:
2839
- _emit_error_json(
2840
- INVALID_INPUT_CODE,
2841
- f"invalid value '{format}' for --format. "
2842
- f"Valid options: {', '.join(_PC_FORMAT_CHOICES)}.",
2843
- flag="--format",
2844
- value=format,
2845
- valid_values=list(_PC_FORMAT_CHOICES),
2846
- hint="Choose one of the supported prepare-context output formats.",
2847
- expected=f"One of: {', '.join(_PC_FORMAT_CHOICES)}",
2848
- )
2849
- raise typer.Exit(code=2)
2852
+ # None means "use default" (json); a concrete value is validated against the contract.
2853
+ if format is not None:
2854
+ _enforce_format("prepare-context", format)
2850
2855
  # github-comment only renders for review-pr; warn and normalize for other tasks.
2851
2856
  if format == "github-comment" and task != "review-pr":
2852
2857
  typer.echo(
@@ -3479,14 +3484,7 @@ def repo_ir_cmd(
3479
3484
 
3480
3485
  from sourcecode.repository_ir import apply_ir_size_limits, build_repo_ir, find_java_files
3481
3486
 
3482
- if format not in ("json", "yaml"):
3483
- _emit_error_json(
3484
- INVALID_INPUT_CODE,
3485
- f"Invalid format '{format}'.",
3486
- hint="Valid values: json, yaml.",
3487
- expected="json | yaml",
3488
- )
3489
- raise typer.Exit(code=1)
3487
+ _enforce_format("repo-ir", format)
3490
3488
 
3491
3489
  root = path.resolve()
3492
3490
  if not root.is_dir():
@@ -3712,14 +3710,7 @@ def impact_cmd(
3712
3710
  from sourcecode.license import require_repo_or_pro as _require_repo_or_pro
3713
3711
  _require_repo_or_pro(str(path.resolve()), "impact")
3714
3712
 
3715
- if format not in ("json", "yaml"):
3716
- _emit_error_json(
3717
- INVALID_INPUT_CODE,
3718
- f"Invalid format '{format}'.",
3719
- hint="format must be: json or yaml.",
3720
- expected="json | yaml",
3721
- )
3722
- raise typer.Exit(code=1)
3713
+ _enforce_format("impact", format)
3723
3714
 
3724
3715
  from sourcecode.repository_ir import (
3725
3716
  build_repo_ir, find_java_files, compute_blast_radius,
@@ -3875,14 +3866,7 @@ def endpoints_cmd(
3875
3866
  sourcecode endpoints . --controller LiquidacionJornada
3876
3867
  sourcecode endpoints . --limit 10
3877
3868
  """
3878
- if format not in ("json", "yaml"):
3879
- _emit_error_json(
3880
- INVALID_INPUT_CODE,
3881
- f"Invalid format '{format}'.",
3882
- hint="format must be: json or yaml.",
3883
- expected="json | yaml",
3884
- )
3885
- raise typer.Exit(code=1)
3869
+ _enforce_format("endpoints", format)
3886
3870
 
3887
3871
  target = path.resolve()
3888
3872
  if not target.exists() or not target.is_dir():
@@ -3933,6 +3917,105 @@ def endpoints_cmd(
3933
3917
  _nudge()
3934
3918
 
3935
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
+
3936
4019
  # ── Spring Semantic Audit ─────────────────────────────────────────────────────
3937
4020
 
3938
4021
 
@@ -4116,14 +4199,7 @@ def spring_audit_cmd(
4116
4199
  )
4117
4200
  raise typer.Exit(code=1)
4118
4201
 
4119
- if format not in ("json", "yaml", "github-comment"):
4120
- _emit_error_json(
4121
- INVALID_INPUT_CODE,
4122
- f"Invalid format '{format}'.",
4123
- hint="format must be one of: json, yaml, github-comment.",
4124
- expected="json | yaml | github-comment",
4125
- )
4126
- raise typer.Exit(code=1)
4202
+ _enforce_format("spring-audit", format)
4127
4203
 
4128
4204
  _file_limitations: list[str] = []
4129
4205
  file_list = find_java_files(target, limitations=_file_limitations)
@@ -4274,14 +4350,7 @@ def migrate_check_cmd(
4274
4350
  )
4275
4351
  raise typer.Exit(code=1)
4276
4352
 
4277
- if format not in ("json", "text"):
4278
- _emit_error_json(
4279
- INVALID_INPUT_CODE,
4280
- f"Invalid format '{format}'.",
4281
- hint="format must be one of: json, text.",
4282
- expected="json | text",
4283
- )
4284
- raise typer.Exit(code=1)
4353
+ _enforce_format("migrate-check", format)
4285
4354
 
4286
4355
  if min_severity not in ("critical", "high", "medium", "low"):
4287
4356
  _emit_error_json(
@@ -4426,14 +4495,7 @@ def impact_chain_cmd(
4426
4495
  )
4427
4496
  raise typer.Exit(code=1)
4428
4497
 
4429
- if format not in ("json", "yaml"):
4430
- _emit_error_json(
4431
- INVALID_INPUT_CODE,
4432
- f"Invalid format '{format}'.",
4433
- hint="format must be: json or yaml.",
4434
- expected="json | yaml",
4435
- )
4436
- raise typer.Exit(code=1)
4498
+ _enforce_format("impact-chain", format)
4437
4499
 
4438
4500
  file_list = find_java_files(target)
4439
4501
  if not file_list:
@@ -4567,14 +4629,7 @@ def pr_impact_cmd(
4567
4629
  )
4568
4630
  raise typer.Exit(code=1)
4569
4631
 
4570
- if format not in ("text", "json"):
4571
- _emit_error_json(
4572
- INVALID_INPUT_CODE,
4573
- f"Invalid format '{format}'.",
4574
- hint="format must be: text or json.",
4575
- expected="text | json",
4576
- )
4577
- raise typer.Exit(code=1)
4632
+ _enforce_format("pr-impact", format)
4578
4633
 
4579
4634
  # Read changed-files list
4580
4635
  changed_files = [
@@ -4699,14 +4754,7 @@ def explain_cmd(
4699
4754
  )
4700
4755
  raise typer.Exit(code=1)
4701
4756
 
4702
- if format not in ("text", "json"):
4703
- _emit_error_json(
4704
- INVALID_INPUT_CODE,
4705
- f"Invalid format '{format}'.",
4706
- hint="format must be: text or json.",
4707
- expected="text | json",
4708
- )
4709
- raise typer.Exit(code=1)
4757
+ _enforce_format("explain", format)
4710
4758
 
4711
4759
  file_list = find_java_files(target)
4712
4760
  if not file_list:
@@ -0,0 +1,87 @@
1
+ """Single source of truth for per-command output-format contracts.
2
+
3
+ Every CLI command that emits machine-consumable output validates its
4
+ ``--format`` option through this registry so that:
5
+
6
+ * the set of allowed formats for each command lives in exactly one place,
7
+ * ``-f json`` is a strict contract on every command (pure JSON to stdout),
8
+ * invalid-format errors share an identical envelope shape and exit code.
9
+
10
+ The registry value is an *ordered* tuple; element ``0`` is the command's
11
+ default and matches its Typer option default. Defaults are intentionally NOT
12
+ changed when centralizing — ``explain`` and ``pr-impact`` keep their
13
+ human-facing ``text`` default — to avoid breaking existing scripts. The strict
14
+ guarantee is on ``-f json``, which every command supports.
15
+
16
+ Exit-code policy: an invalid ``--format`` is an argument-validation error and
17
+ exits with code ``2`` for every command (matching the documented
18
+ ``arg validation -> exit 2`` convention used by the root command).
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ # Command name (as registered with ``@app.command``, or "main" for the root
24
+ # command) -> ordered tuple of allowed formats. Element 0 is the default.
25
+ FORMAT_REGISTRY: "dict[str, tuple[str, ...]]" = {
26
+ "main": ("json", "yaml"),
27
+ "repo-ir": ("json", "yaml"),
28
+ "impact": ("json", "yaml"),
29
+ "endpoints": ("json", "yaml"),
30
+ "validation": ("json", "yaml"),
31
+ "impact-chain": ("json", "yaml"),
32
+ "pr-impact": ("text", "json"),
33
+ "migrate-check": ("json", "text"),
34
+ "spring-audit": ("json", "yaml", "github-comment"),
35
+ "explain": ("text", "json"),
36
+ "prepare-context": ("json", "github-comment"),
37
+ }
38
+
39
+ # Invalid --format is an argument-validation error.
40
+ FORMAT_ERROR_EXIT_CODE = 2
41
+
42
+ # The strict machine-readable format every command must support.
43
+ STRICT_FORMAT = "json"
44
+
45
+
46
+ def allowed_formats(command: str) -> "tuple[str, ...]":
47
+ """Return the ordered tuple of allowed formats for ``command``.
48
+
49
+ Raises ``KeyError`` if the command has no registered contract — a
50
+ programming error, surfaced loudly rather than silently allowing anything.
51
+ """
52
+ try:
53
+ return FORMAT_REGISTRY[command]
54
+ except KeyError as exc:
55
+ raise KeyError(
56
+ f"No format contract registered for command '{command}'. "
57
+ f"Add it to FORMAT_REGISTRY in sourcecode/format_contract.py."
58
+ ) from exc
59
+
60
+
61
+ def default_format(command: str) -> str:
62
+ """Return the default format for ``command`` (registry element 0)."""
63
+ return allowed_formats(command)[0]
64
+
65
+
66
+ def is_valid_format(command: str, fmt: str) -> bool:
67
+ """True iff ``fmt`` is allowed for ``command``."""
68
+ return fmt in FORMAT_REGISTRY.get(command, ())
69
+
70
+
71
+ def format_error_context(command: str, fmt: str) -> "dict[str, object]":
72
+ """Build the homogeneous error-envelope fields for an invalid ``--format``.
73
+
74
+ Returns a dict whose ``message`` key is the human message and whose
75
+ remaining keys are passed verbatim as the error-envelope context, so every
76
+ command produces an identically shaped ``--format`` error.
77
+ """
78
+ allowed = list(allowed_formats(command))
79
+ joined = ", ".join(allowed)
80
+ return {
81
+ "message": f"Invalid value '{fmt}' for --format. Valid values: {joined}.",
82
+ "flag": "--format",
83
+ "value": fmt,
84
+ "valid_values": allowed,
85
+ "hint": "Choose one of the supported --format values.",
86
+ "expected": f"One of: {joined}",
87
+ }
@@ -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