sourcecode 1.35.20__tar.gz → 1.35.23__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.20 → sourcecode-1.35.23}/PKG-INFO +3 -3
- {sourcecode-1.35.20 → sourcecode-1.35.23}/README.md +2 -2
- {sourcecode-1.35.20 → sourcecode-1.35.23}/pyproject.toml +1 -1
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/__init__.py +1 -1
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/canonical_ir.py +4 -4
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cli.py +9 -2
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/dependency_analyzer.py +72 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/server.py +119 -8
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/migrate_check.py +1 -1
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/prepare_context.py +21 -2
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/repository_ir.py +8 -1
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_security_audit.py +14 -7
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_tx_analyzer.py +13 -6
- {sourcecode-1.35.20 → sourcecode-1.35.23}/.github/workflows/build-windows.yml +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/.gitignore +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/.ruff.toml +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/CHANGELOG.md +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/CONTRIBUTING.md +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/LICENSE +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/SECURITY.md +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/raw +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/adaptive_scanner.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ast_extractor.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cache.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cir_graphs.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/classifier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/confidence_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/context_scorer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/context_summarizer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/contract_model.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/contract_pipeline.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/csproj_parser.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/hybrid.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/java.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/entrypoint_classifier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/env_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/error_schema.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/explain.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/file_classifier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/flow_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/fqn_utils.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/license.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/__init__.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/applier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/backup.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/detector.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/planner.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/orchestrator.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/registry.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/runner.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp_nudge.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/output_budget.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/path_filters.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/pr_comment_renderer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/pr_impact.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/progress.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ranking_engine.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/redactor.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/relevance_scorer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/repo_classifier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ris.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/runtime_classifier.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/scanner.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/schema.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/serializer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_event_topology.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_findings.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_impact.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_model.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_semantic.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/__init__.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/config.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/consent.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/events.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/filters.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/transport.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-1.35.20 → sourcecode-1.35.23}/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.23
|
|
4
4
|
Summary: Persistent structural context and ultra-fast repeated analysis for AI coding agents
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
Keywords: agents,ai,codebase,context,developer-tools,llm
|
|
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
|
|
|
40
40
|
|
|
41
41
|
**Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
|
|
42
42
|
|
|
43
|
-

|
|
44
44
|

