sourcecode 1.35.13__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.13 → sourcecode-1.35.14}/PKG-INFO +3 -3
- {sourcecode-1.35.13 → sourcecode-1.35.14}/README.md +2 -2
- {sourcecode-1.35.13 → sourcecode-1.35.14}/pyproject.toml +1 -1
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/cli.py +117 -0
- sourcecode-1.35.14/src/sourcecode/explain.py +483 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/.gitignore +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/.ruff.toml +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/CHANGELOG.md +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/LICENSE +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/SECURITY.md +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/raw +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/canonical_ir.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/dependency_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp/server.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/repository_ir.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_security_audit.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/spring_tx_analyzer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.13 → sourcecode-1.35.14}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.13 → 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"
|
|
@@ -229,6 +229,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
|
|
|
229
229
|
"impact-chain",
|
|
230
230
|
# PR blast-radius report
|
|
231
231
|
"pr-impact",
|
|
232
|
+
# Class architectural summary
|
|
233
|
+
"explain",
|
|
232
234
|
}
|
|
233
235
|
)
|
|
234
236
|
|
|
@@ -4179,6 +4181,121 @@ def pr_impact_cmd(
|
|
|
4179
4181
|
typer.echo("✓ copied to clipboard", err=True)
|
|
4180
4182
|
|
|
4181
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
|
+
|
|
4182
4299
|
# ── Enterprise Workflow Commands ──────────────────────────────────────────────
|
|
4183
4300
|
#
|
|
4184
4301
|
# These are the five canonical enterprise workflows. Each is a thin wrapper
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
"""explain.py — Human-readable architectural summary for a Java class.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
cir = build_canonical_ir(find_java_files(root), root)
|
|
5
|
+
model = SpringSemanticModel.build(cir)
|
|
6
|
+
result = explain_class("UserService", cir, model)
|
|
7
|
+
print(result.render_text())
|
|
8
|
+
|
|
9
|
+
Derives all data from existing CIR + SpringSemanticModel — no new parsers.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import TYPE_CHECKING, Optional
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from sourcecode.canonical_ir import CanonicalRepositoryIR
|
|
18
|
+
from sourcecode.spring_model import SpringSemanticModel
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_STEREOTYPE_DESC: dict[str, str] = {
|
|
22
|
+
"service": "Spring @Service — business logic layer",
|
|
23
|
+
"repository": "Spring @Repository — data access layer",
|
|
24
|
+
"controller": "Spring @Controller — MVC request handler",
|
|
25
|
+
"restcontroller": "Spring @RestController — REST request handler",
|
|
26
|
+
"component": "Spring @Component — general-purpose bean",
|
|
27
|
+
"configuration": "Spring @Configuration — bean factory / config",
|
|
28
|
+
"bean": "Spring @Bean — managed component",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
_SECURITY_ANNOTATION_PREFIXES = (
|
|
32
|
+
"@PreAuthorize", "@PostAuthorize", "@Secured", "@RolesAllowed",
|
|
33
|
+
"@PermitAll", "@DenyAll",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
_DEFAULT_PROPAGATION = "REQUIRED"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _simple(fqn: str) -> str:
|
|
40
|
+
"""pkg.Foo#bar → Foo#bar; pkg.Foo → Foo."""
|
|
41
|
+
if "." not in fqn and "#" not in fqn:
|
|
42
|
+
return fqn
|
|
43
|
+
# Strip package prefix before simple class name
|
|
44
|
+
if "#" in fqn:
|
|
45
|
+
cls_part, method = fqn.rsplit("#", 1)
|
|
46
|
+
return f"{cls_part.rsplit('.', 1)[-1]}#{method}"
|
|
47
|
+
return fqn.rsplit(".", 1)[-1]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _method_name(fqn: str) -> str:
|
|
51
|
+
"""pkg.Foo#bar → bar."""
|
|
52
|
+
return fqn.rsplit("#", 1)[-1] if "#" in fqn else fqn
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Output model
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
@dataclass
|
|
60
|
+
class ClassExplanation:
|
|
61
|
+
"""Structured architectural summary for a single class."""
|
|
62
|
+
|
|
63
|
+
class_name: str
|
|
64
|
+
class_fqn: str
|
|
65
|
+
stereotype: str
|
|
66
|
+
purpose: str
|
|
67
|
+
public_methods: list[str] = field(default_factory=list)
|
|
68
|
+
incoming_callers: list[str] = field(default_factory=list)
|
|
69
|
+
outgoing_deps: list[str] = field(default_factory=list)
|
|
70
|
+
events_published: list[str] = field(default_factory=list)
|
|
71
|
+
events_consumed: list[str] = field(default_factory=list)
|
|
72
|
+
transactions: list[str] = field(default_factory=list)
|
|
73
|
+
security_constraints: list[str] = field(default_factory=list)
|
|
74
|
+
rest_endpoints: list[str] = field(default_factory=list)
|
|
75
|
+
warnings: list[str] = field(default_factory=list)
|
|
76
|
+
|
|
77
|
+
def render_text(self) -> str:
|
|
78
|
+
lines: list[str] = [f"## {self.class_name}", ""]
|
|
79
|
+
|
|
80
|
+
if self.class_fqn != self.class_name:
|
|
81
|
+
lines += [f"**FQN:** `{self.class_fqn}`", ""]
|
|
82
|
+
|
|
83
|
+
lines += ["**Purpose:**", self.purpose, ""]
|
|
84
|
+
|
|
85
|
+
def _section(title: str, items: list[str]) -> None:
|
|
86
|
+
if not items:
|
|
87
|
+
return
|
|
88
|
+
lines.append(f"**{title}:**")
|
|
89
|
+
for item in items:
|
|
90
|
+
lines.append(f"* {item}")
|
|
91
|
+
lines.append("")
|
|
92
|
+
|
|
93
|
+
_section("Public Methods", self.public_methods)
|
|
94
|
+
_section("Used By", self.incoming_callers)
|
|
95
|
+
_section("Calls", self.outgoing_deps)
|
|
96
|
+
_section("Publishes", self.events_published)
|
|
97
|
+
_section("Consumes", self.events_consumed)
|
|
98
|
+
_section("Transactions", self.transactions)
|
|
99
|
+
_section("Security", self.security_constraints)
|
|
100
|
+
_section("REST Endpoints", self.rest_endpoints)
|
|
101
|
+
_section("Warnings", self.warnings)
|
|
102
|
+
|
|
103
|
+
return "\n".join(lines).rstrip()
|
|
104
|
+
|
|
105
|
+
def to_dict(self) -> dict:
|
|
106
|
+
return {
|
|
107
|
+
"class_name": self.class_name,
|
|
108
|
+
"class_fqn": self.class_fqn,
|
|
109
|
+
"stereotype": self.stereotype,
|
|
110
|
+
"purpose": self.purpose,
|
|
111
|
+
"public_methods": self.public_methods,
|
|
112
|
+
"incoming_callers": self.incoming_callers,
|
|
113
|
+
"outgoing_deps": self.outgoing_deps,
|
|
114
|
+
"events_published": self.events_published,
|
|
115
|
+
"events_consumed": self.events_consumed,
|
|
116
|
+
"transactions": self.transactions,
|
|
117
|
+
"security_constraints": self.security_constraints,
|
|
118
|
+
"rest_endpoints": self.rest_endpoints,
|
|
119
|
+
"warnings": self.warnings,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ---------------------------------------------------------------------------
|
|
124
|
+
# FQN resolution
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
def _resolve_fqn(class_name: str, cir: "CanonicalRepositoryIR") -> tuple[str, list[str]]:
|
|
128
|
+
"""Find all class FQNs matching simple class_name in cir.symbols.
|
|
129
|
+
|
|
130
|
+
Returns (best_fqn, all_matches).
|
|
131
|
+
best_fqn is empty string when no match found.
|
|
132
|
+
"""
|
|
133
|
+
suffix_dot = f".{class_name}"
|
|
134
|
+
suffix_hash = f"{class_name}#"
|
|
135
|
+
matches: list[str] = []
|
|
136
|
+
|
|
137
|
+
for sym in (getattr(cir, "symbols", None) or []):
|
|
138
|
+
if not isinstance(sym, str):
|
|
139
|
+
continue
|
|
140
|
+
if "#" in sym:
|
|
141
|
+
continue # method symbol — skip
|
|
142
|
+
# Exact match or package-qualified match
|
|
143
|
+
if sym == class_name or sym.endswith(suffix_dot):
|
|
144
|
+
matches.append(sym)
|
|
145
|
+
|
|
146
|
+
# Also scan raw_ir graph nodes for class symbols (more reliable for kind)
|
|
147
|
+
raw_nodes = _get_raw_nodes(cir)
|
|
148
|
+
node_fqns: set[str] = set()
|
|
149
|
+
for node in raw_nodes:
|
|
150
|
+
fqn = node.get("fqn") or ""
|
|
151
|
+
if "#" in fqn:
|
|
152
|
+
continue
|
|
153
|
+
kind = node.get("symbol_kind") or node.get("type") or ""
|
|
154
|
+
if kind not in ("class", "interface", "enum", "annotation", ""):
|
|
155
|
+
continue
|
|
156
|
+
if fqn == class_name or fqn.endswith(suffix_dot):
|
|
157
|
+
node_fqns.add(fqn)
|
|
158
|
+
|
|
159
|
+
# Merge — prefer node_fqns when available
|
|
160
|
+
all_fqns = list(dict.fromkeys(list(node_fqns) + matches))
|
|
161
|
+
|
|
162
|
+
if not all_fqns:
|
|
163
|
+
return "", []
|
|
164
|
+
return all_fqns[0], all_fqns
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _get_raw_nodes(cir: "CanonicalRepositoryIR") -> list[dict]:
|
|
168
|
+
raw_ir = getattr(cir, "_raw_ir", None) or {}
|
|
169
|
+
return (raw_ir.get("graph") or {}).get("nodes") or []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
# Section builders
|
|
174
|
+
# ---------------------------------------------------------------------------
|
|
175
|
+
|
|
176
|
+
def _build_purpose(
|
|
177
|
+
class_fqn: str,
|
|
178
|
+
raw_nodes: list[dict],
|
|
179
|
+
stereotype: str,
|
|
180
|
+
cir: "CanonicalRepositoryIR",
|
|
181
|
+
model: "SpringSemanticModel",
|
|
182
|
+
) -> str:
|
|
183
|
+
# Collect class-level annotations from raw_ir node
|
|
184
|
+
class_anns: list[str] = []
|
|
185
|
+
parent_sig = ""
|
|
186
|
+
for node in raw_nodes:
|
|
187
|
+
if node.get("fqn") == class_fqn:
|
|
188
|
+
class_anns = node.get("annotations") or []
|
|
189
|
+
break
|
|
190
|
+
|
|
191
|
+
# Inheritance
|
|
192
|
+
parent_sig = model.inheritance.immediate_parent(class_fqn)
|
|
193
|
+
|
|
194
|
+
# Base description from stereotype
|
|
195
|
+
desc = _STEREOTYPE_DESC.get(stereotype, "")
|
|
196
|
+
|
|
197
|
+
# Enrich with class-level @Transactional
|
|
198
|
+
tx_class = model.tx_index.class_level.get(class_fqn)
|
|
199
|
+
if tx_class:
|
|
200
|
+
desc = f"{desc}. Class-level @Transactional ({tx_class.propagation})" if desc else f"@Transactional ({tx_class.propagation})"
|
|
201
|
+
|
|
202
|
+
# Enrich with parent
|
|
203
|
+
if parent_sig:
|
|
204
|
+
parent_simple = _simple(parent_sig.split("<")[0])
|
|
205
|
+
if desc:
|
|
206
|
+
desc = f"{desc}. Extends {parent_simple}"
|
|
207
|
+
else:
|
|
208
|
+
desc = f"Extends {parent_simple}"
|
|
209
|
+
|
|
210
|
+
# Fallback: derive from annotations present
|
|
211
|
+
if not desc:
|
|
212
|
+
role_anns = [a for a in class_anns if a in (
|
|
213
|
+
"@Service", "@Repository", "@Controller", "@RestController",
|
|
214
|
+
"@Component", "@Configuration",
|
|
215
|
+
)]
|
|
216
|
+
if role_anns:
|
|
217
|
+
desc = f"{role_anns[0]} bean"
|
|
218
|
+
|
|
219
|
+
return desc or "No stereotype detected — may be a plain class or utility."
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _build_public_methods(class_fqn: str, raw_nodes: list[dict]) -> list[str]:
|
|
223
|
+
"""Return public method names for class_fqn from raw_ir nodes."""
|
|
224
|
+
prefix = class_fqn + "#"
|
|
225
|
+
methods: list[str] = []
|
|
226
|
+
for node in raw_nodes:
|
|
227
|
+
fqn = node.get("fqn") or ""
|
|
228
|
+
if not fqn.startswith(prefix):
|
|
229
|
+
continue
|
|
230
|
+
kind = node.get("symbol_kind") or node.get("type") or ""
|
|
231
|
+
if kind not in ("method", "endpoint", "constructor", ""):
|
|
232
|
+
continue
|
|
233
|
+
modifiers: list[str] = node.get("modifiers") or []
|
|
234
|
+
if "public" not in modifiers and "protected" not in modifiers:
|
|
235
|
+
continue
|
|
236
|
+
method_name = fqn[len(prefix):]
|
|
237
|
+
if method_name and not method_name.startswith("<"):
|
|
238
|
+
methods.append(method_name)
|
|
239
|
+
return sorted(set(methods))
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _build_callers(class_fqn: str, cir: "CanonicalRepositoryIR") -> list[str]:
|
|
243
|
+
"""DI dependents + reverse call graph callers, deduplicated, simple names."""
|
|
244
|
+
seen: set[str] = set()
|
|
245
|
+
result: list[str] = []
|
|
246
|
+
|
|
247
|
+
# DI injection dependents
|
|
248
|
+
for fqn in (getattr(cir, "injection_graph", None) and cir.injection_graph.dependents_of(class_fqn) or []):
|
|
249
|
+
s = _simple(fqn)
|
|
250
|
+
if s not in seen:
|
|
251
|
+
seen.add(s)
|
|
252
|
+
result.append(s)
|
|
253
|
+
|
|
254
|
+
# Direct call reverse graph: reverse_graph[target] → {type → [callers]}
|
|
255
|
+
rev: dict = (getattr(cir, "reverse_graph", None) or {}).get(class_fqn) or {}
|
|
256
|
+
for callers in rev.values():
|
|
257
|
+
for caller_fqn in (callers or []):
|
|
258
|
+
# Strip method part to get class
|
|
259
|
+
cls_fqn = caller_fqn.rsplit("#", 1)[0] if "#" in caller_fqn else caller_fqn
|
|
260
|
+
if cls_fqn == class_fqn:
|
|
261
|
+
continue
|
|
262
|
+
s = _simple(cls_fqn)
|
|
263
|
+
if s not in seen:
|
|
264
|
+
seen.add(s)
|
|
265
|
+
result.append(s)
|
|
266
|
+
|
|
267
|
+
return result
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _build_deps(class_fqn: str, cir: "CanonicalRepositoryIR") -> list[str]:
|
|
271
|
+
"""DI injected dependencies, simple names."""
|
|
272
|
+
deps = (getattr(cir, "injection_graph", None) and cir.injection_graph.dependencies_of(class_fqn) or [])
|
|
273
|
+
return sorted({_simple(d) for d in deps})
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _build_events_published(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
|
|
277
|
+
"""Event types published by this class (any method of the class)."""
|
|
278
|
+
prefix = class_fqn + "#"
|
|
279
|
+
result: list[str] = []
|
|
280
|
+
for event_type, publishers in model.event_graph.publishers.items():
|
|
281
|
+
for pub in publishers:
|
|
282
|
+
if pub == class_fqn or pub.startswith(prefix):
|
|
283
|
+
result.append(_simple(event_type))
|
|
284
|
+
break
|
|
285
|
+
return sorted(set(result))
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _build_events_consumed(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
|
|
289
|
+
"""Event types consumed/listened by this class."""
|
|
290
|
+
prefix = class_fqn + "#"
|
|
291
|
+
result: list[str] = []
|
|
292
|
+
for event_type, listeners in model.event_graph.listeners.items():
|
|
293
|
+
for lst in listeners:
|
|
294
|
+
if lst == class_fqn or lst.startswith(prefix):
|
|
295
|
+
result.append(_simple(event_type))
|
|
296
|
+
break
|
|
297
|
+
return sorted(set(result))
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def _build_transactions(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
|
|
301
|
+
"""Transaction boundaries for this class."""
|
|
302
|
+
result: list[str] = []
|
|
303
|
+
|
|
304
|
+
# Class-level
|
|
305
|
+
cls_tx = model.tx_index.class_level.get(class_fqn)
|
|
306
|
+
if cls_tx:
|
|
307
|
+
label = f"@Transactional (class-level, {cls_tx.propagation})"
|
|
308
|
+
if cls_tx.read_only:
|
|
309
|
+
label += ", readOnly"
|
|
310
|
+
result.append(label)
|
|
311
|
+
|
|
312
|
+
# Method-level
|
|
313
|
+
for boundary in (model.tx_index.by_class.get(class_fqn) or []):
|
|
314
|
+
method = _method_name(boundary.symbol)
|
|
315
|
+
label = method + "()"
|
|
316
|
+
extras: list[str] = []
|
|
317
|
+
if boundary.propagation != _DEFAULT_PROPAGATION:
|
|
318
|
+
extras.append(boundary.propagation)
|
|
319
|
+
if boundary.read_only:
|
|
320
|
+
extras.append("readOnly")
|
|
321
|
+
if extras:
|
|
322
|
+
label += f" [{', '.join(extras)}]"
|
|
323
|
+
result.append(label)
|
|
324
|
+
|
|
325
|
+
return result
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def _build_security(
|
|
329
|
+
class_fqn: str,
|
|
330
|
+
raw_nodes: list[dict],
|
|
331
|
+
cir: "CanonicalRepositoryIR",
|
|
332
|
+
) -> list[str]:
|
|
333
|
+
"""Security annotations from method-level nodes + cir.security_index."""
|
|
334
|
+
seen: set[str] = set()
|
|
335
|
+
result: list[str] = []
|
|
336
|
+
|
|
337
|
+
prefix = class_fqn + "#"
|
|
338
|
+
|
|
339
|
+
# From cir.security_index (handler_symbol → CanonicalSecurity)
|
|
340
|
+
for handler_sym, sec in (getattr(cir, "security_index", None) or {}).items():
|
|
341
|
+
if not (handler_sym == class_fqn or handler_sym.startswith(prefix)):
|
|
342
|
+
continue
|
|
343
|
+
policy = getattr(sec, "policy", "") or ""
|
|
344
|
+
roles = getattr(sec, "roles", []) or []
|
|
345
|
+
method = _method_name(handler_sym)
|
|
346
|
+
if roles:
|
|
347
|
+
label = f"{method}(): {policy} ({', '.join(roles)})"
|
|
348
|
+
else:
|
|
349
|
+
label = f"{method}(): {policy}"
|
|
350
|
+
if label not in seen:
|
|
351
|
+
seen.add(label)
|
|
352
|
+
result.append(label)
|
|
353
|
+
|
|
354
|
+
# From raw_ir method annotations
|
|
355
|
+
for node in raw_nodes:
|
|
356
|
+
fqn = node.get("fqn") or ""
|
|
357
|
+
if not fqn.startswith(prefix):
|
|
358
|
+
continue
|
|
359
|
+
anns: list[str] = node.get("annotations") or []
|
|
360
|
+
for ann in anns:
|
|
361
|
+
if any(ann.startswith(p) for p in _SECURITY_ANNOTATION_PREFIXES):
|
|
362
|
+
method = _method_name(fqn)
|
|
363
|
+
label = f"{method}(): {ann}"
|
|
364
|
+
if label not in seen:
|
|
365
|
+
seen.add(label)
|
|
366
|
+
result.append(label)
|
|
367
|
+
|
|
368
|
+
return result
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _build_endpoints(class_fqn: str, model: "SpringSemanticModel") -> list[str]:
|
|
372
|
+
"""REST endpoints declared on this controller class."""
|
|
373
|
+
endpoints = model.endpoint_index.endpoints_for(class_fqn)
|
|
374
|
+
result: list[str] = []
|
|
375
|
+
for ep in endpoints:
|
|
376
|
+
method = getattr(ep, "method", "") or "?"
|
|
377
|
+
path = getattr(ep, "path", "") or "?"
|
|
378
|
+
result.append(f"{method} {path}")
|
|
379
|
+
return sorted(set(result))
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
# ---------------------------------------------------------------------------
|
|
383
|
+
# Main entry point
|
|
384
|
+
# ---------------------------------------------------------------------------
|
|
385
|
+
|
|
386
|
+
def explain_class(
|
|
387
|
+
class_name: str,
|
|
388
|
+
cir: "CanonicalRepositoryIR",
|
|
389
|
+
model: "SpringSemanticModel",
|
|
390
|
+
) -> ClassExplanation:
|
|
391
|
+
"""Build a ClassExplanation for class_name from existing CIR + model.
|
|
392
|
+
|
|
393
|
+
Never raises — wraps all derivation in try/except.
|
|
394
|
+
"""
|
|
395
|
+
warnings: list[str] = []
|
|
396
|
+
|
|
397
|
+
try:
|
|
398
|
+
class_fqn, all_matches = _resolve_fqn(class_name, cir)
|
|
399
|
+
except Exception:
|
|
400
|
+
class_fqn, all_matches = "", []
|
|
401
|
+
|
|
402
|
+
if not class_fqn:
|
|
403
|
+
return ClassExplanation(
|
|
404
|
+
class_name=class_name,
|
|
405
|
+
class_fqn=class_name,
|
|
406
|
+
stereotype="unknown",
|
|
407
|
+
purpose="Class not found in repository symbols.",
|
|
408
|
+
warnings=[f"'{class_name}' not found in CIR symbols. Is this a Java/Kotlin repo?"],
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
if len(all_matches) > 1:
|
|
412
|
+
warnings.append(
|
|
413
|
+
f"Ambiguous: {len(all_matches)} classes named '{class_name}'. "
|
|
414
|
+
f"Showing first: {class_fqn}"
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
raw_nodes = _get_raw_nodes(cir)
|
|
418
|
+
|
|
419
|
+
try:
|
|
420
|
+
stereotype = model.bean_graph.get_stereotype(class_fqn) or "unknown"
|
|
421
|
+
except Exception:
|
|
422
|
+
stereotype = "unknown"
|
|
423
|
+
|
|
424
|
+
try:
|
|
425
|
+
purpose = _build_purpose(class_fqn, raw_nodes, stereotype, cir, model)
|
|
426
|
+
except Exception:
|
|
427
|
+
purpose = f"{stereotype} class"
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
public_methods = _build_public_methods(class_fqn, raw_nodes)
|
|
431
|
+
except Exception:
|
|
432
|
+
public_methods = []
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
incoming_callers = _build_callers(class_fqn, cir)
|
|
436
|
+
except Exception:
|
|
437
|
+
incoming_callers = []
|
|
438
|
+
|
|
439
|
+
try:
|
|
440
|
+
outgoing_deps = _build_deps(class_fqn, cir)
|
|
441
|
+
except Exception:
|
|
442
|
+
outgoing_deps = []
|
|
443
|
+
|
|
444
|
+
try:
|
|
445
|
+
events_published = _build_events_published(class_fqn, model)
|
|
446
|
+
except Exception:
|
|
447
|
+
events_published = []
|
|
448
|
+
|
|
449
|
+
try:
|
|
450
|
+
events_consumed = _build_events_consumed(class_fqn, model)
|
|
451
|
+
except Exception:
|
|
452
|
+
events_consumed = []
|
|
453
|
+
|
|
454
|
+
try:
|
|
455
|
+
transactions = _build_transactions(class_fqn, model)
|
|
456
|
+
except Exception:
|
|
457
|
+
transactions = []
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
security_constraints = _build_security(class_fqn, raw_nodes, cir)
|
|
461
|
+
except Exception:
|
|
462
|
+
security_constraints = []
|
|
463
|
+
|
|
464
|
+
try:
|
|
465
|
+
rest_endpoints = _build_endpoints(class_fqn, model)
|
|
466
|
+
except Exception:
|
|
467
|
+
rest_endpoints = []
|
|
468
|
+
|
|
469
|
+
return ClassExplanation(
|
|
470
|
+
class_name=class_name,
|
|
471
|
+
class_fqn=class_fqn,
|
|
472
|
+
stereotype=stereotype,
|
|
473
|
+
purpose=purpose,
|
|
474
|
+
public_methods=public_methods,
|
|
475
|
+
incoming_callers=incoming_callers,
|
|
476
|
+
outgoing_deps=outgoing_deps,
|
|
477
|
+
events_published=events_published,
|
|
478
|
+
events_consumed=events_consumed,
|
|
479
|
+
transactions=transactions,
|
|
480
|
+
security_constraints=security_constraints,
|
|
481
|
+
rest_endpoints=rest_endpoints,
|
|
482
|
+
warnings=warnings,
|
|
483
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|