sourcecode 1.35.12__tar.gz → 1.35.13__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.13}/PKG-INFO +3 -3
- {sourcecode-1.35.12 → sourcecode-1.35.13}/README.md +2 -2
- {sourcecode-1.35.12 → sourcecode-1.35.13}/pyproject.toml +1 -1
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/cli.py +152 -0
- sourcecode-1.35.13/src/sourcecode/pr_impact.py +475 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/.gitignore +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/.ruff.toml +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/CHANGELOG.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/LICENSE +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/SECURITY.md +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/raw +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.12 → sourcecode-1.35.13}/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.13
|
|
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.13
|
|
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.13
|
|
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.13"
|
|
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,8 @@ _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",
|
|
230
232
|
}
|
|
231
233
|
)
|
|
232
234
|
|
|
@@ -266,6 +268,7 @@ _OPTIONS_WITH_VALUE: frozenset[str] = frozenset({
|
|
|
266
268
|
"--symbol",
|
|
267
269
|
"--max-importers",
|
|
268
270
|
"--exclude",
|
|
271
|
+
"--files",
|
|
269
272
|
})
|
|
270
273
|
|
|
271
274
|
|
|
@@ -4027,6 +4030,155 @@ def impact_chain_cmd(
|
|
|
4027
4030
|
typer.echo("✓ copied to clipboard", err=True)
|
|
4028
4031
|
|
|
4029
4032
|
|
|
4033
|
+
# ── PR Impact Report ──────────────────────────────────────────────────────────
|
|
4034
|
+
|
|
4035
|
+
@app.command("pr-impact")
|
|
4036
|
+
def pr_impact_cmd(
|
|
4037
|
+
path: Path = typer.Argument(
|
|
4038
|
+
Path("."),
|
|
4039
|
+
help="Repository root (default: current directory)",
|
|
4040
|
+
),
|
|
4041
|
+
files: Path = typer.Option(
|
|
4042
|
+
...,
|
|
4043
|
+
"--files",
|
|
4044
|
+
help="File containing the list of changed Java files, one path per line.",
|
|
4045
|
+
),
|
|
4046
|
+
output_path: Optional[Path] = typer.Option(
|
|
4047
|
+
None, "--output", "-o",
|
|
4048
|
+
help="Write output to a file instead of stdout.",
|
|
4049
|
+
),
|
|
4050
|
+
format: str = typer.Option(
|
|
4051
|
+
"text", "--format", "-f",
|
|
4052
|
+
help="Output format: text (default) or json.",
|
|
4053
|
+
show_default=True,
|
|
4054
|
+
),
|
|
4055
|
+
copy: bool = typer.Option(
|
|
4056
|
+
False, "--copy", "-c",
|
|
4057
|
+
help="Copy output to clipboard after a successful run.",
|
|
4058
|
+
),
|
|
4059
|
+
) -> None:
|
|
4060
|
+
"""PR blast-radius report: what can break if this PR is merged?
|
|
4061
|
+
|
|
4062
|
+
\b
|
|
4063
|
+
Reads a list of changed Java files and produces a consolidated report:
|
|
4064
|
+
- Modified classes found in the changed files
|
|
4065
|
+
- Affected REST endpoints reachable through the call chain
|
|
4066
|
+
- Direct callers of each modified class
|
|
4067
|
+
- Event publishers and consumers triggered by the change
|
|
4068
|
+
- @Transactional methods in the changed classes
|
|
4069
|
+
- Consolidated risk level (CRITICAL / HIGH / MEDIUM / LOW)
|
|
4070
|
+
|
|
4071
|
+
\b
|
|
4072
|
+
Reuses existing graph and impact analysis — no new parsers.
|
|
4073
|
+
JAVA/SPRING ONLY.
|
|
4074
|
+
|
|
4075
|
+
\b
|
|
4076
|
+
Examples:
|
|
4077
|
+
sourcecode pr-impact --files changed_files.txt
|
|
4078
|
+
sourcecode pr-impact /path/to/repo --files diff.txt --format json
|
|
4079
|
+
sourcecode pr-impact --files changes.txt --output pr_report.txt
|
|
4080
|
+
"""
|
|
4081
|
+
import json as _json
|
|
4082
|
+
|
|
4083
|
+
from sourcecode.repository_ir import find_java_files
|
|
4084
|
+
from sourcecode.canonical_ir import build_canonical_ir
|
|
4085
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
4086
|
+
from sourcecode.pr_impact import run_pr_impact
|
|
4087
|
+
|
|
4088
|
+
target = path.resolve()
|
|
4089
|
+
if not target.exists() or not target.is_dir():
|
|
4090
|
+
_emit_error_json(
|
|
4091
|
+
INVALID_INPUT_CODE,
|
|
4092
|
+
f"'{target}' is not a valid directory.",
|
|
4093
|
+
path=str(target),
|
|
4094
|
+
hint="Pass an existing repository directory.",
|
|
4095
|
+
expected="A directory path.",
|
|
4096
|
+
)
|
|
4097
|
+
raise typer.Exit(code=1)
|
|
4098
|
+
|
|
4099
|
+
if not files.exists():
|
|
4100
|
+
_emit_error_json(
|
|
4101
|
+
INVALID_INPUT_CODE,
|
|
4102
|
+
f"--files path '{files}' does not exist.",
|
|
4103
|
+
path=str(files),
|
|
4104
|
+
hint="Pass a file containing one Java file path per line.",
|
|
4105
|
+
expected="An existing file path.",
|
|
4106
|
+
)
|
|
4107
|
+
raise typer.Exit(code=1)
|
|
4108
|
+
|
|
4109
|
+
if format not in ("text", "json"):
|
|
4110
|
+
_emit_error_json(
|
|
4111
|
+
INVALID_INPUT_CODE,
|
|
4112
|
+
f"Invalid format '{format}'.",
|
|
4113
|
+
hint="format must be: text or json.",
|
|
4114
|
+
expected="text | json",
|
|
4115
|
+
)
|
|
4116
|
+
raise typer.Exit(code=1)
|
|
4117
|
+
|
|
4118
|
+
# Read changed-files list
|
|
4119
|
+
changed_files = [
|
|
4120
|
+
line.strip()
|
|
4121
|
+
for line in files.read_text(encoding="utf-8").splitlines()
|
|
4122
|
+
if line.strip()
|
|
4123
|
+
]
|
|
4124
|
+
if not changed_files:
|
|
4125
|
+
_emit_error_json(
|
|
4126
|
+
INVALID_INPUT_CODE,
|
|
4127
|
+
f"--files '{files}' is empty.",
|
|
4128
|
+
hint="File must contain at least one Java file path.",
|
|
4129
|
+
expected="One Java file path per line.",
|
|
4130
|
+
)
|
|
4131
|
+
raise typer.Exit(code=1)
|
|
4132
|
+
|
|
4133
|
+
file_list = find_java_files(target)
|
|
4134
|
+
if not file_list:
|
|
4135
|
+
data: dict = {
|
|
4136
|
+
"schema_version": "1.0",
|
|
4137
|
+
"modified_classes": [],
|
|
4138
|
+
"risk_level": "UNKNOWN",
|
|
4139
|
+
"risk_reason": "No Java files found in repository — Spring analysis requires Java source.",
|
|
4140
|
+
"analysis_warnings": ["No Java files found."],
|
|
4141
|
+
"metadata": {"changed_files_count": len(changed_files)},
|
|
4142
|
+
}
|
|
4143
|
+
output = _json.dumps(data, indent=2, ensure_ascii=False) if format == "json" else (
|
|
4144
|
+
"No Java files found in repository — Spring analysis requires Java source."
|
|
4145
|
+
)
|
|
4146
|
+
if output_path is not None:
|
|
4147
|
+
output_path.write_text(output, encoding="utf-8")
|
|
4148
|
+
typer.echo("PR impact report written to " + str(output_path), err=True)
|
|
4149
|
+
else:
|
|
4150
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
4151
|
+
sys.stdout.buffer.write(b"\n")
|
|
4152
|
+
sys.stdout.buffer.flush()
|
|
4153
|
+
return
|
|
4154
|
+
|
|
4155
|
+
cir = build_canonical_ir(file_list, target)
|
|
4156
|
+
model = SpringSemanticModel.build(cir)
|
|
4157
|
+
report = run_pr_impact(cir, changed_files, root=target, model=model)
|
|
4158
|
+
|
|
4159
|
+
if format == "json":
|
|
4160
|
+
output = _json.dumps(report.to_dict(), indent=2, ensure_ascii=False)
|
|
4161
|
+
else:
|
|
4162
|
+
output = report.render_text()
|
|
4163
|
+
|
|
4164
|
+
if output_path is not None:
|
|
4165
|
+
output_path.write_text(output, encoding="utf-8")
|
|
4166
|
+
typer.echo(
|
|
4167
|
+
f"PR impact report written to {output_path} "
|
|
4168
|
+
f"(risk: {report.risk_level}, "
|
|
4169
|
+
f"{len(report.modified_classes)} classes, "
|
|
4170
|
+
f"{len(report.affected_endpoints)} endpoints)",
|
|
4171
|
+
err=True,
|
|
4172
|
+
)
|
|
4173
|
+
else:
|
|
4174
|
+
sys.stdout.buffer.write(output.encode("utf-8"))
|
|
4175
|
+
sys.stdout.buffer.write(b"\n")
|
|
4176
|
+
sys.stdout.buffer.flush()
|
|
4177
|
+
if copy:
|
|
4178
|
+
if _copy_to_clipboard(output):
|
|
4179
|
+
typer.echo("✓ copied to clipboard", err=True)
|
|
4180
|
+
|
|
4181
|
+
|
|
4030
4182
|
# ── Enterprise Workflow Commands ──────────────────────────────────────────────
|
|
4031
4183
|
#
|
|
4032
4184
|
# These are the five canonical enterprise workflows. Each is a thin wrapper
|
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""pr_impact.py — PR Impact Report: blast radius for a list of changed Java files.
|
|
2
|
+
|
|
3
|
+
Answers: "What can I break if I merge this PR?"
|
|
4
|
+
|
|
5
|
+
Aggregates run_impact_chain() + event topology across all classes in changed files.
|
|
6
|
+
Produces a consolidated text report + structured dict.
|
|
7
|
+
|
|
8
|
+
Reuses: CIR, SpringSemanticModel, ImpactOrchestrator, EventGraph.
|
|
9
|
+
No new parsers or CIR traversals.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
cir = build_canonical_ir(find_java_files(root), root)
|
|
13
|
+
report = run_pr_impact(cir, ["src/.../UserService.java"], root=root)
|
|
14
|
+
print(report.render_text())
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import TYPE_CHECKING, Optional
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
25
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
26
|
+
|
|
27
|
+
_RISK_ORDER: dict[str, int] = {
|
|
28
|
+
"critical": 4, "high": 3, "medium": 2, "low": 1, "unknown": 0
|
|
29
|
+
}
|
|
30
|
+
_RISK_LABEL: dict[int, str] = {
|
|
31
|
+
4: "CRITICAL", 3: "HIGH", 2: "MEDIUM", 1: "LOW", 0: "UNKNOWN"
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
# Output model
|
|
37
|
+
# ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class PRImpactReport:
|
|
41
|
+
"""Consolidated impact report for a list of changed Java files."""
|
|
42
|
+
|
|
43
|
+
modified_classes: list[str] = field(default_factory=list)
|
|
44
|
+
affected_endpoints: list[dict] = field(default_factory=list) # {method, path}
|
|
45
|
+
direct_callers: list[str] = field(default_factory=list)
|
|
46
|
+
event_publishers: list[str] = field(default_factory=list) # human lines
|
|
47
|
+
event_consumers: list[str] = field(default_factory=list) # human lines
|
|
48
|
+
transactional_methods: list[str] = field(default_factory=list)
|
|
49
|
+
risk_level: str = "UNKNOWN"
|
|
50
|
+
risk_reason: str = ""
|
|
51
|
+
analysis_warnings: list[str] = field(default_factory=list)
|
|
52
|
+
metadata: dict = field(default_factory=dict)
|
|
53
|
+
|
|
54
|
+
def to_dict(self) -> dict:
|
|
55
|
+
return {
|
|
56
|
+
"schema_version": "1.0",
|
|
57
|
+
"modified_classes": self.modified_classes,
|
|
58
|
+
"affected_endpoints": self.affected_endpoints,
|
|
59
|
+
"direct_callers": self.direct_callers,
|
|
60
|
+
"event_flow": {
|
|
61
|
+
"publishers": self.event_publishers,
|
|
62
|
+
"consumers": self.event_consumers,
|
|
63
|
+
},
|
|
64
|
+
"transactional_methods": self.transactional_methods,
|
|
65
|
+
"risk_level": self.risk_level,
|
|
66
|
+
"risk_reason": self.risk_reason,
|
|
67
|
+
"analysis_warnings": self.analysis_warnings,
|
|
68
|
+
"metadata": self.metadata,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def render_text(self) -> str:
|
|
72
|
+
sep = "=" * 50
|
|
73
|
+
lines = [sep, "PR IMPACT REPORT", "=" * 16, ""]
|
|
74
|
+
|
|
75
|
+
def _short(fqn: str) -> str:
|
|
76
|
+
return fqn.rsplit(".", 1)[-1] if "." in fqn else fqn
|
|
77
|
+
|
|
78
|
+
def _short_method(fqn: str) -> str:
|
|
79
|
+
if "#" in fqn:
|
|
80
|
+
cls, meth = fqn.rsplit("#", 1)
|
|
81
|
+
return f"{_short(cls)}.{meth}()"
|
|
82
|
+
return _short(fqn)
|
|
83
|
+
|
|
84
|
+
lines.append("Modified:")
|
|
85
|
+
lines.append("")
|
|
86
|
+
if self.modified_classes:
|
|
87
|
+
for cls in self.modified_classes:
|
|
88
|
+
lines.append(f" * {_short(cls)}")
|
|
89
|
+
else:
|
|
90
|
+
lines.append(" (no Spring classes found in changed files)")
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
if self.affected_endpoints:
|
|
94
|
+
lines.append("Affected Endpoints:")
|
|
95
|
+
lines.append("")
|
|
96
|
+
for ep in self.affected_endpoints:
|
|
97
|
+
lines.append(f" * {ep.get('method', '?')} {ep.get('path', '?')}")
|
|
98
|
+
lines.append("")
|
|
99
|
+
|
|
100
|
+
if self.direct_callers:
|
|
101
|
+
lines.append("Direct Callers:")
|
|
102
|
+
lines.append("")
|
|
103
|
+
for caller in self.direct_callers:
|
|
104
|
+
lines.append(f" * {_short(caller)}")
|
|
105
|
+
lines.append("")
|
|
106
|
+
|
|
107
|
+
event_items = self.event_publishers + self.event_consumers
|
|
108
|
+
if event_items:
|
|
109
|
+
lines.append("Event Flow:")
|
|
110
|
+
lines.append("")
|
|
111
|
+
for item in event_items:
|
|
112
|
+
lines.append(f" * {item}")
|
|
113
|
+
lines.append("")
|
|
114
|
+
|
|
115
|
+
if self.transactional_methods:
|
|
116
|
+
lines.append("Transactional Impact:")
|
|
117
|
+
lines.append("")
|
|
118
|
+
for m in self.transactional_methods:
|
|
119
|
+
lines.append(f" * {_short_method(m)}")
|
|
120
|
+
lines.append("")
|
|
121
|
+
|
|
122
|
+
lines.append("Risk Level:")
|
|
123
|
+
lines.append(self.risk_level)
|
|
124
|
+
lines.append("")
|
|
125
|
+
|
|
126
|
+
if self.risk_reason:
|
|
127
|
+
lines.append("Reason:")
|
|
128
|
+
lines.append(self.risk_reason)
|
|
129
|
+
lines.append("")
|
|
130
|
+
|
|
131
|
+
lines.append(sep)
|
|
132
|
+
return "\n".join(lines)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# File → class mapping
|
|
137
|
+
# ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
def _build_file_class_index(cir: "CanonicalRepositoryIR") -> dict[str, list[str]]:
|
|
140
|
+
"""Return {relative_source_file: [class_fqns]} from CIR raw IR nodes.
|
|
141
|
+
|
|
142
|
+
Only collects class-level nodes (no '#' in fqn) — method/field nodes are excluded
|
|
143
|
+
because impact-chain is queried at class granularity.
|
|
144
|
+
"""
|
|
145
|
+
index: dict[str, list[str]] = {}
|
|
146
|
+
nodes: list[dict] = (cir._raw_ir.get("graph") or {}).get("nodes") or []
|
|
147
|
+
for node in nodes:
|
|
148
|
+
fqn: str = node.get("fqn") or ""
|
|
149
|
+
sf: str = node.get("source_file") or ""
|
|
150
|
+
if not fqn or not sf or "#" in fqn:
|
|
151
|
+
continue
|
|
152
|
+
index.setdefault(sf, []).append(fqn)
|
|
153
|
+
return index
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _resolve_changed_files(
|
|
157
|
+
file_list: list[str],
|
|
158
|
+
file_class_index: dict[str, list[str]],
|
|
159
|
+
root: Path,
|
|
160
|
+
) -> tuple[list[str], list[str]]:
|
|
161
|
+
"""Map changed file paths to class FQNs.
|
|
162
|
+
|
|
163
|
+
Matching order:
|
|
164
|
+
1. Exact key in file_class_index (path already relative to repo root)
|
|
165
|
+
2. Relative path derived from absolute path via root
|
|
166
|
+
3. Suffix match (e.g., "UserService.java" matches any CIR file ending with it)
|
|
167
|
+
|
|
168
|
+
Returns (class_fqns, warnings). class_fqns is deduplicated, order-preserving.
|
|
169
|
+
"""
|
|
170
|
+
class_fqns: list[str] = []
|
|
171
|
+
warnings: list[str] = []
|
|
172
|
+
seen_classes: set[str] = set()
|
|
173
|
+
|
|
174
|
+
for raw_path in file_list:
|
|
175
|
+
path_str = raw_path.strip()
|
|
176
|
+
if not path_str:
|
|
177
|
+
continue
|
|
178
|
+
|
|
179
|
+
norm = path_str.replace("\\", "/")
|
|
180
|
+
candidates: list[str] = []
|
|
181
|
+
|
|
182
|
+
# 1. Exact match
|
|
183
|
+
if norm in file_class_index:
|
|
184
|
+
candidates = file_class_index[norm]
|
|
185
|
+
else:
|
|
186
|
+
# 2. Absolute path → relative to root
|
|
187
|
+
try:
|
|
188
|
+
abs_p = Path(path_str)
|
|
189
|
+
if abs_p.is_absolute():
|
|
190
|
+
rel_str = str(abs_p.relative_to(root)).replace("\\", "/")
|
|
191
|
+
if rel_str in file_class_index:
|
|
192
|
+
candidates = file_class_index[rel_str]
|
|
193
|
+
except (ValueError, Exception):
|
|
194
|
+
pass
|
|
195
|
+
|
|
196
|
+
# 3. Suffix match
|
|
197
|
+
if not candidates:
|
|
198
|
+
matches = [
|
|
199
|
+
k for k in file_class_index
|
|
200
|
+
if k == norm or k.endswith("/" + norm.lstrip("/"))
|
|
201
|
+
]
|
|
202
|
+
if len(matches) == 1:
|
|
203
|
+
candidates = file_class_index[matches[0]]
|
|
204
|
+
elif len(matches) > 1:
|
|
205
|
+
warnings.append(
|
|
206
|
+
f"Ambiguous path '{path_str}' matched {len(matches)} files; "
|
|
207
|
+
"using first match."
|
|
208
|
+
)
|
|
209
|
+
candidates = file_class_index[matches[0]]
|
|
210
|
+
|
|
211
|
+
if not candidates:
|
|
212
|
+
warnings.append(
|
|
213
|
+
f"No Spring classes found for '{path_str}' — "
|
|
214
|
+
"file not in CIR or has no class symbols."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
for cls in candidates:
|
|
218
|
+
if cls not in seen_classes:
|
|
219
|
+
seen_classes.add(cls)
|
|
220
|
+
class_fqns.append(cls)
|
|
221
|
+
|
|
222
|
+
return class_fqns, warnings
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
# ---------------------------------------------------------------------------
|
|
226
|
+
# Event flow
|
|
227
|
+
# ---------------------------------------------------------------------------
|
|
228
|
+
|
|
229
|
+
def _collect_event_flow(
|
|
230
|
+
class_fqns_set: set[str],
|
|
231
|
+
model: "SpringSemanticModel",
|
|
232
|
+
) -> tuple[list[str], list[str]]:
|
|
233
|
+
"""Return (publisher_lines, consumer_lines) describing event flow for changed classes.
|
|
234
|
+
|
|
235
|
+
publisher_lines: "Publishes <EventType>" for events published by changed classes.
|
|
236
|
+
consumer_lines: "Consumed by <Listener>" when changed class publishes an event that
|
|
237
|
+
has listeners, or "Listens to <EventType>" when a changed class
|
|
238
|
+
is itself a listener.
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
def _short(fqn: str) -> str:
|
|
242
|
+
return fqn.rsplit(".", 1)[-1] if "." in fqn else fqn
|
|
243
|
+
|
|
244
|
+
def _class_of(fqn: str) -> str:
|
|
245
|
+
return fqn.split("#")[0] if "#" in fqn else fqn
|
|
246
|
+
|
|
247
|
+
publisher_lines: list[str] = []
|
|
248
|
+
consumer_lines: list[str] = []
|
|
249
|
+
seen: set[str] = set()
|
|
250
|
+
|
|
251
|
+
# Changed class publishes an event → report publish + downstream consumers
|
|
252
|
+
for event_type, publishers in model.event_graph.publishers.items():
|
|
253
|
+
for pub_fqn in publishers:
|
|
254
|
+
if _class_of(pub_fqn) not in class_fqns_set:
|
|
255
|
+
continue
|
|
256
|
+
pub_key = f"pub:{event_type}"
|
|
257
|
+
if pub_key not in seen:
|
|
258
|
+
seen.add(pub_key)
|
|
259
|
+
publisher_lines.append(f"Publishes {_short(event_type)}")
|
|
260
|
+
for consumer_fqn in model.event_graph.listeners_of(event_type):
|
|
261
|
+
con_key = f"con:{event_type}:{consumer_fqn}"
|
|
262
|
+
if con_key not in seen:
|
|
263
|
+
seen.add(con_key)
|
|
264
|
+
consumer_lines.append(f"Consumed by {_short(consumer_fqn)}")
|
|
265
|
+
|
|
266
|
+
# Changed class is a listener → report what it listens to
|
|
267
|
+
for event_type, listeners in model.event_graph.listeners.items():
|
|
268
|
+
for lst_fqn in listeners:
|
|
269
|
+
if _class_of(lst_fqn) not in class_fqns_set:
|
|
270
|
+
continue
|
|
271
|
+
lst_class = _class_of(lst_fqn)
|
|
272
|
+
lst_key = f"lst:{lst_class}:{event_type}"
|
|
273
|
+
if lst_key not in seen:
|
|
274
|
+
seen.add(lst_key)
|
|
275
|
+
consumer_lines.append(f"Listens to {_short(event_type)}")
|
|
276
|
+
|
|
277
|
+
return publisher_lines, consumer_lines
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# ---------------------------------------------------------------------------
|
|
281
|
+
# Transactional methods
|
|
282
|
+
# ---------------------------------------------------------------------------
|
|
283
|
+
|
|
284
|
+
def _collect_tx_methods(
|
|
285
|
+
class_fqns_set: set[str],
|
|
286
|
+
model: "SpringSemanticModel",
|
|
287
|
+
) -> list[str]:
|
|
288
|
+
"""Return FQNs with @Transactional boundaries declared in changed classes."""
|
|
289
|
+
tx_methods: list[str] = []
|
|
290
|
+
seen: set[str] = set()
|
|
291
|
+
|
|
292
|
+
for cls in class_fqns_set:
|
|
293
|
+
# Class-level @Transactional: the class symbol itself is the boundary
|
|
294
|
+
if cls in model.tx_index.class_level:
|
|
295
|
+
if cls not in seen:
|
|
296
|
+
seen.add(cls)
|
|
297
|
+
tx_methods.append(cls)
|
|
298
|
+
# Method-level @Transactional
|
|
299
|
+
for boundary in model.tx_index.by_class.get(cls, []):
|
|
300
|
+
sym = boundary.symbol
|
|
301
|
+
if sym not in seen:
|
|
302
|
+
seen.add(sym)
|
|
303
|
+
tx_methods.append(sym)
|
|
304
|
+
|
|
305
|
+
return tx_methods
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ---------------------------------------------------------------------------
|
|
309
|
+
# Risk consolidation
|
|
310
|
+
# ---------------------------------------------------------------------------
|
|
311
|
+
|
|
312
|
+
def _compute_risk(
|
|
313
|
+
endpoints: list[dict],
|
|
314
|
+
callers: list[str],
|
|
315
|
+
event_publishers: list[str],
|
|
316
|
+
event_consumers: list[str],
|
|
317
|
+
tx_methods: list[str],
|
|
318
|
+
individual_risks: list[str],
|
|
319
|
+
) -> tuple[str, str]:
|
|
320
|
+
"""Return (risk_level_label, reason_string).
|
|
321
|
+
|
|
322
|
+
Base risk from individual impact chains. Boost when multiple dimensions present.
|
|
323
|
+
"""
|
|
324
|
+
base = max((_RISK_ORDER.get(r.lower(), 0) for r in individual_risks), default=0)
|
|
325
|
+
|
|
326
|
+
reasons: list[str] = []
|
|
327
|
+
if endpoints:
|
|
328
|
+
reasons.append("Public API")
|
|
329
|
+
if event_publishers or event_consumers:
|
|
330
|
+
reasons.append("Event Flow")
|
|
331
|
+
if tx_methods:
|
|
332
|
+
reasons.append("Transaction Boundary")
|
|
333
|
+
if len(callers) > 5:
|
|
334
|
+
reasons.append("High Call Fan-in")
|
|
335
|
+
|
|
336
|
+
level = base
|
|
337
|
+
if len(reasons) >= 3 and level < _RISK_ORDER["high"]:
|
|
338
|
+
level = _RISK_ORDER["high"]
|
|
339
|
+
elif len(reasons) >= 2 and level < _RISK_ORDER["medium"]:
|
|
340
|
+
level = _RISK_ORDER["medium"]
|
|
341
|
+
|
|
342
|
+
label = _RISK_LABEL.get(level, "LOW")
|
|
343
|
+
reason = " + ".join(reasons) if reasons else "No high-risk signals detected"
|
|
344
|
+
return label, reason
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
# ---------------------------------------------------------------------------
|
|
348
|
+
# Entry point
|
|
349
|
+
# ---------------------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
def run_pr_impact(
|
|
352
|
+
cir: "CanonicalRepositoryIR",
|
|
353
|
+
changed_files: list[str],
|
|
354
|
+
*,
|
|
355
|
+
root: Path,
|
|
356
|
+
model: Optional["SpringSemanticModel"] = None,
|
|
357
|
+
) -> PRImpactReport:
|
|
358
|
+
"""Run PR impact analysis for a list of changed Java file paths.
|
|
359
|
+
|
|
360
|
+
Args:
|
|
361
|
+
cir: CanonicalRepositoryIR from build_canonical_ir().
|
|
362
|
+
changed_files: Paths to changed Java files (relative, absolute, or bare name).
|
|
363
|
+
root: Repo root (used for absolute-path normalization).
|
|
364
|
+
model: Pre-built SpringSemanticModel. Built internally if None.
|
|
365
|
+
|
|
366
|
+
Returns PRImpactReport — always serializable, never raises.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
return _run_pr_impact_internal(cir, changed_files, root=root, model=model)
|
|
370
|
+
except Exception as exc:
|
|
371
|
+
return PRImpactReport(
|
|
372
|
+
risk_level="UNKNOWN",
|
|
373
|
+
risk_reason="Internal error during analysis.",
|
|
374
|
+
analysis_warnings=[f"Internal error: {type(exc).__name__}: {exc}"],
|
|
375
|
+
metadata={"changed_files_count": len(changed_files)},
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def _run_pr_impact_internal(
|
|
380
|
+
cir: "CanonicalRepositoryIR",
|
|
381
|
+
changed_files: list[str],
|
|
382
|
+
*,
|
|
383
|
+
root: Path,
|
|
384
|
+
model: Optional["SpringSemanticModel"],
|
|
385
|
+
) -> PRImpactReport:
|
|
386
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
387
|
+
from sourcecode.spring_impact import run_impact_chain
|
|
388
|
+
|
|
389
|
+
t0 = time.monotonic()
|
|
390
|
+
warnings: list[str] = []
|
|
391
|
+
|
|
392
|
+
if model is None:
|
|
393
|
+
model = SpringSemanticModel.build(cir)
|
|
394
|
+
|
|
395
|
+
# 1. Map file paths → class FQNs
|
|
396
|
+
file_class_index = _build_file_class_index(cir)
|
|
397
|
+
class_fqns, file_warnings = _resolve_changed_files(changed_files, file_class_index, root)
|
|
398
|
+
warnings.extend(file_warnings)
|
|
399
|
+
|
|
400
|
+
if not class_fqns:
|
|
401
|
+
return PRImpactReport(
|
|
402
|
+
risk_level="UNKNOWN",
|
|
403
|
+
risk_reason="No Spring classes found in changed files.",
|
|
404
|
+
analysis_warnings=warnings,
|
|
405
|
+
metadata={
|
|
406
|
+
"changed_files_count": len(changed_files),
|
|
407
|
+
"classes_analyzed": 0,
|
|
408
|
+
},
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
class_fqns_set = set(class_fqns)
|
|
412
|
+
|
|
413
|
+
# 2. Impact chain per modified class
|
|
414
|
+
all_direct_callers: list[str] = []
|
|
415
|
+
all_endpoints: list[dict] = []
|
|
416
|
+
individual_risks: list[str] = []
|
|
417
|
+
seen_callers: set[str] = set()
|
|
418
|
+
seen_ep_ids: set[str] = set()
|
|
419
|
+
|
|
420
|
+
for cls in class_fqns:
|
|
421
|
+
result = run_impact_chain(
|
|
422
|
+
cir, cls,
|
|
423
|
+
root=root,
|
|
424
|
+
model=model,
|
|
425
|
+
prebuilt_findings=[], # skip audit findings — focus on structural impact
|
|
426
|
+
)
|
|
427
|
+
warnings.extend(result.analysis_warnings)
|
|
428
|
+
individual_risks.append(result.risk_level)
|
|
429
|
+
|
|
430
|
+
for caller in result.direct_callers:
|
|
431
|
+
caller_class = caller.split("#")[0] if "#" in caller else caller
|
|
432
|
+
if caller_class not in class_fqns_set and caller_class not in seen_callers:
|
|
433
|
+
seen_callers.add(caller_class)
|
|
434
|
+
all_direct_callers.append(caller_class)
|
|
435
|
+
|
|
436
|
+
for ep in result.endpoints_affected:
|
|
437
|
+
ep_id = ep.endpoint_id
|
|
438
|
+
if ep_id not in seen_ep_ids:
|
|
439
|
+
seen_ep_ids.add(ep_id)
|
|
440
|
+
all_endpoints.append({"method": ep.method, "path": ep.path})
|
|
441
|
+
|
|
442
|
+
# 3. Event flow
|
|
443
|
+
event_publishers, event_consumers = _collect_event_flow(class_fqns_set, model)
|
|
444
|
+
|
|
445
|
+
# 4. Transactional boundaries in changed classes
|
|
446
|
+
tx_methods = _collect_tx_methods(class_fqns_set, model)
|
|
447
|
+
|
|
448
|
+
# 5. Consolidated risk
|
|
449
|
+
risk_level, risk_reason = _compute_risk(
|
|
450
|
+
all_endpoints,
|
|
451
|
+
all_direct_callers,
|
|
452
|
+
event_publishers,
|
|
453
|
+
event_consumers,
|
|
454
|
+
tx_methods,
|
|
455
|
+
individual_risks,
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
elapsed_ms = round((time.monotonic() - t0) * 1000, 2)
|
|
459
|
+
|
|
460
|
+
return PRImpactReport(
|
|
461
|
+
modified_classes=class_fqns,
|
|
462
|
+
affected_endpoints=all_endpoints,
|
|
463
|
+
direct_callers=all_direct_callers,
|
|
464
|
+
event_publishers=event_publishers,
|
|
465
|
+
event_consumers=event_consumers,
|
|
466
|
+
transactional_methods=tx_methods,
|
|
467
|
+
risk_level=risk_level,
|
|
468
|
+
risk_reason=risk_reason,
|
|
469
|
+
analysis_warnings=warnings,
|
|
470
|
+
metadata={
|
|
471
|
+
"changed_files_count": len(changed_files),
|
|
472
|
+
"classes_analyzed": len(class_fqns),
|
|
473
|
+
"analysis_time_ms": elapsed_ms,
|
|
474
|
+
},
|
|
475
|
+
)
|
|
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
|