sourcecode 1.35.12__tar.gz → 1.35.14__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 (105) hide show
  1. {sourcecode-1.35.12 → sourcecode-1.35.14}/PKG-INFO +3 -3
  2. {sourcecode-1.35.12 → sourcecode-1.35.14}/README.md +2 -2
  3. {sourcecode-1.35.12 → sourcecode-1.35.14}/pyproject.toml +1 -1
  4. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cli.py +269 -0
  6. sourcecode-1.35.14/src/sourcecode/explain.py +483 -0
  7. sourcecode-1.35.14/src/sourcecode/pr_impact.py +475 -0
  8. {sourcecode-1.35.12 → sourcecode-1.35.14}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-1.35.12 → sourcecode-1.35.14}/.gitignore +0 -0
  10. {sourcecode-1.35.12 → sourcecode-1.35.14}/.ruff.toml +0 -0
  11. {sourcecode-1.35.12 → sourcecode-1.35.14}/CHANGELOG.md +0 -0
  12. {sourcecode-1.35.12 → sourcecode-1.35.14}/CONTRIBUTING.md +0 -0
  13. {sourcecode-1.35.12 → sourcecode-1.35.14}/LICENSE +0 -0
  14. {sourcecode-1.35.12 → sourcecode-1.35.14}/SECURITY.md +0 -0
  15. {sourcecode-1.35.12 → sourcecode-1.35.14}/raw +0 -0
  16. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/adaptive_scanner.py +0 -0
  17. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/architecture_analyzer.py +0 -0
  18. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/architecture_summary.py +0 -0
  19. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ast_extractor.py +0 -0
  20. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cache.py +0 -0
  21. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/canonical_ir.py +0 -0
  22. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cir_graphs.py +0 -0
  23. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/classifier.py +0 -0
  24. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/code_notes_analyzer.py +0 -0
  25. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/confidence_analyzer.py +0 -0
  26. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/context_scorer.py +0 -0
  27. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/context_summarizer.py +0 -0
  28. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/contract_model.py +0 -0
  29. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/contract_pipeline.py +0 -0
  30. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/coverage_parser.py +0 -0
  31. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/dependency_analyzer.py +0 -0
  32. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/__init__.py +0 -0
  33. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/base.py +0 -0
  34. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/csproj_parser.py +0 -0
  35. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/dart.py +0 -0
  36. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/dotnet.py +0 -0
  37. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/elixir.py +0 -0
  38. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/go.py +0 -0
  39. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/heuristic.py +0 -0
  40. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/hybrid.py +0 -0
  41. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/java.py +0 -0
  42. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/jvm_ext.py +0 -0
  43. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/nodejs.py +0 -0
  44. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/parsers.py +0 -0
  45. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/php.py +0 -0
  46. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/project.py +0 -0
  47. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/python.py +0 -0
  48. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/ruby.py +0 -0
  49. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/rust.py +0 -0
  50. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/systems.py +0 -0
  51. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/terraform.py +0 -0
  52. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/tooling.py +0 -0
  53. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/doc_analyzer.py +0 -0
  54. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/entrypoint_classifier.py +0 -0
  55. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/env_analyzer.py +0 -0
  56. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/error_schema.py +0 -0
  57. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/graph_analyzer.py +0 -0
  61. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/license.py +0 -0
  62. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/__init__.py +0 -0
  63. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  64. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  65. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  66. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  67. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  68. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/orchestrator.py +0 -0
  69. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/registry.py +0 -0
  70. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/runner.py +0 -0
  71. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/server.py +0 -0
  72. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp_nudge.py +0 -0
  73. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/metrics_analyzer.py +0 -0
  74. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/output_budget.py +0 -0
  75. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/path_filters.py +0 -0
  76. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/pr_comment_renderer.py +0 -0
  77. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/prepare_context.py +0 -0
  78. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/progress.py +0 -0
  79. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ranking_engine.py +0 -0
  80. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/redactor.py +0 -0
  81. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/relevance_scorer.py +0 -0
  82. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/repo_classifier.py +0 -0
  83. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/repository_ir.py +0 -0
  84. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ris.py +0 -0
  85. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/runtime_classifier.py +0 -0
  86. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/scanner.py +0 -0
  87. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/schema.py +0 -0
  88. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/semantic_analyzer.py +0 -0
  89. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/serializer.py +0 -0
  90. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_event_topology.py +0 -0
  91. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_findings.py +0 -0
  92. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_impact.py +0 -0
  93. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_model.py +0 -0
  94. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_security_audit.py +0 -0
  95. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_semantic.py +0 -0
  96. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_tx_analyzer.py +0 -0
  97. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/summarizer.py +0 -0
  98. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/__init__.py +0 -0
  99. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/config.py +0 -0
  100. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/consent.py +0 -0
  101. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/events.py +0 -0
  102. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/filters.py +0 -0
  103. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/transport.py +0 -0
  104. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/tree_utils.py +0 -0
  105. {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.12
3
+ Version: 1.35.14
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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
41
 
42
- ![Version](https://img.shields.io/badge/version-1.35.12-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.35.14-blue)
43
43
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
44
 
45
45
  ---
@@ -113,7 +113,7 @@ pipx install sourcecode
113
113
 
114
114
  ```bash
115
115
  sourcecode version
116
- # sourcecode 1.35.12
116
+ # sourcecode 1.35.14
117
117
  ```
118
118
 
119
119
  ---
@@ -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.35.12-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.14-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%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.35.12
79
+ # sourcecode 1.35.14
80
80
  ```
81
81
 
82
82
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.35.12"
7
+ version = "1.35.14"
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.35.12"
3
+ __version__ = "1.35.14"
@@ -227,6 +227,10 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
227
227
  "spring-audit",
228
228
  # Spring impact chain
229
229
  "impact-chain",
230
+ # PR blast-radius report
231
+ "pr-impact",
232
+ # Class architectural summary
233
+ "explain",
230
234
  }
231
235
  )
232
236
 
@@ -266,6 +270,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
266
270
  "--symbol",
267
271
  "--max-importers",
268
272
  "--exclude",
273
+ "--files",
269
274
  })
270
275
 
271
276
 
@@ -4027,6 +4032,270 @@ def impact_chain_cmd(
4027
4032
  typer.echo("✓ copied to clipboard", err=True)
4028
4033
 
4029
4034
 
4035
+ # ── PR Impact Report ──────────────────────────────────────────────────────────
4036
+
4037
+ @app.command("pr-impact")
4038
+ def pr_impact_cmd(
4039
+ path: Path = typer.Argument(
4040
+ Path("."),
4041
+ help="Repository root (default: current directory)",
4042
+ ),
4043
+ files: Path = typer.Option(
4044
+ ...,
4045
+ "--files",
4046
+ help="File containing the list of changed Java files, one path per line.",
4047
+ ),
4048
+ output_path: Optional[Path] = typer.Option(
4049
+ None, "--output", "-o",
4050
+ help="Write output to a file instead of stdout.",
4051
+ ),
4052
+ format: str = typer.Option(
4053
+ "text", "--format", "-f",
4054
+ help="Output format: text (default) or json.",
4055
+ show_default=True,
4056
+ ),
4057
+ copy: bool = typer.Option(
4058
+ False, "--copy", "-c",
4059
+ help="Copy output to clipboard after a successful run.",
4060
+ ),
4061
+ ) -> None:
4062
+ """PR blast-radius report: what can break if this PR is merged?
4063
+
4064
+ \b
4065
+ Reads a list of changed Java files and produces a consolidated report:
4066
+ - Modified classes found in the changed files
4067
+ - Affected REST endpoints reachable through the call chain
4068
+ - Direct callers of each modified class
4069
+ - Event publishers and consumers triggered by the change
4070
+ - @Transactional methods in the changed classes
4071
+ - Consolidated risk level (CRITICAL / HIGH / MEDIUM / LOW)
4072
+
4073
+ \b
4074
+ Reuses existing graph and impact analysis — no new parsers.
4075
+ JAVA/SPRING ONLY.
4076
+
4077
+ \b
4078
+ Examples:
4079
+ sourcecode pr-impact --files changed_files.txt
4080
+ sourcecode pr-impact /path/to/repo --files diff.txt --format json
4081
+ sourcecode pr-impact --files changes.txt --output pr_report.txt
4082
+ """
4083
+ import json as _json
4084
+
4085
+ from sourcecode.repository_ir import find_java_files
4086
+ from sourcecode.canonical_ir import build_canonical_ir
4087
+ from sourcecode.spring_model import SpringSemanticModel
4088
+ from sourcecode.pr_impact import run_pr_impact
4089
+
4090
+ target = path.resolve()
4091
+ if not target.exists() or not target.is_dir():
4092
+ _emit_error_json(
4093
+ INVALID_INPUT_CODE,
4094
+ f"'{target}' is not a valid directory.",
4095
+ path=str(target),
4096
+ hint="Pass an existing repository directory.",
4097
+ expected="A directory path.",
4098
+ )
4099
+ raise typer.Exit(code=1)
4100
+
4101
+ if not files.exists():
4102
+ _emit_error_json(
4103
+ INVALID_INPUT_CODE,
4104
+ f"--files path '{files}' does not exist.",
4105
+ path=str(files),
4106
+ hint="Pass a file containing one Java file path per line.",
4107
+ expected="An existing file path.",
4108
+ )
4109
+ raise typer.Exit(code=1)
4110
+
4111
+ if format not in ("text", "json"):
4112
+ _emit_error_json(
4113
+ INVALID_INPUT_CODE,
4114
+ f"Invalid format '{format}'.",
4115
+ hint="format must be: text or json.",
4116
+ expected="text | json",
4117
+ )
4118
+ raise typer.Exit(code=1)
4119
+
4120
+ # Read changed-files list
4121
+ changed_files = [
4122
+ line.strip()
4123
+ for line in files.read_text(encoding="utf-8").splitlines()
4124
+ if line.strip()
4125
+ ]
4126
+ if not changed_files:
4127
+ _emit_error_json(
4128
+ INVALID_INPUT_CODE,
4129
+ f"--files '{files}' is empty.",
4130
+ hint="File must contain at least one Java file path.",
4131
+ expected="One Java file path per line.",
4132
+ )
4133
+ raise typer.Exit(code=1)
4134
+
4135
+ file_list = find_java_files(target)
4136
+ if not file_list:
4137
+ data: dict = {
4138
+ "schema_version": "1.0",
4139
+ "modified_classes": [],
4140
+ "risk_level": "UNKNOWN",
4141
+ "risk_reason": "No Java files found in repository — Spring analysis requires Java source.",
4142
+ "analysis_warnings": ["No Java files found."],
4143
+ "metadata": {"changed_files_count": len(changed_files)},
4144
+ }
4145
+ output = _json.dumps(data, indent=2, ensure_ascii=False) if format == "json" else (
4146
+ "No Java files found in repository — Spring analysis requires Java source."
4147
+ )
4148
+ if output_path is not None:
4149
+ output_path.write_text(output, encoding="utf-8")
4150
+ typer.echo("PR impact report written to " + str(output_path), err=True)
4151
+ else:
4152
+ sys.stdout.buffer.write(output.encode("utf-8"))
4153
+ sys.stdout.buffer.write(b"\n")
4154
+ sys.stdout.buffer.flush()
4155
+ return
4156
+
4157
+ cir = build_canonical_ir(file_list, target)
4158
+ model = SpringSemanticModel.build(cir)
4159
+ report = run_pr_impact(cir, changed_files, root=target, model=model)
4160
+
4161
+ if format == "json":
4162
+ output = _json.dumps(report.to_dict(), indent=2, ensure_ascii=False)
4163
+ else:
4164
+ output = report.render_text()
4165
+
4166
+ if output_path is not None:
4167
+ output_path.write_text(output, encoding="utf-8")
4168
+ typer.echo(
4169
+ f"PR impact report written to {output_path} "
4170
+ f"(risk: {report.risk_level}, "
4171
+ f"{len(report.modified_classes)} classes, "
4172
+ f"{len(report.affected_endpoints)} endpoints)",
4173
+ err=True,
4174
+ )
4175
+ else:
4176
+ sys.stdout.buffer.write(output.encode("utf-8"))
4177
+ sys.stdout.buffer.write(b"\n")
4178
+ sys.stdout.buffer.flush()
4179
+ if copy:
4180
+ if _copy_to_clipboard(output):
4181
+ typer.echo("✓ copied to clipboard", err=True)
4182
+
4183
+
4184
+ # ── Explain Command ───────────────────────────────────────────────────────────
4185
+
4186
+ @app.command("explain")
4187
+ def explain_cmd(
4188
+ class_name: str = typer.Argument(
4189
+ ...,
4190
+ help="Simple class name to explain (e.g. UserService, OrderController).",
4191
+ ),
4192
+ path: Path = typer.Argument(
4193
+ Path("."),
4194
+ help="Repository root (default: current directory)",
4195
+ ),
4196
+ format: str = typer.Option(
4197
+ "text", "--format", "-f",
4198
+ help="Output format: text (default) or json.",
4199
+ show_default=True,
4200
+ ),
4201
+ output_path: Optional[Path] = typer.Option(
4202
+ None, "--output", "-o",
4203
+ help="Write output to a file instead of stdout.",
4204
+ ),
4205
+ copy: bool = typer.Option(
4206
+ False, "--copy", "-c",
4207
+ help="Copy output to clipboard after a successful run.",
4208
+ ),
4209
+ ) -> None:
4210
+ """Human-readable architectural summary for a class.
4211
+
4212
+ \b
4213
+ Generates a structured explanation derived entirely from static analysis:
4214
+ - Purpose and Spring stereotype
4215
+ - Public methods
4216
+ - Incoming callers (who uses this class)
4217
+ - Outgoing dependencies (what this class calls)
4218
+ - Events published and consumed
4219
+ - @Transactional boundaries
4220
+ - Security constraints (@PreAuthorize, @Secured, etc.)
4221
+ - REST endpoints related
4222
+
4223
+ \b
4224
+ JAVA/SPRING ONLY. Reads from existing CIR — no new parsers.
4225
+
4226
+ \b
4227
+ Examples:
4228
+ sourcecode explain UserService
4229
+ sourcecode explain OrderController /path/to/repo
4230
+ sourcecode explain UserService --format json
4231
+ """
4232
+ import json as _json
4233
+
4234
+ from sourcecode.repository_ir import find_java_files
4235
+ from sourcecode.canonical_ir import build_canonical_ir
4236
+ from sourcecode.spring_model import SpringSemanticModel
4237
+ from sourcecode.explain import explain_class
4238
+
4239
+ target = path.resolve()
4240
+ if not target.exists() or not target.is_dir():
4241
+ _emit_error_json(
4242
+ INVALID_INPUT_CODE,
4243
+ f"'{target}' is not a valid directory.",
4244
+ path=str(target),
4245
+ hint="Pass an existing repository directory.",
4246
+ expected="A directory path.",
4247
+ )
4248
+ raise typer.Exit(code=1)
4249
+
4250
+ if format not in ("text", "json"):
4251
+ _emit_error_json(
4252
+ INVALID_INPUT_CODE,
4253
+ f"Invalid format '{format}'.",
4254
+ hint="format must be: text or json.",
4255
+ expected="text | json",
4256
+ )
4257
+ raise typer.Exit(code=1)
4258
+
4259
+ file_list = find_java_files(target)
4260
+ if not file_list:
4261
+ msg = f"No Java files found in '{target}'. sourcecode explain requires a Java/Spring repository."
4262
+ if format == "json":
4263
+ output = _json.dumps({
4264
+ "error": "no_java_files",
4265
+ "message": msg,
4266
+ "class_name": class_name,
4267
+ }, indent=2)
4268
+ else:
4269
+ output = msg
4270
+ if output_path is not None:
4271
+ output_path.write_text(output, encoding="utf-8")
4272
+ else:
4273
+ sys.stdout.buffer.write(output.encode("utf-8"))
4274
+ sys.stdout.buffer.write(b"\n")
4275
+ sys.stdout.buffer.flush()
4276
+ raise typer.Exit(code=1)
4277
+
4278
+ cir = build_canonical_ir(file_list, target)
4279
+ model = SpringSemanticModel.build(cir)
4280
+ explanation = explain_class(class_name, cir, model)
4281
+
4282
+ if format == "json":
4283
+ output = _json.dumps(explanation.to_dict(), indent=2, ensure_ascii=False)
4284
+ else:
4285
+ output = explanation.render_text()
4286
+
4287
+ if output_path is not None:
4288
+ output_path.write_text(output, encoding="utf-8")
4289
+ typer.echo(f"Explanation written to {output_path}", err=True)
4290
+ else:
4291
+ sys.stdout.buffer.write(output.encode("utf-8"))
4292
+ sys.stdout.buffer.write(b"\n")
4293
+ sys.stdout.buffer.flush()
4294
+
4295
+ if copy and _copy_to_clipboard(output):
4296
+ typer.echo("✓ copied to clipboard", err=True)
4297
+
4298
+
4030
4299
  # ── Enterprise Workflow Commands ──────────────────────────────────────────────
4031
4300
  #
4032
4301
  # These are the five canonical enterprise workflows. Each is a thin wrapper