sourcecode 1.35.19__py3-none-any.whl → 1.35.22__py3-none-any.whl

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/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.19"
3
+ __version__ = "1.35.22"
sourcecode/cli.py CHANGED
@@ -3722,6 +3722,80 @@ def endpoints_cmd(
3722
3722
 
3723
3723
  # ── Spring Semantic Audit ─────────────────────────────────────────────────────
3724
3724
 
3725
+
3726
+ def _render_spring_audit_github_comment(result: "SpringAuditResult", min_severity: str = "low") -> str: # type: ignore[name-defined]
3727
+ """Render SpringAuditResult as a GitHub PR comment in Markdown."""
3728
+ from sourcecode.spring_findings import SEVERITY_ORDER
3729
+
3730
+ min_order = SEVERITY_ORDER.get(min_severity, 3)
3731
+ visible = [f for f in result.findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
3732
+
3733
+ sev = result.summary.get("by_severity", {})
3734
+ total = result.summary.get("total_findings", 0)
3735
+ blocking = sev.get("critical", 0) + sev.get("high", 0)
3736
+
3737
+ _ICONS = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🔵"}
3738
+ _LABELS = {"critical": "CRITICAL", "high": "HIGH", "medium": "MEDIUM", "low": "LOW"}
3739
+
3740
+ if total == 0:
3741
+ status_line = "✅ **Spring Audit — no findings**"
3742
+ elif blocking > 0:
3743
+ status_line = f"🔴 **Spring Audit — {total} finding{'s' if total != 1 else ''} ({blocking} blocking)**"
3744
+ else:
3745
+ status_line = f"🟡 **Spring Audit — {total} finding{'s' if total != 1 else ''} (0 blocking)**"
3746
+
3747
+ lines: list[str] = [status_line, ""]
3748
+
3749
+ if total > 0:
3750
+ severity_counts = []
3751
+ for sev_name in ("critical", "high", "medium", "low"):
3752
+ n = sev.get(sev_name, 0)
3753
+ if n:
3754
+ severity_counts.append(f"{_ICONS[sev_name]} {n} {sev_name}")
3755
+ lines.append("**Severity:** " + " · ".join(severity_counts))
3756
+ lines.append("")
3757
+
3758
+ if not visible:
3759
+ lines.append(f"_No findings at or above `{min_severity}` severity._")
3760
+ return "\n".join(lines)
3761
+
3762
+ lines += [
3763
+ "| Sev | Pattern | File | Symbol | Title |",
3764
+ "|-----|---------|------|--------|-------|",
3765
+ ]
3766
+ for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
3767
+ icon = _ICONS.get(f.severity, "")
3768
+ label = _LABELS.get(f.severity, f.severity.upper())
3769
+ short_file = f.source_file.split("/")[-1] if "/" in f.source_file else f.source_file
3770
+ short_sym = f.symbol.split(".")[-1] if "." in f.symbol else f.symbol
3771
+ title_escaped = f.title.replace("|", "\\|")
3772
+ lines.append(f"| {icon} {label} | `{f.pattern_id}` | `{short_file}` | `{short_sym}` | {title_escaped} |")
3773
+
3774
+ lines.append("")
3775
+
3776
+ if visible:
3777
+ lines.append("<details>")
3778
+ lines.append("<summary>Finding details</summary>")
3779
+ lines.append("")
3780
+ for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
3781
+ icon = _ICONS.get(f.severity, "")
3782
+ lines.append(f"### {icon} `{f.pattern_id}` — {f.title}")
3783
+ lines.append(f"**File:** `{f.source_file}` **Symbol:** `{f.symbol}`")
3784
+ lines.append("")
3785
+ lines.append(f.explanation)
3786
+ lines.append("")
3787
+ lines.append(f"**Fix:** {f.fix_hint}")
3788
+ lines.append("")
3789
+ lines.append("</details>")
3790
+
3791
+ lines += [
3792
+ "",
3793
+ f"_Generated by [sourcecode](https://github.com/sourcecode-ai/sourcecode) · "
3794
+ f"scope: {result.scope} · min-severity: {min_severity}_",
3795
+ ]
3796
+ return "\n".join(lines)
3797
+
3798
+
3725
3799
  @app.command("spring-audit")
3726
3800
  def spring_audit_cmd(
3727
3801
  path: Path = typer.Argument(
@@ -3736,7 +3810,7 @@ def spring_audit_cmd(
3736
3810
  "json",
3737
3811
  "--format",
3738
3812
  "-f",
3739
- help="Output format: json (default) or yaml.",
3813
+ help="Output format: json (default), yaml, or github-comment.",
3740
3814
  show_default=True,
3741
3815
  ),
3742
3816
  copy: bool = typer.Option(
@@ -3758,6 +3832,11 @@ def spring_audit_cmd(
3758
3832
  help="Minimum severity to include: critical, high, medium, or low (default).",
3759
3833
  show_default=True,
3760
3834
  ),
3835
+ ci: bool = typer.Option(
3836
+ False,
3837
+ "--ci/--no-ci",
3838
+ help="Exit with code 1 if any findings at or above --min-severity are found. For CI/CD gates.",
3839
+ ),
3761
3840
  ) -> None:
3762
3841
  """Spring semantic audit: TX anomalies (TX-001..005) + security surface (SEC-001..003).
3763
3842
 
@@ -3772,6 +3851,12 @@ def spring_audit_cmd(
3772
3851
  SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
3773
3852
  SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
3774
3853
 
3854
+ \b
3855
+ CI/CD usage:
3856
+ sourcecode spring-audit . --ci # exit 1 on any finding
3857
+ sourcecode spring-audit . --ci --min-severity high # exit 1 only on high/critical
3858
+ sourcecode spring-audit . --ci --format github-comment # Markdown PR comment + exit 1
3859
+
3775
3860
  \b
3776
3861
  Examples:
3777
3862
  sourcecode spring-audit .
@@ -3818,15 +3903,28 @@ def spring_audit_cmd(
3818
3903
  )
3819
3904
  raise typer.Exit(code=1)
3820
3905
 
3821
- file_list = find_java_files(target)
3906
+ if format not in ("json", "yaml", "github-comment"):
3907
+ _emit_error_json(
3908
+ INVALID_INPUT_CODE,
3909
+ f"Invalid format '{format}'.",
3910
+ hint="format must be one of: json, yaml, github-comment.",
3911
+ expected="json | yaml | github-comment",
3912
+ )
3913
+ raise typer.Exit(code=1)
3914
+
3915
+ _file_limitations: list[str] = []
3916
+ file_list = find_java_files(target, limitations=_file_limitations)
3822
3917
  if not file_list:
3823
- data = SpringAuditResult(
3918
+ empty_result = SpringAuditResult(
3824
3919
  spring_detected=False,
3825
3920
  scope=scope,
3826
3921
  limitations=["No Java files found in repository — Spring audit requires Java source."],
3827
3922
  metadata={"java_files_found": 0},
3828
- ).finalize().to_dict()
3829
- output = _serialize_dict(data, format)
3923
+ ).finalize()
3924
+ if format == "github-comment":
3925
+ output = _render_spring_audit_github_comment(empty_result, min_severity)
3926
+ else:
3927
+ output = _serialize_dict(empty_result.to_dict(), format)
3830
3928
  if output_path is not None:
3831
3929
  output_path.write_text(output, encoding="utf-8")
3832
3930
  typer.echo("Spring audit written to " + str(output_path), err=True)
@@ -3864,6 +3962,9 @@ def spring_audit_cmd(
3864
3962
  metadata=merged_meta,
3865
3963
  ).finalize()
3866
3964
 
3965
+ if _file_limitations:
3966
+ combined.limitations.extend(_file_limitations)
3967
+
3867
3968
  # Populate git_head from repo HEAD — non-fatal.
3868
3969
  try:
3869
3970
  import subprocess as _sub_sa
@@ -3885,7 +3986,10 @@ def spring_audit_cmd(
3885
3986
  except Exception:
3886
3987
  pass
3887
3988
 
3888
- output = _serialize_dict(data, format)
3989
+ if format == "github-comment":
3990
+ output = _render_spring_audit_github_comment(combined, min_severity)
3991
+ else:
3992
+ output = _serialize_dict(data, format)
3889
3993
 
3890
3994
  if output_path is not None:
3891
3995
  output_path.write_text(output, encoding="utf-8")
@@ -3899,6 +4003,9 @@ def spring_audit_cmd(
3899
4003
  if _copy_to_clipboard(output):
3900
4004
  typer.echo("✓ copied to clipboard", err=True)
3901
4005
 
4006
+ if ci and combined.findings:
4007
+ raise typer.Exit(code=1)
4008
+
3902
4009
 
3903
4010
  # ── Spring Boot Migration Check ───────────────────────────────────────────────
3904
4011
 
@@ -3985,8 +4092,11 @@ def migrate_check_cmd(
3985
4092
  )
3986
4093
  raise typer.Exit(code=1)
3987
4094
 
3988
- file_list = find_java_files(target)
4095
+ _file_limitations: list[str] = []
4096
+ file_list = find_java_files(target, limitations=_file_limitations)
3989
4097
  report = run_migrate_check(file_list, target, min_severity=min_severity)
4098
+ if _file_limitations:
4099
+ report.limitations.extend(_file_limitations)
3990
4100
 
3991
4101
  if format == "text":
3992
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"):
@@ -1221,6 +1221,7 @@ _MCP_HIDDEN_CANONICAL_TOOLS: frozenset[str] = frozenset({
1221
1221
  # Listed here so validate_registry() skips CLI param-drift checks on the alias.
1222
1222
  "spring_audit", # curated: repo_path + scope + min_severity only (strips output_path/format/copy)
1223
1223
  "impact_chain", # curated: repo_path + symbol + depth + query_type with choices
1224
+ "migrate_check", # curated: repo_path + min_severity only (strips output_path/format/copy/ci)
1224
1225
  # MCP self-management (an agent is not the MCP client admin)
1225
1226
  "mcp_init",
1226
1227
  "mcp_serve",
@@ -1349,7 +1350,57 @@ query_type: "impact" (default) | "events"
1349
1350
  docstring_override=_IMPACT_CHAIN_DOC,
1350
1351
  )
1351
1352
 
1352
- return [spring_audit, impact_chain]
1353
+ _MIGRATE_CHECK_DOC = """\
1354
+ Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA ONLY.
1355
+
1356
+ When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
1357
+ or upgrading from Spring Boot 2.x to 3.x. Use BEFORE get_spring_audit when the goal
1358
+ is migration planning rather than ongoing Spring semantic audit.
1359
+ Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
1360
+
1361
+ Rules detected:
1362
+ MIG-001 critical — javax.persistence imports (JPA; will not compile after migration)
1363
+ MIG-002 high — javax.servlet imports (Servlet API changed)
1364
+ MIG-003 high — javax.validation imports (Bean Validation changed)
1365
+ MIG-004 high — javax.transaction imports (TX API changed)
1366
+ MIG-005 high — extends WebSecurityConfigurerAdapter (removed in Spring Security 6)
1367
+ MIG-006 medium — javax.annotation imports (CDI annotations)
1368
+ MIG-007 medium — javax.inject imports (DI annotations)
1369
+ MIG-008 medium — javax.ws.rs imports (JAX-RS API)
1370
+
1371
+ Returns: schema_version, readiness_score (0–100; 100=ready to migrate), blocking_count,
1372
+ estimated_effort_days, spring_boot_2_detected, summary (total_findings, affected_files,
1373
+ by_severity, by_rule), findings[], limitations, metadata.
1374
+ findings fields: id, rule_id, severity, title, source_file, first_line,
1375
+ imports_found, explanation, fix_hint.
1376
+
1377
+ repo_path: absolute path to the Java repository (default: current working directory).
1378
+ min_severity: "low" (default) | "medium" | "high" | "critical" — filter threshold.
1379
+ """
1380
+
1381
+ migrate_check = _alias_spec(
1382
+ "migrate_check",
1383
+ "Spring Boot 2→3 migration readiness: javax→jakarta blockers. JAVA ONLY.",
1384
+ ("migrate-check",),
1385
+ (
1386
+ ToolParamSpec("repo_path", "argument", str, required=False, default=".", is_path=True,
1387
+ help="Absolute path to the Java repository."),
1388
+ ToolParamSpec("min_severity", "option", str, required=False, default="low",
1389
+ option_names=("--min-severity",), choices=("low", "medium", "high", "critical"),
1390
+ help="low (default) | medium | high | critical"),
1391
+ ),
1392
+ lambda inputs: [
1393
+ "migrate-check",
1394
+ str(inputs.get("repo_path", ".")),
1395
+ "--min-severity", str(inputs.get("min_severity", "low")),
1396
+ ],
1397
+ supported_targets=("repo_path",),
1398
+ unsupported_targets=("file_path",),
1399
+ validator=validate_repo_path,
1400
+ docstring_override=_MIGRATE_CHECK_DOC,
1401
+ )
1402
+
1403
+ return [spring_audit, impact_chain, migrate_check]
1353
1404
 
1354
1405
 
1355
1406
  @lru_cache(maxsize=1)
sourcecode/mcp/server.py CHANGED
@@ -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,79 @@ 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
708
+ except Exception as exc:
709
+ return _err(
710
+ f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
711
+ "INTERNAL_ERROR",
712
+ )
713
+
714
+
715
+ @mcp.tool()
716
+ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") -> dict:
717
+ """Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA ONLY.
718
+
719
+ When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
720
+ or upgrading from Spring Boot 2.x to 3.x. Call this BEFORE get_spring_audit when
721
+ the goal is migration planning — not ongoing audit.
722
+ Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
723
+
724
+ Maps to: sourcecode migrate-check <repo_path> --min-severity <min_severity>
725
+ Returns: MigrationReport with schema_version, readiness_score (0–100; 100=ready to migrate),
726
+ blocking_count, estimated_effort_days, spring_boot_2_detected,
727
+ summary (total_findings, affected_files, by_severity, by_rule),
728
+ findings[], limitations, metadata.
729
+ findings fields: id, rule_id, severity, title, source_file, first_line,
730
+ imports_found, explanation, fix_hint.
731
+ Rules:
732
+ MIG-001 critical — javax.persistence (JPA, will not compile after migration)
733
+ MIG-002 high — javax.servlet (Servlet API)
734
+ MIG-003 high — javax.validation (Bean Validation)
735
+ MIG-004 high — javax.transaction (TX API)
736
+ MIG-005 high — extends WebSecurityConfigurerAdapter (removed in Spring Security 6)
737
+ MIG-006 medium — javax.annotation (CDI annotations)
738
+ MIG-007 medium — javax.inject (DI annotations)
739
+ MIG-008 medium — javax.ws.rs (JAX-RS API)
740
+
741
+ repo_path: absolute path to the Java repository (default: current working directory).
742
+ min_severity: "low" (default) | "medium" | "high" | "critical" — filter threshold.
743
+ """
744
+ _raw = repo_path
745
+ try:
746
+ if not isinstance(repo_path, str):
747
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
748
+ if min_severity not in ("critical", "high", "medium", "low"):
749
+ return _err(
750
+ f"Invalid min_severity '{min_severity}' — must be one of: critical, high, medium, low",
751
+ "INVALID_ARGUMENT",
752
+ )
753
+ repo_path = _normalize_repo_path(repo_path)
754
+ _path_err = _check_repo_path(repo_path)
755
+ if _path_err is not None:
756
+ return _path_err
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
645
761
  except Exception as exc:
646
762
  return _err(
647
763
  f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
@@ -681,7 +797,23 @@ def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) ->
681
797
  if _path_err is not None:
682
798
  return _path_err
683
799
  args = ["impact-chain", symbol.strip(), repo_path, "--depth", str(depth)]
684
- 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)
685
817
  except Exception as exc:
686
818
  return _err(
687
819
  f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
@@ -1098,7 +1230,23 @@ def get_impact_context(repo_path: str = ".", target: str = "", depth: int = 4) -
1098
1230
  if _path_err is not None:
1099
1231
  return _path_err
1100
1232
  args = ["impact", target.strip(), repo_path, "--depth", str(depth)]
1101
- 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)
1102
1250
  except Exception as exc:
1103
1251
  return _err(
1104
1252
  f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
@@ -1219,9 +1367,22 @@ def _finalize_mcp_registry() -> None:
1219
1367
  structured_output=False,
1220
1368
  )
1221
1369
 
1222
- drift = validate_registry()
1223
- if drift:
1224
- 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
+ )
1225
1386
 
1226
1387
 
1227
1388
  _finalize_mcp_registry()
@@ -3312,11 +3312,13 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3312
3312
  }
3313
3313
 
3314
3314
 
3315
- def find_java_files(root: Path, *, max_files: int = 8000) -> list[str]:
3315
+ def find_java_files(root: Path, *, max_files: int = 8000, limitations: list[str] | None = None) -> list[str]:
3316
3316
  """Return relative paths to Java files under root, excluding test dirs and vendor."""
3317
3317
  results: list[str] = []
3318
+ _capped = False
3318
3319
  for p in sorted(root.rglob("*.java")):
3319
3320
  if len(results) >= max_files:
3321
+ _capped = True
3320
3322
  break
3321
3323
  try:
3322
3324
  rel = str(p.relative_to(root)).replace("\\", "/")
@@ -3334,6 +3336,10 @@ def find_java_files(root: Path, *, max_files: int = 8000) -> list[str]:
3334
3336
  if any(f in rel for f in ("/admin-client/", "/rest-client/", "/client-api/", "/api-client/")):
3335
3337
  continue
3336
3338
  results.append(rel)
3339
+ if _capped and limitations is not None:
3340
+ limitations.append(
3341
+ f"MAX_JAVA_FILES_REACHED: scanned {max_files} files — repository likely has more"
3342
+ )
3337
3343
  return results
3338
3344
 
3339
3345
 
@@ -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"],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.19
3
+ Version: 1.35.22
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.16-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.20-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.16
117
+ # sourcecode 1.35.20
118
118
  ```
119
119
 
120
120
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=hzHULzlZzht_1GYLCF6iKVI8a1MFSsEJYu30xrAvGZU,104
1
+ sourcecode/__init__.py,sha256=p3pMcJ60xxF18F5C5hvsyn5B4uh1rOmKBO_RC94DPAA,104
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
@@ -7,7 +7,7 @@ sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
7
7
  sourcecode/canonical_ir.py,sha256=uwpwCnJxMh_eiIVg4cOLv7-aZthvmDFcG4azCOycLkw,24281
8
8
  sourcecode/cir_graphs.py,sha256=rZi8JV4ZrAa2WSCeyNa4JIEKQ_yZzDZTsrvVz2KfuKA,8919
9
9
  sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
10
- sourcecode/cli.py,sha256=CALwHKAoBv1MmhLE7ZingoarOxXTk3VUCrxF4CDYB20,233442
10
+ sourcecode/cli.py,sha256=wy5-T6Ba_hvqJoFkSROCJxOpsy_VzbYI98TlJIEeGW0,238063
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -15,7 +15,7 @@ sourcecode/context_summarizer.py,sha256=zlbm8ytdvJToohU108-dwBmEl52xl0gXpf6PZBOW
15
15
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
16
16
  sourcecode/contract_pipeline.py,sha256=gvTdDniedm_mjq4vaHqnBY2UkQ0s00gtXqzTLILNXHc,28719
17
17
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
18
- sourcecode/dependency_analyzer.py,sha256=5yUBN_Z75RWmBIbsfnMasPSUn-4IaLipt0A3b3LNVfc,56310
18
+ sourcecode/dependency_analyzer.py,sha256=gvFJf9gHyUGRia3tdPz8s0aX2Re6aohMhb40uFEbjp0,60420
19
19
  sourcecode/doc_analyzer.py,sha256=05bjTUbDbmnbajD_cgRnACzS8T7xxBKVX4CjkJlhZg8,24411
20
20
  sourcecode/entrypoint_classifier.py,sha256=jhTYlyqDJH2AtdEcLVaRU3lYRTJuF8DkxVzl4-W3zWE,5322
21
21
  sourcecode/env_analyzer.py,sha256=aNTyYgQk5noJDfJU6FmasmESOHfiomyJw5EvZqjy6qc,22213
@@ -40,7 +40,7 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
40
40
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
41
41
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
42
42
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
43
- sourcecode/repository_ir.py,sha256=SOACZ4viYG8tpmL_-l4XqoX6bN4GmCeX5ZIr9tnuQyA,169298
43
+ sourcecode/repository_ir.py,sha256=5meIvsVN4WlllStxIglZznILKcI0Q2t2XMLB9aVT3uc,169561
44
44
  sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
45
45
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
46
46
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -51,9 +51,9 @@ sourcecode/spring_event_topology.py,sha256=LvGv5RXtU_O-fVB_OO9eDD2UmZM72Jn2oUHgO
51
51
  sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
52
52
  sourcecode/spring_impact.py,sha256=Ohm2k3W4Wts8Kx8Z7DIM-J-cwGtTJBWKFBsX-WkupBQ,32943
53
53
  sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
54
- sourcecode/spring_security_audit.py,sha256=oaV-dK_5iSRwkWSJbMbzxfPbW376n3jTwnRPAspePjg,20293
54
+ sourcecode/spring_security_audit.py,sha256=AmUkqoExkNZ3YxxZf9TwkwX-f7P_SETm0QC7VqEAqh4,20618
55
55
  sourcecode/spring_semantic.py,sha256=CiAf77p48-RFrUF0zbgww4w2Xigrbo1t5M3ZCDIfV_g,12032
56
- sourcecode/spring_tx_analyzer.py,sha256=_RyXblWwMgTfUVtoceTzCrAVmlQ2Ovf0dMcBOy-0-3w,30201
56
+ sourcecode/spring_tx_analyzer.py,sha256=u4_ckdEFZUiIsHdUX4OaIhnvoTdAwrxNTFweG6vc7wE,30526
57
57
  sourcecode/summarizer.py,sha256=YspHEVeYJVmltq0FMtGZF8kIP3qiR2KLcanGL6Y7uTI,20747
58
58
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
59
59
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -80,9 +80,9 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
80
80
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
81
81
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
82
82
  sourcecode/mcp/orchestrator.py,sha256=BMi1D6liJHI3DXiaC8yeBLLP0wXajpCP3-vnRGqrvnw,26850
83
- sourcecode/mcp/registry.py,sha256=y9dBj00xlTun6ZsKer9HZJ4W70ZiSdOrY4d-jBe9e08,60460
83
+ sourcecode/mcp/registry.py,sha256=XeshSuT6NMmeUZ2GCzNVcKcr-2Ljoj4qO-lvSrg17EM,63135
84
84
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
85
- sourcecode/mcp/server.py,sha256=lBSQCw3yFe8rZHp2GGVcfua0EJUYZmsIUbvA4GIJv9s,52210
85
+ sourcecode/mcp/server.py,sha256=Zapr4lY0i4tqSXY2BfA283VzStHTohNr9N0uMnRSIIA,59911
86
86
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
87
87
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
88
88
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -94,8 +94,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
94
94
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
95
95
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
96
96
  sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
97
- sourcecode-1.35.19.dist-info/METADATA,sha256=PoK3wFYqg5mUZAk98YbNEx2Gvd7c55DmNlYqORyKox8,21297
98
- sourcecode-1.35.19.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
- sourcecode-1.35.19.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
- sourcecode-1.35.19.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
- sourcecode-1.35.19.dist-info/RECORD,,
97
+ sourcecode-1.35.22.dist-info/METADATA,sha256=myCZutK9p4r8d-O38SYo4et7pkpwASrkuJOnr_WIAac,21297
98
+ sourcecode-1.35.22.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
+ sourcecode-1.35.22.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
+ sourcecode-1.35.22.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
+ sourcecode-1.35.22.dist-info/RECORD,,