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.
- {sourcecode-1.35.12 → sourcecode-1.35.14}/PKG-INFO +3 -3
- {sourcecode-1.35.12 → sourcecode-1.35.14}/README.md +2 -2
- {sourcecode-1.35.12 → sourcecode-1.35.14}/pyproject.toml +1 -1
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cli.py +269 -0
- sourcecode-1.35.14/src/sourcecode/explain.py +483 -0
- sourcecode-1.35.14/src/sourcecode/pr_impact.py +475 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/.gitignore +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/.ruff.toml +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/CHANGELOG.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/LICENSE +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/SECURITY.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/raw +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.14}/src/sourcecode/tree_utils.py +0 -0
- {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.
|
|
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
|
-

|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -113,7 +113,7 @@ pipx install sourcecode
|
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
115
|
sourcecode version
|
|
116
|
-
# sourcecode 1.35.
|
|
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
|
-

|
|
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.35.
|
|
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.
|
|
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"
|
|
@@ -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
|