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.
Files changed (107) hide show
  1. {sourcecode-1.35.20 → sourcecode-1.35.23}/PKG-INFO +3 -3
  2. {sourcecode-1.35.20 → sourcecode-1.35.23}/README.md +2 -2
  3. {sourcecode-1.35.20 → sourcecode-1.35.23}/pyproject.toml +1 -1
  4. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/canonical_ir.py +4 -4
  6. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cli.py +9 -2
  7. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/dependency_analyzer.py +72 -0
  8. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/server.py +119 -8
  9. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/migrate_check.py +1 -1
  10. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/prepare_context.py +21 -2
  11. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/repository_ir.py +8 -1
  12. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_security_audit.py +14 -7
  13. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_tx_analyzer.py +13 -6
  14. {sourcecode-1.35.20 → sourcecode-1.35.23}/.github/workflows/build-windows.yml +0 -0
  15. {sourcecode-1.35.20 → sourcecode-1.35.23}/.gitignore +0 -0
  16. {sourcecode-1.35.20 → sourcecode-1.35.23}/.ruff.toml +0 -0
  17. {sourcecode-1.35.20 → sourcecode-1.35.23}/CHANGELOG.md +0 -0
  18. {sourcecode-1.35.20 → sourcecode-1.35.23}/CONTRIBUTING.md +0 -0
  19. {sourcecode-1.35.20 → sourcecode-1.35.23}/LICENSE +0 -0
  20. {sourcecode-1.35.20 → sourcecode-1.35.23}/SECURITY.md +0 -0
  21. {sourcecode-1.35.20 → sourcecode-1.35.23}/raw +0 -0
  22. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/adaptive_scanner.py +0 -0
  23. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/architecture_analyzer.py +0 -0
  24. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/architecture_summary.py +0 -0
  25. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ast_extractor.py +0 -0
  26. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cache.py +0 -0
  27. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/cir_graphs.py +0 -0
  28. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/classifier.py +0 -0
  29. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/code_notes_analyzer.py +0 -0
  30. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/confidence_analyzer.py +0 -0
  31. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/context_scorer.py +0 -0
  32. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/context_summarizer.py +0 -0
  33. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/contract_model.py +0 -0
  34. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/contract_pipeline.py +0 -0
  35. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/coverage_parser.py +0 -0
  36. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/__init__.py +0 -0
  37. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/base.py +0 -0
  38. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/csproj_parser.py +0 -0
  39. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/dart.py +0 -0
  40. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/dotnet.py +0 -0
  41. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/elixir.py +0 -0
  42. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/go.py +0 -0
  43. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/heuristic.py +0 -0
  44. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/hybrid.py +0 -0
  45. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/java.py +0 -0
  46. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/jvm_ext.py +0 -0
  47. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/nodejs.py +0 -0
  48. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/parsers.py +0 -0
  49. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/php.py +0 -0
  50. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/project.py +0 -0
  51. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/python.py +0 -0
  52. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/ruby.py +0 -0
  53. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/rust.py +0 -0
  54. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/systems.py +0 -0
  55. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/terraform.py +0 -0
  56. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/detectors/tooling.py +0 -0
  57. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/doc_analyzer.py +0 -0
  58. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/entrypoint_classifier.py +0 -0
  59. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/env_analyzer.py +0 -0
  60. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/error_schema.py +0 -0
  61. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/explain.py +0 -0
  62. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/file_classifier.py +0 -0
  63. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/flow_analyzer.py +0 -0
  64. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/fqn_utils.py +0 -0
  65. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/git_analyzer.py +0 -0
  66. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/graph_analyzer.py +0 -0
  67. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/license.py +0 -0
  68. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/__init__.py +0 -0
  69. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  70. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  71. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  72. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  73. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  74. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/orchestrator.py +0 -0
  75. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/registry.py +0 -0
  76. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp/runner.py +0 -0
  77. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/mcp_nudge.py +0 -0
  78. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/metrics_analyzer.py +0 -0
  79. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/output_budget.py +0 -0
  80. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/path_filters.py +0 -0
  81. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/pr_comment_renderer.py +0 -0
  82. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/pr_impact.py +0 -0
  83. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/progress.py +0 -0
  84. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ranking_engine.py +0 -0
  85. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/redactor.py +0 -0
  86. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/relevance_scorer.py +0 -0
  87. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/repo_classifier.py +0 -0
  88. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/ris.py +0 -0
  89. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/runtime_classifier.py +0 -0
  90. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/scanner.py +0 -0
  91. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/schema.py +0 -0
  92. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/semantic_analyzer.py +0 -0
  93. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/serializer.py +0 -0
  94. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_event_topology.py +0 -0
  95. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_findings.py +0 -0
  96. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_impact.py +0 -0
  97. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_model.py +0 -0
  98. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/spring_semantic.py +0 -0
  99. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/summarizer.py +0 -0
  100. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/__init__.py +0 -0
  101. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/config.py +0 -0
  102. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/consent.py +0 -0
  103. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/events.py +0 -0
  104. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/filters.py +0 -0
  105. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/telemetry/transport.py +0 -0
  106. {sourcecode-1.35.20 → sourcecode-1.35.23}/src/sourcecode/tree_utils.py +0 -0
  107. {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.20
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
- ![Version](https://img.shields.io/badge/version-1.35.20-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.23-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.20
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
- ![Version](https://img.shields.io/badge/version-1.35.20-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.35.23-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.35.20
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.20"
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"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.20"
3
+ __version__ = "1.35.23"
@@ -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
- scope = "inherited" if depth > 0 else "method"
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
- file_list = find_java_files(target)
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
- file_list = find_java_files(target)
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
- return _execute(args)
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
- return _execute(["spring-audit", repo_path, "--scope", scope])
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
- return _execute(["migrate-check", repo_path, "--min-severity", min_severity])
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
- return _execute(args)
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
- return _execute(args)
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
- drift = validate_registry()
1273
- if drift:
1274
- raise RuntimeError(f"MCP registry drift detected: {drift}")
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.boot[^\"'\n]*[\"']?2\.\d+", re.IGNORECASE)
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=compact.get("key_dependencies") or [],
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
- dep_records, dep_summary = DependencyAnalyzer().analyze(self.root)
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
- pass
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
- pass
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