|
|
45
45
|
|
|
46
46
|
---
|
|
@@ -114,7 +114,7 @@ pipx install sourcecode
|
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
116
|
sourcecode version
|
|
117
|
-
# sourcecode 1.35.
|
|
117
|
+
# sourcecode 1.35.23
|
|
118
118
|
```
|
|
119
119
|
|
|
120
120
|
---
|
|
@@ -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.23
|
|
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.23"
|
|
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"
|
|
@@ -244,11 +244,11 @@ def _route_to_canonical_endpoint(route: dict) -> CanonicalEndpoint:
|
|
|
244
244
|
security_dict = route.get("security_annotations")
|
|
245
245
|
security: Optional[CanonicalSecurity] = None
|
|
246
246
|
if security_dict:
|
|
247
|
-
# Determine source_scope from inheritance_depth:
|
|
248
|
-
# depth=0 → annotation on method or class (method takes precedence)
|
|
249
|
-
# depth>0 → inherited from parent class
|
|
250
247
|
depth = route.get("inheritance_depth") or 0
|
|
251
|
-
|
|
248
|
+
if depth > 0:
|
|
249
|
+
scope = "inherited"
|
|
250
|
+
else:
|
|
251
|
+
scope = security_dict.get("_scope", "method")
|
|
252
252
|
security = CanonicalSecurity.from_policy_dict(security_dict, source_scope=scope)
|
|
253
253
|
|
|
254
254
|
endpoint_id = CanonicalEndpoint.make_id(method, path, controller_class, handler_symbol)
|
|
@@ -3912,7 +3912,8 @@ def spring_audit_cmd(
|
|
|
3912
3912
|
)
|
|
3913
3913
|
raise typer.Exit(code=1)
|
|
3914
3914
|
|
|
3915
|
-
|
|
3915
|
+
_file_limitations: list[str] = []
|
|
3916
|
+
file_list = find_java_files(target, limitations=_file_limitations)
|
|
3916
3917
|
if not file_list:
|
|
3917
3918
|
empty_result = SpringAuditResult(
|
|
3918
3919
|
spring_detected=False,
|
|
@@ -3961,6 +3962,9 @@ def spring_audit_cmd(
|
|
|
3961
3962
|
metadata=merged_meta,
|
|
3962
3963
|
).finalize()
|
|
3963
3964
|
|
|
3965
|
+
if _file_limitations:
|
|
3966
|
+
combined.limitations.extend(_file_limitations)
|
|
3967
|
+
|
|
3964
3968
|
# Populate git_head from repo HEAD — non-fatal.
|
|
3965
3969
|
try:
|
|
3966
3970
|
import subprocess as _sub_sa
|
|
@@ -4088,8 +4092,11 @@ def migrate_check_cmd(
|
|
|
4088
4092
|
)
|
|
4089
4093
|
raise typer.Exit(code=1)
|
|
4090
4094
|
|
|
4091
|
-
|
|
4095
|
+
_file_limitations: list[str] = []
|
|
4096
|
+
file_list = find_java_files(target, limitations=_file_limitations)
|
|
4092
4097
|
report = run_migrate_check(file_list, target, min_severity=min_severity)
|
|
4098
|
+
if _file_limitations:
|
|
4099
|
+
report.limitations.extend(_file_limitations)
|
|
4093
4100
|
|
|
4094
4101
|
if format == "text":
|
|
4095
4102
|
output = report.to_text(min_severity=min_severity)
|
|
@@ -1142,6 +1142,78 @@ class DependencyAnalyzer:
|
|
|
1142
1142
|
records: list[DependencyRecord] = []
|
|
1143
1143
|
deps_elem = root_elem.find(f"{ns}dependencies")
|
|
1144
1144
|
if deps_elem is None:
|
|
1145
|
+
# Multi-module aggregator POM: scan up to 5 declared submodule pom.xml files.
|
|
1146
|
+
modules_elem = root_elem.find(f"{ns}modules")
|
|
1147
|
+
if modules_elem is not None:
|
|
1148
|
+
submodule_records: list[DependencyRecord] = []
|
|
1149
|
+
submodule_limitations: list[str] = []
|
|
1150
|
+
seen_submodule_deps: set[str] = set()
|
|
1151
|
+
_all_modules = list(modules_elem.findall(f"{ns}module"))
|
|
1152
|
+
if len(_all_modules) > 5:
|
|
1153
|
+
submodule_limitations.append(
|
|
1154
|
+
f"MAX_SUBMODULES_REACHED: scanned 5 of {len(_all_modules)} declared Maven submodules"
|
|
1155
|
+
)
|
|
1156
|
+
for mod_elem in _all_modules[:5]:
|
|
1157
|
+
mod_name = (mod_elem.text or "").strip()
|
|
1158
|
+
if not mod_name:
|
|
1159
|
+
continue
|
|
1160
|
+
sub_pom = root / mod_name / "pom.xml"
|
|
1161
|
+
if not sub_pom.exists():
|
|
1162
|
+
continue
|
|
1163
|
+
try:
|
|
1164
|
+
sub_tree = ET.parse(sub_pom)
|
|
1165
|
+
except (ET.ParseError, OSError):
|
|
1166
|
+
continue
|
|
1167
|
+
sub_root = sub_tree.getroot()
|
|
1168
|
+
sub_ns_match = re.match(r"\{[^}]+\}", sub_root.tag)
|
|
1169
|
+
sub_ns = sub_ns_match.group(0) if sub_ns_match else ""
|
|
1170
|
+
sub_props = self._parse_maven_properties(sub_root, sub_ns)
|
|
1171
|
+
sub_dm = self._parse_dependency_management(sub_root, sub_ns, sub_props)
|
|
1172
|
+
# Inherit parent properties for version resolution
|
|
1173
|
+
for k, v in properties.items():
|
|
1174
|
+
sub_props.setdefault(k, v)
|
|
1175
|
+
for k, v in dm_versions.items():
|
|
1176
|
+
sub_dm.setdefault(k, v)
|
|
1177
|
+
sub_deps_elem = sub_root.find(f"{sub_ns}dependencies")
|
|
1178
|
+
if sub_deps_elem is None:
|
|
1179
|
+
continue
|
|
1180
|
+
for dep in sub_deps_elem.findall(f"{sub_ns}dependency"):
|
|
1181
|
+
gid = (dep.findtext(f"{sub_ns}groupId") or "").strip()
|
|
1182
|
+
aid = (dep.findtext(f"{sub_ns}artifactId") or "").strip()
|
|
1183
|
+
if not gid or not aid:
|
|
1184
|
+
continue
|
|
1185
|
+
dep_key = f"{gid}:{aid}"
|
|
1186
|
+
if dep_key in seen_submodule_deps:
|
|
1187
|
+
continue
|
|
1188
|
+
seen_submodule_deps.add(dep_key)
|
|
1189
|
+
ver_raw = (dep.findtext(f"{sub_ns}version") or "").strip() or None
|
|
1190
|
+
declared = self._resolve_maven_version(ver_raw, sub_props)
|
|
1191
|
+
if declared is None:
|
|
1192
|
+
declared = sub_dm.get(dep_key)
|
|
1193
|
+
scope_text = (dep.findtext(f"{sub_ns}scope") or "compile").strip().lower()
|
|
1194
|
+
if scope_text == "test":
|
|
1195
|
+
scope = "dev"
|
|
1196
|
+
elif scope_text == "provided":
|
|
1197
|
+
scope = "provided"
|
|
1198
|
+
else:
|
|
1199
|
+
scope = "direct"
|
|
1200
|
+
resolved_version = None
|
|
1201
|
+
if declared is None and parent_version:
|
|
1202
|
+
if gid == "org.springframework.boot":
|
|
1203
|
+
resolved_version = parent_version
|
|
1204
|
+
elif gid == "org.springframework.security" and "spring-security.version" in sub_props:
|
|
1205
|
+
resolved_version = sub_props["spring-security.version"]
|
|
1206
|
+
submodule_records.append(DependencyRecord(
|
|
1207
|
+
name=dep_key,
|
|
1208
|
+
ecosystem="java",
|
|
1209
|
+
scope=scope,
|
|
1210
|
+
declared_version=declared,
|
|
1211
|
+
resolved_version=resolved_version,
|
|
1212
|
+
source="manifest",
|
|
1213
|
+
manifest_path=f"{mod_name}/pom.xml",
|
|
1214
|
+
))
|
|
1215
|
+
if submodule_records:
|
|
1216
|
+
return submodule_records, submodule_limitations
|
|
1145
1217
|
return [], ["java: pom.xml has no <dependencies> block"]
|
|
1146
1218
|
|
|
1147
1219
|
for dep in deps_elem.findall(f"{ns}dependency"):
|
|
@@ -192,7 +192,48 @@ def _execute(args: list[str]) -> dict | CallToolResult:
|
|
|
192
192
|
return _ok(result)
|
|
193
193
|
|
|
194
194
|
|
|
195
|
+
# Per-tool character budgets (4 chars ≈ 1 token; keeps Cursor under its 10k token limit).
|
|
196
|
+
# List fields are trimmed front-to-back until output fits.
|
|
197
|
+
_MCP_CHAR_BUDGETS: dict[str, int] = {
|
|
198
|
+
"get_agent_context": 38_000,
|
|
199
|
+
"get_spring_audit": 38_000,
|
|
200
|
+
"get_migration_readiness": 38_000,
|
|
201
|
+
}
|
|
202
|
+
_MCP_TRIM_FIELDS = (
|
|
203
|
+
"findings", "relevant_files", "key_dependencies",
|
|
204
|
+
"entry_points", "limitations", "gaps", "code_notes",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _cap_mcp_output(tool_name: str, data: Any) -> Any:
|
|
209
|
+
"""Trim list fields in data until JSON serialisation fits within the tool's char budget."""
|
|
210
|
+
budget = _MCP_CHAR_BUDGETS.get(tool_name)
|
|
211
|
+
if not budget or not isinstance(data, dict):
|
|
212
|
+
return data
|
|
213
|
+
if len(json.dumps(data, default=str)) <= budget:
|
|
214
|
+
return data
|
|
215
|
+
result = dict(data)
|
|
216
|
+
for field in _MCP_TRIM_FIELDS:
|
|
217
|
+
if field not in result or not isinstance(result[field], list):
|
|
218
|
+
continue
|
|
219
|
+
items = list(result[field])
|
|
220
|
+
while items and len(json.dumps(result, default=str)) > budget:
|
|
221
|
+
items = items[:-1]
|
|
222
|
+
result[field] = items
|
|
223
|
+
if len(json.dumps(result, default=str)) <= budget:
|
|
224
|
+
break
|
|
225
|
+
if len(json.dumps(result, default=str)) > budget:
|
|
226
|
+
result["_mcp_truncated"] = True
|
|
227
|
+
result["_mcp_truncation_note"] = (
|
|
228
|
+
f"Output capped at ~{budget // 4}k tokens for MCP compatibility. "
|
|
229
|
+
"Use CLI directly for full output."
|
|
230
|
+
)
|
|
231
|
+
return result
|
|
232
|
+
|
|
233
|
+
|
|
195
234
|
_DEFAULT_TESTS_TIMEOUT_MS = 15_000
|
|
235
|
+
_DEFAULT_SPRING_AUDIT_TIMEOUT_MS = 120_000 # 2 min
|
|
236
|
+
_DEFAULT_IMPACT_TIMEOUT_MS = 60_000 # 1 min
|
|
196
237
|
|
|
197
238
|
# Regex for MINGW paths: /c/some/path → C:/some/path
|
|
198
239
|
_MINGW_PATH_RE = re.compile(r"^/([a-zA-Z])(/.*)?$")
|
|
@@ -568,7 +609,10 @@ def get_agent_context(repo_path: str = ".", git_context: bool = False) -> dict:
|
|
|
568
609
|
args = [repo_path, "--agent"]
|
|
569
610
|
if git_context:
|
|
570
611
|
args.append("--git-context")
|
|
571
|
-
|
|
612
|
+
result = _execute(args)
|
|
613
|
+
if isinstance(result, dict) and result.get("success"):
|
|
614
|
+
result = _ok(_cap_mcp_output("get_agent_context", result.get("data")))
|
|
615
|
+
return result
|
|
572
616
|
except Exception as exc:
|
|
573
617
|
return _err(
|
|
574
618
|
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
@@ -641,7 +685,26 @@ def get_spring_audit(repo_path: str = ".", scope: str = "all") -> dict:
|
|
|
641
685
|
_path_err = _check_repo_path(repo_path)
|
|
642
686
|
if _path_err is not None:
|
|
643
687
|
return _path_err
|
|
644
|
-
|
|
688
|
+
timeout_ms = int(os.environ.get("SOURCECODE_SPRING_AUDIT_TIMEOUT_MS", str(_DEFAULT_SPRING_AUDIT_TIMEOUT_MS)))
|
|
689
|
+
timeout_s = timeout_ms / 1000.0
|
|
690
|
+
_exec = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
691
|
+
try:
|
|
692
|
+
_fut = _exec.submit(_execute, ["spring-audit", repo_path, "--scope", scope])
|
|
693
|
+
_done, _pending = concurrent.futures.wait([_fut], timeout=timeout_s)
|
|
694
|
+
if _pending:
|
|
695
|
+
_exec.shutdown(wait=False)
|
|
696
|
+
return _ok({
|
|
697
|
+
"truncated": True,
|
|
698
|
+
"truncated_reason": f"timeout_{timeout_s:.0f}s",
|
|
699
|
+
"analysis": "timed out — repository may be too large for MCP transport",
|
|
700
|
+
"suggestion": f"Increase SOURCECODE_SPRING_AUDIT_TIMEOUT_MS (current: {timeout_ms}ms) or run via CLI directly",
|
|
701
|
+
})
|
|
702
|
+
result = _fut.result()
|
|
703
|
+
finally:
|
|
704
|
+
_exec.shutdown(wait=True)
|
|
705
|
+
if isinstance(result, dict) and result.get("success"):
|
|
706
|
+
result = _ok(_cap_mcp_output("get_spring_audit", result.get("data")))
|
|
707
|
+
return result
|
|
645
708
|
except Exception as exc:
|
|
646
709
|
return _err(
|
|
647
710
|
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
@@ -691,7 +754,10 @@ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") ->
|
|
|
691
754
|
_path_err = _check_repo_path(repo_path)
|
|
692
755
|
if _path_err is not None:
|
|
693
756
|
return _path_err
|
|
694
|
-
|
|
757
|
+
result = _execute(["migrate-check", repo_path, "--min-severity", min_severity])
|
|
758
|
+
if isinstance(result, dict) and result.get("success"):
|
|
759
|
+
result = _ok(_cap_mcp_output("get_migration_readiness", result.get("data")))
|
|
760
|
+
return result
|
|
695
761
|
except Exception as exc:
|
|
696
762
|
return _err(
|
|
697
763
|
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
@@ -731,7 +797,23 @@ def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) ->
|
|
|
731
797
|
if _path_err is not None:
|
|
732
798
|
return _path_err
|
|
733
799
|
args = ["impact-chain", symbol.strip(), repo_path, "--depth", str(depth)]
|
|
734
|
-
|
|
800
|
+
timeout_ms = int(os.environ.get("SOURCECODE_IMPACT_TIMEOUT_MS", str(_DEFAULT_IMPACT_TIMEOUT_MS)))
|
|
801
|
+
timeout_s = timeout_ms / 1000.0
|
|
802
|
+
_exec = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
803
|
+
try:
|
|
804
|
+
_fut = _exec.submit(_execute, args)
|
|
805
|
+
_done, _pending = concurrent.futures.wait([_fut], timeout=timeout_s)
|
|
806
|
+
if _pending:
|
|
807
|
+
_exec.shutdown(wait=False)
|
|
808
|
+
return _ok({
|
|
809
|
+
"truncated": True,
|
|
810
|
+
"truncated_reason": f"timeout_{timeout_s:.0f}s",
|
|
811
|
+
"analysis": "timed out — repository may be too large for MCP transport",
|
|
812
|
+
"suggestion": f"Increase SOURCECODE_IMPACT_TIMEOUT_MS (current: {timeout_ms}ms) or run via CLI directly",
|
|
813
|
+
})
|
|
814
|
+
return _fut.result()
|
|
815
|
+
finally:
|
|
816
|
+
_exec.shutdown(wait=True)
|
|
735
817
|
except Exception as exc:
|
|
736
818
|
return _err(
|
|
737
819
|
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
@@ -1148,7 +1230,23 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
|
|
|
1148
1230
|
if _path_err is not None:
|
|
1149
1231
|
return _path_err
|
|
1150
1232
|
args = ["impact", target.strip(), repo_path, "--depth", str(depth)]
|
|
1151
|
-
|
|
1233
|
+
timeout_ms = int(os.environ.get("SOURCECODE_IMPACT_TIMEOUT_MS", str(_DEFAULT_IMPACT_TIMEOUT_MS)))
|
|
1234
|
+
timeout_s = timeout_ms / 1000.0
|
|
1235
|
+
_exec = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
|
1236
|
+
try:
|
|
1237
|
+
_fut = _exec.submit(_execute, args)
|
|
1238
|
+
_done, _pending = concurrent.futures.wait([_fut], timeout=timeout_s)
|
|
1239
|
+
if _pending:
|
|
1240
|
+
_exec.shutdown(wait=False)
|
|
1241
|
+
return _ok({
|
|
1242
|
+
"truncated": True,
|
|
1243
|
+
"truncated_reason": f"timeout_{timeout_s:.0f}s",
|
|
1244
|
+
"analysis": "timed out — repository may be too large for MCP transport",
|
|
1245
|
+
"suggestion": f"Increase SOURCECODE_IMPACT_TIMEOUT_MS (current: {timeout_ms}ms) or run via CLI directly",
|
|
1246
|
+
})
|
|
1247
|
+
return _fut.result()
|
|
1248
|
+
finally:
|
|
1249
|
+
_exec.shutdown(wait=True)
|
|
1152
1250
|
except Exception as exc:
|
|
1153
1251
|
return _err(
|
|
1154
1252
|
f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
|
|
@@ -1269,9 +1367,22 @@ def _finalize_mcp_registry() -> None:
|
|
|
1269
1367
|
structured_output=False,
|
|
1270
1368
|
)
|
|
1271
1369
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1370
|
+
try:
|
|
1371
|
+
drift = validate_registry()
|
|
1372
|
+
if drift:
|
|
1373
|
+
import warnings
|
|
1374
|
+
warnings.warn(
|
|
1375
|
+
f"MCP registry drift detected — server running with potential tool mismatch: {drift}",
|
|
1376
|
+
RuntimeWarning,
|
|
1377
|
+
stacklevel=2,
|
|
1378
|
+
)
|
|
1379
|
+
except Exception as _reg_exc:
|
|
1380
|
+
import warnings
|
|
1381
|
+
warnings.warn(
|
|
1382
|
+
f"MCP registry validation failed — server starting in degraded mode: {_reg_exc}",
|
|
1383
|
+
RuntimeWarning,
|
|
1384
|
+
stacklevel=2,
|
|
1385
|
+
)
|
|
1275
1386
|
|
|
1276
1387
|
|
|
1277
1388
|
_finalize_mcp_registry()
|
|
@@ -422,7 +422,7 @@ def run_migrate_check(
|
|
|
422
422
|
|
|
423
423
|
def _detect_spring_boot_2(root: Path) -> bool:
|
|
424
424
|
"""Return True if any pom.xml or build.gradle declares spring-boot 2.x."""
|
|
425
|
-
_SB2 = re.compile(r"spring
|
|
425
|
+
_SB2 = re.compile(r"spring[-.]boot[^\"'\n]*[\"']?2\.\d+", re.IGNORECASE)
|
|
426
426
|
for name in ("pom.xml", "build.gradle", "build.gradle.kts"):
|
|
427
427
|
candidate = root / name
|
|
428
428
|
try:
|
|
@@ -1034,6 +1034,12 @@ class TaskContextBuilder:
|
|
|
1034
1034
|
if g
|
|
1035
1035
|
]
|
|
1036
1036
|
|
|
1037
|
+
_ris_key_deps = compact.get("key_dependencies") or []
|
|
1038
|
+
# Fall through to full analysis if deps expected but RIS has none.
|
|
1039
|
+
# RIS may have been built before dependency analysis was run on this repo.
|
|
1040
|
+
if spec.enable_dependencies and not _ris_key_deps:
|
|
1041
|
+
return None
|
|
1042
|
+
|
|
1037
1043
|
return TaskOutput(
|
|
1038
1044
|
task=task_name,
|
|
1039
1045
|
goal=spec.goal,
|
|
@@ -1043,7 +1049,7 @@ class TaskContextBuilder:
|
|
|
1043
1049
|
suspected_areas=[],
|
|
1044
1050
|
improvement_opportunities=[],
|
|
1045
1051
|
test_gaps=[],
|
|
1046
|
-
key_dependencies=
|
|
1052
|
+
key_dependencies=_ris_key_deps,
|
|
1047
1053
|
code_notes_summary=None,
|
|
1048
1054
|
limitations=[],
|
|
1049
1055
|
confidence=compact.get("confidence") or compact.get("confidence_summary") or "high",
|
|
@@ -1303,7 +1309,20 @@ class TaskContextBuilder:
|
|
|
1303
1309
|
from dataclasses import asdict
|
|
1304
1310
|
from sourcecode.dependency_analyzer import DependencyAnalyzer
|
|
1305
1311
|
|
|
1306
|
-
|
|
1312
|
+
_dep_analyzer = DependencyAnalyzer()
|
|
1313
|
+
dep_records, dep_summary = _dep_analyzer.analyze(self.root)
|
|
1314
|
+
# For multi-module repos (Maven/Gradle), root pom.xml is a parent POM
|
|
1315
|
+
# with few deps. Per-workspace analysis finds the actual module deps.
|
|
1316
|
+
if workspace_analysis.workspaces:
|
|
1317
|
+
for _ws in workspace_analysis.workspaces:
|
|
1318
|
+
_ws_root = self.root / _ws.path
|
|
1319
|
+
if _ws_root.exists() and _ws_root.is_dir():
|
|
1320
|
+
try:
|
|
1321
|
+
_ws_deps, _ws_summary = _dep_analyzer.analyze(_ws_root)
|
|
1322
|
+
dep_records = dep_records + _ws_deps
|
|
1323
|
+
dep_summary.limitations.extend(_ws_summary.limitations)
|
|
1324
|
+
except Exception:
|
|
1325
|
+
pass
|
|
1307
1326
|
primary_eco = stacks[0].stack if stacks else ""
|
|
1308
1327
|
_direct_raw = [
|
|
1309
1328
|
d for d in dep_records
|
|
@@ -2543,6 +2543,7 @@ def _route_security_from_sym(
|
|
|
2543
2543
|
for candidate in filter(None, [method_sym, class_sym]):
|
|
2544
2544
|
result = _extract_from(candidate)
|
|
2545
2545
|
if result is not None:
|
|
2546
|
+
result["_scope"] = "class" if candidate is class_sym else "method"
|
|
2546
2547
|
return result
|
|
2547
2548
|
return None
|
|
2548
2549
|
|
|
@@ -3312,11 +3313,13 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3312
3313
|
}
|
|
3313
3314
|
|
|
3314
3315
|
|
|
3315
|
-
def find_java_files(root: Path, *, max_files: int = 8000) -> list[str]:
|
|
3316
|
+
def find_java_files(root: Path, *, max_files: int = 8000, limitations: list[str] | None = None) -> list[str]:
|
|
3316
3317
|
"""Return relative paths to Java files under root, excluding test dirs and vendor."""
|
|
3317
3318
|
results: list[str] = []
|
|
3319
|
+
_capped = False
|
|
3318
3320
|
for p in sorted(root.rglob("*.java")):
|
|
3319
3321
|
if len(results) >= max_files:
|
|
3322
|
+
_capped = True
|
|
3320
3323
|
break
|
|
3321
3324
|
try:
|
|
3322
3325
|
rel = str(p.relative_to(root)).replace("\\", "/")
|
|
@@ -3334,6 +3337,10 @@ def find_java_files(root: Path, *, max_files: int = 8000) -> list[str]:
|
|
|
3334
3337
|
if any(f in rel for f in ("/admin-client/", "/rest-client/", "/client-api/", "/api-client/")):
|
|
3335
3338
|
continue
|
|
3336
3339
|
results.append(rel)
|
|
3340
|
+
if _capped and limitations is not None:
|
|
3341
|
+
limitations.append(
|
|
3342
|
+
f"MAX_JAVA_FILES_REACHED: scanned {max_files} files — repository likely has more"
|
|
3343
|
+
)
|
|
3337
3344
|
return results
|
|
3338
3345
|
|
|
3339
3346
|
|
|
@@ -418,12 +418,15 @@ class SecurityScanner:
|
|
|
418
418
|
model: Optional[SpringSemanticModel] = None,
|
|
419
419
|
) -> list[SpringFinding]:
|
|
420
420
|
all_findings: list[SpringFinding] = []
|
|
421
|
+
self._last_analysis_errors: list[str] = []
|
|
421
422
|
for pattern in self.patterns:
|
|
422
423
|
try:
|
|
423
424
|
found = _call_pattern_analyze(pattern, cir, tx_index, root, model)
|
|
424
425
|
all_findings.extend(found)
|
|
425
|
-
except Exception:
|
|
426
|
-
|
|
426
|
+
except Exception as exc:
|
|
427
|
+
self._last_analysis_errors.append(
|
|
428
|
+
f"{pattern.pattern_id}: {type(exc).__name__}: {exc}"
|
|
429
|
+
)
|
|
427
430
|
deduped = deduplicate_findings(all_findings)
|
|
428
431
|
return sorted(deduped, key=lambda f: (SEVERITY_ORDER.get(f.severity, 9), f.symbol))
|
|
429
432
|
|
|
@@ -475,16 +478,20 @@ def run_security_audit(
|
|
|
475
478
|
or cir.metadata.get("security_model", "unknown") != "unknown"
|
|
476
479
|
)
|
|
477
480
|
|
|
481
|
+
_sec_limitations = [
|
|
482
|
+
"SEC-001: only emitted for annotation_based security model",
|
|
483
|
+
"SEC-002: generic type detection is regex-based on extends edge signatures",
|
|
484
|
+
"SEC-003: only detects controllers visible via cir.endpoints",
|
|
485
|
+
]
|
|
486
|
+
for _err in getattr(scanner, "_last_analysis_errors", []):
|
|
487
|
+
_sec_limitations.append(f"PATTERN_ERROR: {_err}")
|
|
488
|
+
|
|
478
489
|
result = SpringAuditResult(
|
|
479
490
|
repo_id=getattr(cir, "cir_hash", "")[:16],
|
|
480
491
|
spring_detected=_spring_detected,
|
|
481
492
|
scope="security",
|
|
482
493
|
findings=findings,
|
|
483
|
-
limitations=
|
|
484
|
-
"SEC-001: only emitted for annotation_based security model",
|
|
485
|
-
"SEC-002: generic type detection is regex-based on extends edge signatures",
|
|
486
|
-
"SEC-003: only detects controllers visible via cir.endpoints",
|
|
487
|
-
],
|
|
494
|
+
limitations=_sec_limitations,
|
|
488
495
|
metadata={
|
|
489
496
|
"endpoints_analyzed": len(cir.endpoints),
|
|
490
497
|
"security_model": cir.metadata.get("security_model", "unknown"),
|
|
@@ -672,12 +672,15 @@ class TxPatternEngine:
|
|
|
672
672
|
model: Optional[SpringSemanticModel] = None,
|
|
673
673
|
) -> list[SpringFinding]:
|
|
674
674
|
all_findings: list[SpringFinding] = []
|
|
675
|
+
self._last_analysis_errors: list[str] = []
|
|
675
676
|
for pattern in self.patterns:
|
|
676
677
|
try:
|
|
677
678
|
found = _call_pattern_analyze(pattern, cir, tx_index, root, model)
|
|
678
679
|
all_findings.extend(found)
|
|
679
|
-
except Exception:
|
|
680
|
-
|
|
680
|
+
except Exception as exc:
|
|
681
|
+
self._last_analysis_errors.append(
|
|
682
|
+
f"{pattern.pattern_id}: {type(exc).__name__}: {exc}"
|
|
683
|
+
)
|
|
681
684
|
deduped = deduplicate_findings(all_findings)
|
|
682
685
|
return sorted(deduped, key=lambda f: (SEVERITY_ORDER.get(f.severity, 9), f.symbol))
|
|
683
686
|
|
|
@@ -721,15 +724,19 @@ def run_tx_audit(
|
|
|
721
724
|
|
|
722
725
|
_spring_detected = tx_index.stats()["total"] > 0 or bool(model.bean_graph.beans)
|
|
723
726
|
|
|
727
|
+
_tx_limitations = [
|
|
728
|
+
"Self-invocation via this.method() not detected — requires AST-level analysis",
|
|
729
|
+
"Dynamic dispatch (interface/polymorphic calls) may produce incomplete call chains",
|
|
730
|
+
]
|
|
731
|
+
for _err in getattr(engine, "_last_analysis_errors", []):
|
|
732
|
+
_tx_limitations.append(f"PATTERN_ERROR: {_err}")
|
|
733
|
+
|
|
724
734
|
result = SpringAuditResult(
|
|
725
735
|
repo_id=getattr(cir, "cir_hash", "")[:16],
|
|
726
736
|
spring_detected=_spring_detected,
|
|
727
737
|
scope="tx",
|
|
728
738
|
findings=findings,
|
|
729
|
-
limitations=
|
|
730
|
-
"Self-invocation via this.method() not detected — requires AST-level analysis",
|
|
731
|
-
"Dynamic dispatch (interface/polymorphic calls) may produce incomplete call chains",
|
|
732
|
-
],
|
|
739
|
+
limitations=_tx_limitations,
|
|
733
740
|
metadata={
|
|
734
741
|
"symbols_analyzed": len(getattr(cir, "symbols", [])),
|
|
735
742
|
"tx_boundaries_found": tx_index.stats()["total"],
|
|
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
|