sourcecode 1.35.18__py3-none-any.whl → 1.35.20__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.18"
3
+ __version__ = "1.35.20"
sourcecode/cli.py CHANGED
@@ -240,6 +240,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
240
240
  "pr-impact",
241
241
  # Class architectural summary
242
242
  "explain",
243
+ # Spring Boot 2→3 migration readiness
244
+ "migrate-check",
243
245
  }
244
246
  )
245
247
 
@@ -3720,6 +3722,80 @@ def endpoints_cmd(
3720
3722
 
3721
3723
  # ── Spring Semantic Audit ─────────────────────────────────────────────────────
3722
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
+
3723
3799
  @app.command("spring-audit")
3724
3800
  def spring_audit_cmd(
3725
3801
  path: Path = typer.Argument(
@@ -3734,7 +3810,7 @@ def spring_audit_cmd(
3734
3810
  "json",
3735
3811
  "--format",
3736
3812
  "-f",
3737
- help="Output format: json (default) or yaml.",
3813
+ help="Output format: json (default), yaml, or github-comment.",
3738
3814
  show_default=True,
3739
3815
  ),
3740
3816
  copy: bool = typer.Option(
@@ -3756,6 +3832,11 @@ def spring_audit_cmd(
3756
3832
  help="Minimum severity to include: critical, high, medium, or low (default).",
3757
3833
  show_default=True,
3758
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
+ ),
3759
3840
  ) -> None:
3760
3841
  """Spring semantic audit: TX anomalies (TX-001..005) + security surface (SEC-001..003).
3761
3842
 
@@ -3770,6 +3851,12 @@ def spring_audit_cmd(
3770
3851
  SEC-002 CVE-2025-41248: @PreAuthorize on inherited method from generic supertype
3771
3852
  SEC-003 @Transactional on @Controller/@RestController (TX in wrong layer)
3772
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
+
3773
3860
  \b
3774
3861
  Examples:
3775
3862
  sourcecode spring-audit .
@@ -3816,15 +3903,27 @@ def spring_audit_cmd(
3816
3903
  )
3817
3904
  raise typer.Exit(code=1)
3818
3905
 
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
+
3819
3915
  file_list = find_java_files(target)
3820
3916
  if not file_list:
3821
- data = SpringAuditResult(
3917
+ empty_result = SpringAuditResult(
3822
3918
  spring_detected=False,
3823
3919
  scope=scope,
3824
3920
  limitations=["No Java files found in repository — Spring audit requires Java source."],
3825
3921
  metadata={"java_files_found": 0},
3826
- ).finalize().to_dict()
3827
- output = _serialize_dict(data, format)
3922
+ ).finalize()
3923
+ if format == "github-comment":
3924
+ output = _render_spring_audit_github_comment(empty_result, min_severity)
3925
+ else:
3926
+ output = _serialize_dict(empty_result.to_dict(), format)
3828
3927
  if output_path is not None:
3829
3928
  output_path.write_text(output, encoding="utf-8")
3830
3929
  typer.echo("Spring audit written to " + str(output_path), err=True)
@@ -3883,7 +3982,10 @@ def spring_audit_cmd(
3883
3982
  except Exception:
3884
3983
  pass
3885
3984
 
3886
- output = _serialize_dict(data, format)
3985
+ if format == "github-comment":
3986
+ output = _render_spring_audit_github_comment(combined, min_severity)
3987
+ else:
3988
+ output = _serialize_dict(data, format)
3887
3989
 
3888
3990
  if output_path is not None:
3889
3991
  output_path.write_text(output, encoding="utf-8")
@@ -3897,6 +3999,119 @@ def spring_audit_cmd(
3897
3999
  if _copy_to_clipboard(output):
3898
4000
  typer.echo("✓ copied to clipboard", err=True)
3899
4001
 
4002
+ if ci and combined.findings:
4003
+ raise typer.Exit(code=1)
4004
+
4005
+
4006
+ # ── Spring Boot Migration Check ───────────────────────────────────────────────
4007
+
4008
+
4009
+ @app.command("migrate-check")
4010
+ def migrate_check_cmd(
4011
+ path: Path = typer.Argument(
4012
+ Path("."),
4013
+ help="Repository path to scan (default: current directory)",
4014
+ ),
4015
+ output_path: Optional[Path] = typer.Option(
4016
+ None, "--output", "-o",
4017
+ help="Write output to a file instead of stdout.",
4018
+ ),
4019
+ format: str = typer.Option(
4020
+ "json",
4021
+ "--format",
4022
+ "-f",
4023
+ help="Output format: json (default) or text.",
4024
+ show_default=True,
4025
+ ),
4026
+ copy: bool = typer.Option(
4027
+ False,
4028
+ "--copy",
4029
+ "-c",
4030
+ help="Copy output to system clipboard after a successful run.",
4031
+ ),
4032
+ min_severity: str = typer.Option(
4033
+ "low",
4034
+ "--min-severity",
4035
+ help="Minimum severity to include: critical, high, medium, or low (default).",
4036
+ show_default=True,
4037
+ ),
4038
+ ) -> None:
4039
+ """Spring Boot 2→3 migration readiness: detect javax→jakarta namespace blockers.
4040
+
4041
+ \b
4042
+ Detects:
4043
+ MIG-001 javax.persistence import (CRITICAL — JPA will not compile)
4044
+ MIG-002 javax.servlet import (HIGH — Servlet API changed)
4045
+ MIG-003 javax.validation import (HIGH — Bean Validation changed)
4046
+ MIG-004 javax.transaction import (HIGH — TX API changed)
4047
+ MIG-005 extends WebSecurityConfigurerAdapter (HIGH — removed in Spring 6)
4048
+ MIG-006 javax.annotation import (MEDIUM)
4049
+ MIG-007 javax.inject import (MEDIUM)
4050
+ MIG-008 javax.ws.rs import (MEDIUM — JAX-RS changed)
4051
+
4052
+ \b
4053
+ Examples:
4054
+ sourcecode migrate-check .
4055
+ sourcecode migrate-check /path/to/repo --format text
4056
+ sourcecode migrate-check . --min-severity high
4057
+ sourcecode migrate-check . --output migration.json
4058
+ """
4059
+ from sourcecode.repository_ir import find_java_files
4060
+ from sourcecode.migrate_check import run_migrate_check
4061
+
4062
+ target = path.resolve()
4063
+ if not target.exists() or not target.is_dir():
4064
+ _emit_error_json(
4065
+ INVALID_INPUT_CODE,
4066
+ f"'{target}' is not a valid directory.",
4067
+ path=str(target),
4068
+ hint="Pass an existing repository directory.",
4069
+ expected="A directory path.",
4070
+ )
4071
+ raise typer.Exit(code=1)
4072
+
4073
+ if format not in ("json", "text"):
4074
+ _emit_error_json(
4075
+ INVALID_INPUT_CODE,
4076
+ f"Invalid format '{format}'.",
4077
+ hint="format must be one of: json, text.",
4078
+ expected="json | text",
4079
+ )
4080
+ raise typer.Exit(code=1)
4081
+
4082
+ if min_severity not in ("critical", "high", "medium", "low"):
4083
+ _emit_error_json(
4084
+ INVALID_INPUT_CODE,
4085
+ f"Invalid min-severity '{min_severity}'.",
4086
+ hint="min-severity must be one of: critical, high, medium, low.",
4087
+ expected="critical | high | medium | low",
4088
+ )
4089
+ raise typer.Exit(code=1)
4090
+
4091
+ file_list = find_java_files(target)
4092
+ report = run_migrate_check(file_list, target, min_severity=min_severity)
4093
+
4094
+ if format == "text":
4095
+ output = report.to_text(min_severity=min_severity)
4096
+ else:
4097
+ output = _serialize_dict(report.to_dict(), "json")
4098
+
4099
+ if output_path is not None:
4100
+ output_path.write_text(output, encoding="utf-8")
4101
+ total = report.summary.get("total_findings", 0)
4102
+ typer.echo(
4103
+ f"Migration check written to {output_path} "
4104
+ f"(score: {report.readiness_score}/100, {total} findings)",
4105
+ err=True,
4106
+ )
4107
+ else:
4108
+ sys.stdout.buffer.write(output.encode("utf-8"))
4109
+ sys.stdout.buffer.write(b"\n")
4110
+ sys.stdout.buffer.flush()
4111
+ if copy:
4112
+ if _copy_to_clipboard(output):
4113
+ typer.echo("✓ copied to clipboard", err=True)
4114
+
3900
4115
 
3901
4116
  # ── Spring Impact Chain ───────────────────────────────────────────────────────
3902
4117
 
@@ -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
@@ -649,6 +649,56 @@ def get_spring_audit(repo_path: str = ".", scope: str = "all") -> dict:
649
649
  )
650
650
 
651
651
 
652
+ @mcp.tool()
653
+ def get_migration_readiness(repo_path: str = ".", min_severity: str = "low") -> dict:
654
+ """Spring Boot 2→3 migration readiness: javax→jakarta namespace blockers. JAVA ONLY.
655
+
656
+ When to call: when asked about Spring Boot migration readiness, javax vs jakarta imports,
657
+ or upgrading from Spring Boot 2.x to 3.x. Call this BEFORE get_spring_audit when
658
+ the goal is migration planning — not ongoing audit.
659
+ Do NOT call on non-Java repositories — returns readiness_score=100 with no findings.
660
+
661
+ Maps to: sourcecode migrate-check <repo_path> --min-severity <min_severity>
662
+ Returns: MigrationReport with schema_version, readiness_score (0–100; 100=ready to migrate),
663
+ blocking_count, estimated_effort_days, spring_boot_2_detected,
664
+ summary (total_findings, affected_files, by_severity, by_rule),
665
+ findings[], limitations, metadata.
666
+ findings fields: id, rule_id, severity, title, source_file, first_line,
667
+ imports_found, explanation, fix_hint.
668
+ Rules:
669
+ MIG-001 critical — javax.persistence (JPA, will not compile after migration)
670
+ MIG-002 high — javax.servlet (Servlet API)
671
+ MIG-003 high — javax.validation (Bean Validation)
672
+ MIG-004 high — javax.transaction (TX API)
673
+ MIG-005 high — extends WebSecurityConfigurerAdapter (removed in Spring Security 6)
674
+ MIG-006 medium — javax.annotation (CDI annotations)
675
+ MIG-007 medium — javax.inject (DI annotations)
676
+ MIG-008 medium — javax.ws.rs (JAX-RS API)
677
+
678
+ repo_path: absolute path to the Java repository (default: current working directory).
679
+ min_severity: "low" (default) | "medium" | "high" | "critical" — filter threshold.
680
+ """
681
+ _raw = repo_path
682
+ try:
683
+ if not isinstance(repo_path, str):
684
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
685
+ if min_severity not in ("critical", "high", "medium", "low"):
686
+ return _err(
687
+ f"Invalid min_severity '{min_severity}' — must be one of: critical, high, medium, low",
688
+ "INVALID_ARGUMENT",
689
+ )
690
+ repo_path = _normalize_repo_path(repo_path)
691
+ _path_err = _check_repo_path(repo_path)
692
+ if _path_err is not None:
693
+ return _path_err
694
+ return _execute(["migrate-check", repo_path, "--min-severity", min_severity])
695
+ except Exception as exc:
696
+ return _err(
697
+ f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
698
+ "INTERNAL_ERROR",
699
+ )
700
+
701
+
652
702
  @mcp.tool()
653
703
  def get_impact_chain(repo_path: str = ".", symbol: str = "", depth: int = 4) -> dict:
654
704
  """Spring impact-chain: systemic blast radius of a symbol with TX/SEC semantic enrichment. JAVA/SPRING ONLY.
@@ -0,0 +1,434 @@
1
+ """migrate_check.py — Spring Boot 2→3 (javax→jakarta) migration readiness checker.
2
+
3
+ Scans Java source files for import namespaces and class patterns that must be
4
+ updated when migrating from Spring Boot 2.x (javax.*) to Spring Boot 3.x (jakarta.*).
5
+
6
+ Entry point: run_migrate_check(file_paths, root) → MigrationReport
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import hashlib
11
+ import re
12
+ from dataclasses import dataclass, field
13
+ from datetime import datetime, timezone
14
+ from pathlib import Path
15
+ from typing import Optional
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Rule catalogue
20
+ # ---------------------------------------------------------------------------
21
+
22
+ @dataclass(frozen=True)
23
+ class _Rule:
24
+ id: str
25
+ severity: str
26
+ title: str
27
+ explanation: str
28
+ fix_hint: str
29
+ import_pattern: Optional[re.Pattern] = None # matches the import statement
30
+ extends_pattern: Optional[re.Pattern] = None # matches an extends clause
31
+
32
+
33
+ _IMPORT_RULES: list[_Rule] = [
34
+ _Rule(
35
+ id="MIG-001",
36
+ severity="critical",
37
+ title="javax.persistence import — JPA namespace not migrated to jakarta",
38
+ explanation=(
39
+ "Spring Boot 3 uses Jakarta EE 9 which moved JPA to the jakarta.persistence "
40
+ "namespace. Files importing javax.persistence will not compile after migration."
41
+ ),
42
+ fix_hint="Replace 'javax.persistence' with 'jakarta.persistence' across all affected files.",
43
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.persistence[^;]+);", re.MULTILINE),
44
+ ),
45
+ _Rule(
46
+ id="MIG-002",
47
+ severity="high",
48
+ title="javax.servlet import — Servlet API not migrated to jakarta",
49
+ explanation=(
50
+ "Spring Boot 3 bundles Jakarta Servlet 6.0. Filters, HttpServletRequest, and "
51
+ "HttpServletResponse referencing javax.servlet will break after migration."
52
+ ),
53
+ fix_hint="Replace 'javax.servlet' with 'jakarta.servlet' and update the servlet-api dependency.",
54
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.servlet[^;]+);", re.MULTILINE),
55
+ ),
56
+ _Rule(
57
+ id="MIG-003",
58
+ severity="high",
59
+ title="javax.validation import — Bean Validation not migrated to jakarta",
60
+ explanation=(
61
+ "Spring Boot 3 uses Hibernate Validator 8.x which implements jakarta.validation. "
62
+ "Constraint annotations (@NotNull, @Valid, etc.) under javax.validation will not be "
63
+ "picked up by the validator after migration."
64
+ ),
65
+ fix_hint="Replace 'javax.validation' with 'jakarta.validation'.",
66
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.validation[^;]+);", re.MULTILINE),
67
+ ),
68
+ _Rule(
69
+ id="MIG-004",
70
+ severity="high",
71
+ title="javax.transaction import — TX API not migrated to jakarta",
72
+ explanation=(
73
+ "Spring Boot 3 depends on Jakarta Transactions (jakarta.transaction). "
74
+ "Direct javax.transaction imports (@Transactional from javax or UserTransaction) "
75
+ "will resolve to the wrong class after migration."
76
+ ),
77
+ fix_hint="Replace 'javax.transaction' with 'jakarta.transaction'.",
78
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.transaction[^;]+);", re.MULTILINE),
79
+ ),
80
+ _Rule(
81
+ id="MIG-006",
82
+ severity="medium",
83
+ title="javax.annotation import — CDI annotations not migrated to jakarta",
84
+ explanation=(
85
+ "jakarta.annotation replaces javax.annotation in Jakarta EE 9+. "
86
+ "@PostConstruct, @PreDestroy, @Resource are affected."
87
+ ),
88
+ fix_hint="Replace 'javax.annotation' with 'jakarta.annotation'.",
89
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.annotation[^;]+);", re.MULTILINE),
90
+ ),
91
+ _Rule(
92
+ id="MIG-007",
93
+ severity="medium",
94
+ title="javax.inject import — DI annotations not migrated to jakarta",
95
+ explanation=(
96
+ "jakarta.inject replaces javax.inject in Jakarta EE 9+. "
97
+ "@Inject and @Named from javax.inject are affected."
98
+ ),
99
+ fix_hint="Replace 'javax.inject' with 'jakarta.inject'.",
100
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.inject[^;]+);", re.MULTILINE),
101
+ ),
102
+ _Rule(
103
+ id="MIG-008",
104
+ severity="medium",
105
+ title="javax.ws.rs import — JAX-RS API not migrated to jakarta",
106
+ explanation=(
107
+ "jakarta.ws.rs replaces javax.ws.rs in Jakarta EE 9+. "
108
+ "JAX-RS resource classes, Response, and client code are affected."
109
+ ),
110
+ fix_hint="Replace 'javax.ws.rs' with 'jakarta.ws.rs'.",
111
+ import_pattern=re.compile(r"^[ \t]*import\s+(javax\.ws\.rs[^;]+);", re.MULTILINE),
112
+ ),
113
+ ]
114
+
115
+ _EXTENDS_RULES: list[_Rule] = [
116
+ _Rule(
117
+ id="MIG-005",
118
+ severity="high",
119
+ title="extends WebSecurityConfigurerAdapter — removed in Spring Security 6",
120
+ explanation=(
121
+ "WebSecurityConfigurerAdapter was deprecated in Spring Security 5.7 and removed in "
122
+ "Spring Security 6 (Spring Boot 3). Classes extending it must be replaced with "
123
+ "SecurityFilterChain @Bean methods in a @Configuration class."
124
+ ),
125
+ fix_hint=(
126
+ "Remove the class extension and expose a SecurityFilterChain @Bean instead. "
127
+ "See the Spring Security 6 migration guide."
128
+ ),
129
+ extends_pattern=re.compile(r"\bextends\s+WebSecurityConfigurerAdapter\b"),
130
+ ),
131
+ ]
132
+
133
+ _ALL_RULES: list[_Rule] = _IMPORT_RULES + _EXTENDS_RULES
134
+
135
+ SEVERITY_ORDER: dict[str, int] = {"critical": 0, "high": 1, "medium": 2, "low": 3}
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # Finding
140
+ # ---------------------------------------------------------------------------
141
+
142
+ @dataclass
143
+ class MigrationFinding:
144
+ id: str # deterministic: "{rule_id}-{file_hash[:12]}"
145
+ rule_id: str # "MIG-001" .. "MIG-008"
146
+ severity: str # "critical" | "high" | "medium" | "low"
147
+ title: str
148
+ source_file: str # relative path
149
+ first_line: int # 1-based line number of first match
150
+ imports_found: list[str] = field(default_factory=list) # matched import statements
151
+ explanation: str = ""
152
+ fix_hint: str = ""
153
+
154
+ @staticmethod
155
+ def make_id(rule_id: str, source_file: str) -> str:
156
+ h = hashlib.sha256(f"{rule_id}:{source_file}".encode()).hexdigest()[:12]
157
+ return f"{rule_id}-{h}"
158
+
159
+ def to_dict(self) -> dict:
160
+ d: dict = {
161
+ "id": self.id,
162
+ "rule_id": self.rule_id,
163
+ "severity": self.severity,
164
+ "title": self.title,
165
+ "source_file": self.source_file,
166
+ "first_line": self.first_line,
167
+ "explanation": self.explanation,
168
+ "fix_hint": self.fix_hint,
169
+ }
170
+ if self.imports_found:
171
+ d["imports_found"] = self.imports_found
172
+ return d
173
+
174
+
175
+ # ---------------------------------------------------------------------------
176
+ # Report
177
+ # ---------------------------------------------------------------------------
178
+
179
+ @dataclass
180
+ class MigrationReport:
181
+ schema_version: str = "1.0"
182
+ generated_at: str = ""
183
+ repo_id: str = ""
184
+ git_head: str = ""
185
+
186
+ # Core metrics
187
+ readiness_score: int = 100 # 0–100; 100 = ready to migrate
188
+ blocking_count: int = 0 # critical + high finding count
189
+ estimated_effort_days: float = 0.0
190
+ spring_boot_2_detected: bool = False
191
+
192
+ findings: list[MigrationFinding] = field(default_factory=list)
193
+ limitations: list[str] = field(default_factory=list)
194
+ summary: dict = field(default_factory=dict)
195
+ metadata: dict = field(default_factory=dict)
196
+
197
+ def finalize(self) -> "MigrationReport":
198
+ if not self.generated_at:
199
+ self.generated_at = datetime.now(timezone.utc).isoformat()
200
+
201
+ by_severity: dict[str, int] = {"critical": 0, "high": 0, "medium": 0, "low": 0}
202
+ by_rule: dict[str, int] = {}
203
+ affected_files: set[str] = set()
204
+
205
+ for f in self.findings:
206
+ by_severity[f.severity] = by_severity.get(f.severity, 0) + 1
207
+ by_rule[f.rule_id] = by_rule.get(f.rule_id, 0) + 1
208
+ affected_files.add(f.source_file)
209
+
210
+ self.blocking_count = by_severity["critical"] + by_severity["high"]
211
+
212
+ # Score: deduct per affected-file/severity combination (not per finding, to avoid
213
+ # double-counting a file that imports 10 javax.persistence classes).
214
+ critical_files: set[str] = set()
215
+ high_files: set[str] = set()
216
+ medium_files: set[str] = set()
217
+ low_files: set[str] = set()
218
+ for f in self.findings:
219
+ if f.severity == "critical":
220
+ critical_files.add(f.source_file)
221
+ elif f.severity == "high":
222
+ high_files.add(f.source_file)
223
+ elif f.severity == "medium":
224
+ medium_files.add(f.source_file)
225
+ else:
226
+ low_files.add(f.source_file)
227
+
228
+ deduction = (
229
+ len(critical_files) * 15
230
+ + len(high_files) * 8
231
+ + len(medium_files) * 3
232
+ + len(low_files) * 1
233
+ )
234
+ self.readiness_score = max(0, 100 - deduction)
235
+
236
+ # Effort: sum per distinct affected file weighted by severity
237
+ self.estimated_effort_days = round(
238
+ len(critical_files) * 0.5
239
+ + len(high_files) * 0.25
240
+ + len(medium_files) * 0.1
241
+ + len(low_files) * 0.05,
242
+ 1,
243
+ )
244
+
245
+ self.summary = {
246
+ "total_findings": len(self.findings),
247
+ "affected_files": len(affected_files),
248
+ "by_severity": by_severity,
249
+ "by_rule": by_rule,
250
+ }
251
+ return self
252
+
253
+ def to_dict(self) -> dict:
254
+ return {
255
+ "schema_version": self.schema_version,
256
+ "generated_at": self.generated_at,
257
+ "repo_id": self.repo_id,
258
+ "git_head": self.git_head,
259
+ "readiness_score": self.readiness_score,
260
+ "blocking_count": self.blocking_count,
261
+ "estimated_effort_days": self.estimated_effort_days,
262
+ "spring_boot_2_detected": self.spring_boot_2_detected,
263
+ "summary": self.summary,
264
+ "findings": [f.to_dict() for f in self.findings],
265
+ "limitations": self.limitations,
266
+ "metadata": self.metadata,
267
+ }
268
+
269
+ def to_text(self, min_severity: str = "low") -> str:
270
+ min_order = SEVERITY_ORDER.get(min_severity, 3)
271
+ visible = [f for f in self.findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
272
+
273
+ lines: list[str] = [
274
+ f"Migration Readiness: {self.readiness_score}/100",
275
+ f"Blocking issues: {self.blocking_count} "
276
+ f"(critical: {self.summary.get('by_severity', {}).get('critical', 0)}, "
277
+ f"high: {self.summary.get('by_severity', {}).get('high', 0)})",
278
+ f"Affected files: {self.summary.get('affected_files', 0)}",
279
+ f"Estimated effort: {self.estimated_effort_days}d",
280
+ "",
281
+ ]
282
+
283
+ if not visible:
284
+ lines.append("No findings at or above selected severity.")
285
+ return "\n".join(lines)
286
+
287
+ for f in sorted(visible, key=lambda x: (SEVERITY_ORDER.get(x.severity, 3), x.source_file)):
288
+ lines.append(
289
+ f"{f.rule_id} [{f.severity.upper()}] {f.source_file}:{f.first_line}"
290
+ )
291
+ lines.append(f" {f.title}")
292
+ lines.append(f" Fix: {f.fix_hint}")
293
+ lines.append("")
294
+
295
+ return "\n".join(lines)
296
+
297
+
298
+ # ---------------------------------------------------------------------------
299
+ # Scanner
300
+ # ---------------------------------------------------------------------------
301
+
302
+ def _scan_file(
303
+ source: str,
304
+ rel_path: str,
305
+ rules: list[_Rule],
306
+ ) -> list[MigrationFinding]:
307
+ findings: list[MigrationFinding] = []
308
+
309
+ for rule in rules:
310
+ if rule.import_pattern is not None:
311
+ matches = list(rule.import_pattern.finditer(source))
312
+ if not matches:
313
+ continue
314
+ # Compute 1-based line number of first match
315
+ first_line = source[: matches[0].start()].count("\n") + 1
316
+ imports_found = [m.group(1) for m in matches]
317
+ findings.append(
318
+ MigrationFinding(
319
+ id=MigrationFinding.make_id(rule.id, rel_path),
320
+ rule_id=rule.id,
321
+ severity=rule.severity,
322
+ title=rule.title,
323
+ source_file=rel_path,
324
+ first_line=first_line,
325
+ imports_found=imports_found,
326
+ explanation=rule.explanation,
327
+ fix_hint=rule.fix_hint,
328
+ )
329
+ )
330
+
331
+ elif rule.extends_pattern is not None:
332
+ m = rule.extends_pattern.search(source)
333
+ if m is None:
334
+ continue
335
+ first_line = source[: m.start()].count("\n") + 1
336
+ findings.append(
337
+ MigrationFinding(
338
+ id=MigrationFinding.make_id(rule.id, rel_path),
339
+ rule_id=rule.id,
340
+ severity=rule.severity,
341
+ title=rule.title,
342
+ source_file=rel_path,
343
+ first_line=first_line,
344
+ explanation=rule.explanation,
345
+ fix_hint=rule.fix_hint,
346
+ )
347
+ )
348
+
349
+ return findings
350
+
351
+
352
+ # ---------------------------------------------------------------------------
353
+ # Public entry point
354
+ # ---------------------------------------------------------------------------
355
+
356
+ def run_migrate_check(
357
+ file_paths: list[str],
358
+ root: Path,
359
+ *,
360
+ min_severity: str = "low",
361
+ ) -> MigrationReport:
362
+ """Scan Java files for Spring Boot 2→3 migration blockers.
363
+
364
+ Args:
365
+ file_paths: Relative Java file paths (from find_java_files).
366
+ root: Absolute repo root.
367
+ min_severity: Filter threshold — findings below this severity are excluded
368
+ from the report. Choices: critical | high | medium | low.
369
+
370
+ Returns:
371
+ MigrationReport with findings, readiness_score, and effort estimate.
372
+ """
373
+ min_order = SEVERITY_ORDER.get(min_severity, 3)
374
+ all_findings: list[MigrationFinding] = []
375
+ limitations: list[str] = []
376
+ read_errors = 0
377
+
378
+ for rel_path in file_paths:
379
+ abs_path = root / rel_path
380
+ try:
381
+ source = abs_path.read_text(encoding="utf-8", errors="replace")
382
+ except OSError:
383
+ read_errors += 1
384
+ continue
385
+
386
+ file_findings = _scan_file(source, rel_path, _ALL_RULES)
387
+ # Apply min_severity filter
388
+ filtered = [f for f in file_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
389
+ all_findings.extend(filtered)
390
+
391
+ if read_errors:
392
+ limitations.append(f"{read_errors} file(s) could not be read and were skipped.")
393
+
394
+ # Detect Spring Boot 2 pom.xml heuristic (best-effort, non-fatal)
395
+ spring_boot_2 = _detect_spring_boot_2(root)
396
+
397
+ report = MigrationReport(
398
+ spring_boot_2_detected=spring_boot_2,
399
+ findings=all_findings,
400
+ limitations=limitations,
401
+ metadata={
402
+ "java_files_scanned": len(file_paths),
403
+ "min_severity": min_severity,
404
+ "rules_applied": [r.id for r in _ALL_RULES],
405
+ },
406
+ )
407
+
408
+ # Populate git_head — non-fatal
409
+ try:
410
+ import subprocess as _sub
411
+ _r = _sub.run(
412
+ ["git", "-C", str(root), "rev-parse", "--short", "HEAD"],
413
+ capture_output=True, text=True, timeout=3,
414
+ )
415
+ if _r.returncode == 0:
416
+ report.git_head = _r.stdout.strip()
417
+ except Exception:
418
+ pass
419
+
420
+ return report.finalize()
421
+
422
+
423
+ def _detect_spring_boot_2(root: Path) -> bool:
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)
426
+ for name in ("pom.xml", "build.gradle", "build.gradle.kts"):
427
+ candidate = root / name
428
+ try:
429
+ text = candidate.read_text(encoding="utf-8", errors="replace")
430
+ if _SB2.search(text):
431
+ return True
432
+ except OSError:
433
+ pass
434
+ return False
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.18
3
+ Version: 1.35.20
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=r4oQ6QPKKMHrjnfFwyEU8-Za6oZOvpxIorOgVbDG10o,104
1
+ sourcecode/__init__.py,sha256=l8p-xmLEzkjJbgIarJoeYeAvJ-XevKuJ0pJk4s4nFtM,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=k_MeCOfAlal3VORr6PaWr9sRbMbhKHOV0sI3O2jcxes,229485
10
+ sourcecode/cli.py,sha256=YOcCerZFukibhfR5urXXfGXqX9MOyZgK_cgiOdHJH_k,237764
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
@@ -29,6 +29,7 @@ sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,
29
29
  sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
30
30
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
31
31
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
32
+ sourcecode/migrate_check.py,sha256=KQJRuQPrZSDYnIsekEjF-6j6b702Cu-pjojo-teO4wM,16338
32
33
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
33
34
  sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
34
35
  sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
@@ -79,9 +80,9 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
79
80
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
80
81
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
81
82
  sourcecode/mcp/orchestrator.py,sha256=BMi1D6liJHI3DXiaC8yeBLLP0wXajpCP3-vnRGqrvnw,26850
82
- sourcecode/mcp/registry.py,sha256=y9dBj00xlTun6ZsKer9HZJ4W70ZiSdOrY4d-jBe9e08,60460
83
+ sourcecode/mcp/registry.py,sha256=XeshSuT6NMmeUZ2GCzNVcKcr-2Ljoj4qO-lvSrg17EM,63135
83
84
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
84
- sourcecode/mcp/server.py,sha256=lBSQCw3yFe8rZHp2GGVcfua0EJUYZmsIUbvA4GIJv9s,52210
85
+ sourcecode/mcp/server.py,sha256=G2vGG791pEjd7NNUP7vcDPsaFUEyfnGYyYJjMiiFZls,54789
85
86
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
86
87
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
87
88
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -93,8 +94,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
93
94
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
94
95
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
95
96
  sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
96
- sourcecode-1.35.18.dist-info/METADATA,sha256=A-e9C9ArfzHZ7g2CPnXdfg2YGTRk_N1QfrFKbMT6QMs,21297
97
- sourcecode-1.35.18.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
98
- sourcecode-1.35.18.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
99
- sourcecode-1.35.18.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
100
- sourcecode-1.35.18.dist-info/RECORD,,
97
+ sourcecode-1.35.20.dist-info/METADATA,sha256=KVCGOgyCNzyPgXlmXaBGm15I45DFenMGxvduoVE6gYg,21297
98
+ sourcecode-1.35.20.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
+ sourcecode-1.35.20.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
+ sourcecode-1.35.20.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
+ sourcecode-1.35.20.dist-info/RECORD,,