sourcecode 1.31.0__py3-none-any.whl → 1.31.2__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 +1 -1
- sourcecode/cli.py +287 -26
- sourcecode/confidence_analyzer.py +1 -1
- sourcecode/mcp/onboarding/detector.py +61 -16
- sourcecode/mcp/runner.py +10 -3
- sourcecode/mcp/server.py +93 -84
- sourcecode/prepare_context.py +165 -35
- sourcecode/serializer.py +1 -1
- {sourcecode-1.31.0.dist-info → sourcecode-1.31.2.dist-info}/METADATA +4 -3
- {sourcecode-1.31.0.dist-info → sourcecode-1.31.2.dist-info}/RECORD +13 -13
- {sourcecode-1.31.0.dist-info → sourcecode-1.31.2.dist-info}/WHEEL +0 -0
- {sourcecode-1.31.0.dist-info → sourcecode-1.31.2.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.31.0.dist-info → sourcecode-1.31.2.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cli.py
CHANGED
|
@@ -165,7 +165,7 @@ Compressed AI-ready context for Java/Spring enterprise codebases.
|
|
|
165
165
|
# Known subcommand names — tokens matching these are routed as subcommands,
|
|
166
166
|
# not consumed as a repository path.
|
|
167
167
|
_SUBCOMMANDS: frozenset[str] = frozenset(
|
|
168
|
-
{"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir", "mcp"}
|
|
168
|
+
{"telemetry", "prepare-context", "version", "config", "analyze", "repo-ir", "mcp", "endpoints"}
|
|
169
169
|
)
|
|
170
170
|
|
|
171
171
|
# Mutable container holding the path extracted by _preprocess_argv().
|
|
@@ -2037,6 +2037,27 @@ def prepare_context_cmd(
|
|
|
2037
2037
|
out["changed_files"] = output.changed_files
|
|
2038
2038
|
if _task_include("affected_entry_points") and output.affected_entry_points:
|
|
2039
2039
|
out["affected_entry_points"] = output.affected_entry_points
|
|
2040
|
+
# compact_base fields — included for all non-delta/review-pr tasks (Fix #1)
|
|
2041
|
+
if task not in ("delta", "review-pr"):
|
|
2042
|
+
if output.entry_points_structured:
|
|
2043
|
+
out["entry_points"] = output.entry_points_structured
|
|
2044
|
+
if output.deployment:
|
|
2045
|
+
out["deployment"] = output.deployment
|
|
2046
|
+
if output.deployment_risks:
|
|
2047
|
+
out["deployment_risks"] = output.deployment_risks
|
|
2048
|
+
if output.security_surface:
|
|
2049
|
+
out["security_surface"] = output.security_surface
|
|
2050
|
+
if output.mybatis:
|
|
2051
|
+
out["mybatis"] = output.mybatis
|
|
2052
|
+
if output.transactional_boundaries:
|
|
2053
|
+
out["transactional_boundaries"] = output.transactional_boundaries
|
|
2054
|
+
if output.spring_profiles_info:
|
|
2055
|
+
out["spring_profiles"] = output.spring_profiles_info
|
|
2056
|
+
if output.angular_analysis and (
|
|
2057
|
+
output.angular_analysis.get("component_count", 0) > 0
|
|
2058
|
+
or output.angular_analysis.get("angular_version")
|
|
2059
|
+
):
|
|
2060
|
+
out["angular_analysis"] = output.angular_analysis
|
|
2040
2061
|
# Delta-specific impact fields
|
|
2041
2062
|
if task == "delta":
|
|
2042
2063
|
if output.error_code:
|
|
@@ -2420,6 +2441,190 @@ def repo_ir_cmd(
|
|
|
2420
2441
|
_sys.stdout.write("\n")
|
|
2421
2442
|
|
|
2422
2443
|
|
|
2444
|
+
# ── endpoints ─────────────────────────────────────────────────────────────────
|
|
2445
|
+
|
|
2446
|
+
def _extract_java_endpoints(root: "Path") -> "dict[str, Any]":
|
|
2447
|
+
"""Extract REST endpoint surface from Java source files.
|
|
2448
|
+
|
|
2449
|
+
Scans all .java files for @RequestMapping/@GetMapping/@PostMapping/@PutMapping/
|
|
2450
|
+
@DeleteMapping/@PatchMapping and @M3FiltroSeguridad annotations.
|
|
2451
|
+
Returns JSON-serializable dict with endpoints list, total, and undocumented count.
|
|
2452
|
+
"""
|
|
2453
|
+
import re as _re
|
|
2454
|
+
from pathlib import Path as _Path
|
|
2455
|
+
|
|
2456
|
+
_HTTP_MAPPING_RE = _re.compile(
|
|
2457
|
+
r'@(Get|Post|Put|Delete|Patch|Request)Mapping\s*'
|
|
2458
|
+
r'(?:\(\s*(?:value\s*=\s*)?(?:"([^"]*)"|\{[^}]*\}|[^)]*)\s*\))?',
|
|
2459
|
+
)
|
|
2460
|
+
_CLASS_RE = _re.compile(r'(?:class|interface)\s+(\w+)')
|
|
2461
|
+
_METHOD_RE = _re.compile(
|
|
2462
|
+
r'(?:public|protected|private)\s+\S+\s+(\w+)\s*\(',
|
|
2463
|
+
)
|
|
2464
|
+
_FILTRO_RE = _re.compile(
|
|
2465
|
+
r'@M3FiltroSeguridad\s*\(\s*(?:nombreRecurso\s*=\s*)?["\']([^"\']+)["\']',
|
|
2466
|
+
)
|
|
2467
|
+
_CLASS_PATH_RE = _re.compile(
|
|
2468
|
+
r'@RequestMapping\s*\(\s*(?:value\s*=\s*)?["\']([^"\']+)["\']',
|
|
2469
|
+
)
|
|
2470
|
+
|
|
2471
|
+
_HTTP_METHOD_MAP = {
|
|
2472
|
+
"Get": "GET", "Post": "POST", "Put": "PUT",
|
|
2473
|
+
"Delete": "DELETE", "Patch": "PATCH", "Request": "GET",
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
endpoints: list[dict] = []
|
|
2477
|
+
seen: set[tuple] = set()
|
|
2478
|
+
|
|
2479
|
+
java_files = [
|
|
2480
|
+
p for p in root.rglob("*.java")
|
|
2481
|
+
if "/test/" not in str(p).replace("\\", "/")
|
|
2482
|
+
and "/tests/" not in str(p).replace("\\", "/")
|
|
2483
|
+
and "target/" not in str(p).replace("\\", "/")
|
|
2484
|
+
]
|
|
2485
|
+
|
|
2486
|
+
for java_file in java_files:
|
|
2487
|
+
try:
|
|
2488
|
+
content = java_file.read_text(encoding="utf-8", errors="replace")
|
|
2489
|
+
except OSError:
|
|
2490
|
+
continue
|
|
2491
|
+
|
|
2492
|
+
# Only process files with REST controller or mapping annotations
|
|
2493
|
+
if not any(x in content for x in ("@RestController", "@Controller", "@RequestMapping")):
|
|
2494
|
+
continue
|
|
2495
|
+
|
|
2496
|
+
try:
|
|
2497
|
+
rel_path = str(java_file.relative_to(root)).replace("\\", "/")
|
|
2498
|
+
except ValueError:
|
|
2499
|
+
rel_path = str(java_file).replace("\\", "/")
|
|
2500
|
+
|
|
2501
|
+
# Extract class name
|
|
2502
|
+
cls_m = _CLASS_RE.search(content)
|
|
2503
|
+
class_name = cls_m.group(1) if cls_m else java_file.stem
|
|
2504
|
+
|
|
2505
|
+
# Extract class-level base path from @RequestMapping on the class
|
|
2506
|
+
class_base = ""
|
|
2507
|
+
lines = content.splitlines()
|
|
2508
|
+
for i, line in enumerate(lines):
|
|
2509
|
+
if "@RequestMapping" in line and i < len(lines) - 1:
|
|
2510
|
+
# Check if next non-blank line is class declaration or it's on same block
|
|
2511
|
+
block = "\n".join(lines[max(0, i - 1): i + 5])
|
|
2512
|
+
if "class " in block or "interface " in block:
|
|
2513
|
+
path_m = _CLASS_PATH_RE.search(block)
|
|
2514
|
+
if path_m:
|
|
2515
|
+
class_base = path_m.group(1).rstrip("/")
|
|
2516
|
+
break
|
|
2517
|
+
|
|
2518
|
+
# Extract method-level endpoints
|
|
2519
|
+
# Parse line-by-line to associate annotations with methods
|
|
2520
|
+
pending_annotations: list[tuple[str, str]] = [] # (http_verb, path_suffix)
|
|
2521
|
+
pending_filtro: Optional[str] = None
|
|
2522
|
+
|
|
2523
|
+
for i, line in enumerate(lines):
|
|
2524
|
+
stripped = line.strip()
|
|
2525
|
+
|
|
2526
|
+
# Check for @M3FiltroSeguridad
|
|
2527
|
+
fm = _FILTRO_RE.search(stripped)
|
|
2528
|
+
if fm:
|
|
2529
|
+
pending_filtro = fm.group(1)
|
|
2530
|
+
continue
|
|
2531
|
+
|
|
2532
|
+
# Check for HTTP mapping annotations
|
|
2533
|
+
hm = _HTTP_MAPPING_RE.search(stripped)
|
|
2534
|
+
if hm:
|
|
2535
|
+
verb_key = hm.group(1)
|
|
2536
|
+
http_verb = _HTTP_METHOD_MAP.get(verb_key, "GET")
|
|
2537
|
+
path_suffix = (hm.group(2) or "").strip()
|
|
2538
|
+
pending_annotations.append((http_verb, path_suffix))
|
|
2539
|
+
continue
|
|
2540
|
+
|
|
2541
|
+
# Check for method declaration — flush pending annotations
|
|
2542
|
+
if pending_annotations and ("public " in stripped or "protected " in stripped):
|
|
2543
|
+
mm = _METHOD_RE.search(stripped)
|
|
2544
|
+
handler = mm.group(1) if mm else ""
|
|
2545
|
+
if handler and not handler.startswith("class"):
|
|
2546
|
+
for http_verb, path_suffix in pending_annotations:
|
|
2547
|
+
full_path = (class_base + "/" + path_suffix).replace("//", "/").rstrip("/") or "/"
|
|
2548
|
+
if not full_path.startswith("/"):
|
|
2549
|
+
full_path = "/" + full_path
|
|
2550
|
+
key = (class_name, handler, http_verb)
|
|
2551
|
+
if key not in seen:
|
|
2552
|
+
seen.add(key)
|
|
2553
|
+
entry: dict[str, Any] = {
|
|
2554
|
+
"method": http_verb,
|
|
2555
|
+
"path": full_path,
|
|
2556
|
+
"controller": class_name,
|
|
2557
|
+
"handler": handler,
|
|
2558
|
+
}
|
|
2559
|
+
if pending_filtro:
|
|
2560
|
+
entry["required_permission"] = pending_filtro
|
|
2561
|
+
endpoints.append(entry)
|
|
2562
|
+
pending_annotations = []
|
|
2563
|
+
pending_filtro = None
|
|
2564
|
+
continue
|
|
2565
|
+
|
|
2566
|
+
# Non-annotation, non-method line — reset if it's a closing brace or blank
|
|
2567
|
+
if stripped in ("}", "{", "") or stripped.startswith("//") or stripped.startswith("*"):
|
|
2568
|
+
if stripped == "}":
|
|
2569
|
+
pending_annotations = []
|
|
2570
|
+
pending_filtro = None
|
|
2571
|
+
|
|
2572
|
+
endpoints.sort(key=lambda e: (e.get("controller", ""), e.get("path", "")))
|
|
2573
|
+
undocumented = sum(1 for e in endpoints if "required_permission" not in e)
|
|
2574
|
+
|
|
2575
|
+
return {
|
|
2576
|
+
"endpoints": endpoints,
|
|
2577
|
+
"total": len(endpoints),
|
|
2578
|
+
"undocumented": undocumented,
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
|
|
2582
|
+
@app.command("endpoints")
|
|
2583
|
+
def endpoints_cmd(
|
|
2584
|
+
path: Path = typer.Argument(
|
|
2585
|
+
Path("."),
|
|
2586
|
+
help="Repository path to scan for REST endpoints (default: current directory)",
|
|
2587
|
+
),
|
|
2588
|
+
output_path: Optional[Path] = typer.Option(
|
|
2589
|
+
None, "--output", "-o",
|
|
2590
|
+
help="Write output to a file instead of stdout.",
|
|
2591
|
+
),
|
|
2592
|
+
) -> None:
|
|
2593
|
+
"""Extract REST API endpoint surface from Java source files.
|
|
2594
|
+
|
|
2595
|
+
\b
|
|
2596
|
+
Scans all @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping
|
|
2597
|
+
and @RequestMapping annotations. Extracts HTTP method, path, controller class,
|
|
2598
|
+
handler method, and @M3FiltroSeguridad permission resource name.
|
|
2599
|
+
|
|
2600
|
+
\b
|
|
2601
|
+
Examples:
|
|
2602
|
+
sourcecode endpoints .
|
|
2603
|
+
sourcecode endpoints /path/to/repo
|
|
2604
|
+
sourcecode endpoints . --output endpoints.json
|
|
2605
|
+
"""
|
|
2606
|
+
import sys as _sys
|
|
2607
|
+
|
|
2608
|
+
target = path.resolve()
|
|
2609
|
+
if not target.exists() or not target.is_dir():
|
|
2610
|
+
typer.echo(f"Error: '{target}' is not a valid directory.", err=True)
|
|
2611
|
+
raise typer.Exit(code=1)
|
|
2612
|
+
|
|
2613
|
+
data = _extract_java_endpoints(target)
|
|
2614
|
+
output = json.dumps(data, indent=2, ensure_ascii=False)
|
|
2615
|
+
|
|
2616
|
+
if output_path is not None:
|
|
2617
|
+
output_path.write_text(output, encoding="utf-8")
|
|
2618
|
+
typer.echo(
|
|
2619
|
+
f"Endpoints written to {output_path} ({data['total']} endpoints)",
|
|
2620
|
+
err=True,
|
|
2621
|
+
)
|
|
2622
|
+
else:
|
|
2623
|
+
_sys.stdout.buffer.write(output.encode("utf-8"))
|
|
2624
|
+
_sys.stdout.buffer.write(b"\n")
|
|
2625
|
+
_sys.stdout.buffer.flush()
|
|
2626
|
+
|
|
2627
|
+
|
|
2423
2628
|
# ── version ───────────────────────────────────────────────────────────────────
|
|
2424
2629
|
|
|
2425
2630
|
@app.command("version")
|
|
@@ -2466,10 +2671,6 @@ def analyze_cmd(
|
|
|
2466
2671
|
def mcp_serve() -> None:
|
|
2467
2672
|
"""Start the MCP server on stdio for AI agent integration.
|
|
2468
2673
|
|
|
2469
|
-
\b
|
|
2470
|
-
Requires the 'mcp' extra:
|
|
2471
|
-
pip install sourcecode[mcp]
|
|
2472
|
-
|
|
2473
2674
|
\b
|
|
2474
2675
|
Configure in your MCP client (e.g. Claude Desktop):
|
|
2475
2676
|
{
|
|
@@ -2487,15 +2688,7 @@ def mcp_serve() -> None:
|
|
|
2487
2688
|
level=logging.INFO,
|
|
2488
2689
|
format="[sourcecode-mcp] %(levelname)s %(message)s",
|
|
2489
2690
|
)
|
|
2490
|
-
|
|
2491
|
-
from sourcecode.mcp.server import mcp as _mcp
|
|
2492
|
-
except ImportError:
|
|
2493
|
-
typer.echo(
|
|
2494
|
-
"MCP support not available. Install with:\n"
|
|
2495
|
-
" pip install sourcecode[mcp]",
|
|
2496
|
-
err=True,
|
|
2497
|
-
)
|
|
2498
|
-
raise typer.Exit(code=1)
|
|
2691
|
+
from sourcecode.mcp.server import mcp as _mcp
|
|
2499
2692
|
|
|
2500
2693
|
log = logging.getLogger(__name__)
|
|
2501
2694
|
log.info("sourcecode-mcp starting (stdio transport)")
|
|
@@ -2513,21 +2706,44 @@ def mcp_serve() -> None:
|
|
|
2513
2706
|
@mcp_app.command("init")
|
|
2514
2707
|
def mcp_init(
|
|
2515
2708
|
yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
|
|
2709
|
+
target: Optional[str] = typer.Option(
|
|
2710
|
+
None,
|
|
2711
|
+
"--target",
|
|
2712
|
+
"-t",
|
|
2713
|
+
help="Target client: claude-desktop | cursor. Default: auto-detect all.",
|
|
2714
|
+
),
|
|
2516
2715
|
) -> None:
|
|
2517
2716
|
"""Setup MCP integration for Claude Desktop, Cursor, and other clients.
|
|
2518
2717
|
|
|
2519
2718
|
\b
|
|
2520
2719
|
Detects installed MCP clients, backs up their config files, and safely
|
|
2521
2720
|
inserts the sourcecode server entry. Fully idempotent — safe to re-run.
|
|
2721
|
+
|
|
2722
|
+
\b
|
|
2723
|
+
Examples:
|
|
2724
|
+
sourcecode mcp init
|
|
2725
|
+
sourcecode mcp init --target claude-desktop
|
|
2726
|
+
sourcecode mcp init --target cursor --yes
|
|
2522
2727
|
"""
|
|
2523
|
-
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
2728
|
+
from sourcecode.mcp.onboarding.detector import detect_clients, is_client_running
|
|
2524
2729
|
from sourcecode.mcp.onboarding.planner import build_install_plan
|
|
2525
2730
|
from sourcecode.mcp.onboarding import backup, applier
|
|
2526
2731
|
|
|
2527
2732
|
typer.echo("Detecting MCP clients...")
|
|
2528
2733
|
typer.echo("")
|
|
2529
2734
|
|
|
2530
|
-
|
|
2735
|
+
all_clients = detect_clients()
|
|
2736
|
+
|
|
2737
|
+
if target:
|
|
2738
|
+
target_slug = target.lower()
|
|
2739
|
+
clients = [c for c in all_clients if c.slug == target_slug]
|
|
2740
|
+
if not clients:
|
|
2741
|
+
valid = ", ".join(c.slug for c in all_clients)
|
|
2742
|
+
typer.echo(f"Unknown target '{target}'. Valid: {valid}", err=True)
|
|
2743
|
+
raise typer.Exit(code=1)
|
|
2744
|
+
else:
|
|
2745
|
+
clients = all_clients
|
|
2746
|
+
|
|
2531
2747
|
if not clients:
|
|
2532
2748
|
typer.echo("No MCP clients found on this system.")
|
|
2533
2749
|
typer.echo("")
|
|
@@ -2603,36 +2819,81 @@ def mcp_init(
|
|
|
2603
2819
|
typer.echo("MCP integration active.")
|
|
2604
2820
|
typer.echo("")
|
|
2605
2821
|
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2822
|
+
# Post-write: validate config and warn if client not running
|
|
2823
|
+
for a in actionable:
|
|
2824
|
+
if not is_client_running(a.client):
|
|
2825
|
+
typer.echo(
|
|
2826
|
+
f" ⚠ Config written but {a.client.name} is not running. "
|
|
2827
|
+
f"Start {a.client.name} and run sourcecode mcp status to verify.",
|
|
2828
|
+
err=False,
|
|
2829
|
+
)
|
|
2830
|
+
else:
|
|
2831
|
+
restart_msg = "" if a.will_create_file else f" Restart {a.client.name} to apply."
|
|
2832
|
+
typer.echo(f" ✓ {a.client.name} is running.{restart_msg}")
|
|
2833
|
+
|
|
2834
|
+
typer.echo("")
|
|
2609
2835
|
typer.echo(" Remove: sourcecode mcp remove")
|
|
2610
2836
|
|
|
2611
2837
|
|
|
2612
2838
|
@mcp_app.command("status")
|
|
2613
2839
|
def mcp_status() -> None:
|
|
2614
|
-
"""Show MCP integration status
|
|
2615
|
-
from sourcecode.mcp.onboarding.detector import detect_clients
|
|
2840
|
+
"""Show MCP integration status: dependencies, config files, and connectivity."""
|
|
2841
|
+
from sourcecode.mcp.onboarding.detector import detect_clients, is_client_running
|
|
2616
2842
|
from sourcecode.mcp.onboarding import applier
|
|
2617
2843
|
|
|
2618
|
-
|
|
2619
|
-
|
|
2844
|
+
sep = "─" * 46
|
|
2845
|
+
|
|
2846
|
+
typer.echo("MCP Status")
|
|
2847
|
+
typer.echo(sep)
|
|
2848
|
+
|
|
2849
|
+
# Stage 1: Dependencies
|
|
2850
|
+
try:
|
|
2851
|
+
import mcp as _mcp_pkg # noqa: F401
|
|
2852
|
+
typer.echo("Dependencies ✓ installed")
|
|
2853
|
+
except ImportError:
|
|
2854
|
+
typer.echo("Dependencies ✗ missing")
|
|
2855
|
+
typer.echo(" Fix: pip install sourcecode[mcp]")
|
|
2620
2856
|
typer.echo("")
|
|
2621
2857
|
|
|
2858
|
+
clients = detect_clients()
|
|
2622
2859
|
if not clients:
|
|
2623
2860
|
typer.echo(" No MCP clients detected on this system.")
|
|
2861
|
+
typer.echo(sep)
|
|
2862
|
+
typer.echo(" Setup: sourcecode mcp init")
|
|
2624
2863
|
raise typer.Exit(code=0)
|
|
2625
2864
|
|
|
2865
|
+
# Stage 2: Config files
|
|
2866
|
+
typer.echo("Config files")
|
|
2626
2867
|
for client in clients:
|
|
2627
2868
|
if not client.app_installed:
|
|
2628
|
-
typer.echo(f"
|
|
2869
|
+
typer.echo(f" {client.name:<20} ✗ not found")
|
|
2870
|
+
typer.echo(f" Expected: {client.config_path}")
|
|
2871
|
+
typer.echo(f" Fix: sourcecode mcp init --target {client.slug}")
|
|
2629
2872
|
continue
|
|
2630
2873
|
config = applier.read_config(client.config_path)
|
|
2631
2874
|
if applier.is_installed(config):
|
|
2632
|
-
typer.echo(f"
|
|
2875
|
+
typer.echo(f" {client.name:<20} ✓ configured {client.config_path}")
|
|
2633
2876
|
else:
|
|
2634
|
-
typer.echo(f"
|
|
2877
|
+
typer.echo(f" {client.name:<20} ✗ not configured")
|
|
2878
|
+
typer.echo(f" Fix: sourcecode mcp init --target {client.slug}")
|
|
2635
2879
|
typer.echo("")
|
|
2880
|
+
|
|
2881
|
+
# Stage 3: Connectivity
|
|
2882
|
+
typer.echo("Connectivity")
|
|
2883
|
+
any_installed = any(c.app_installed for c in clients)
|
|
2884
|
+
if not any_installed:
|
|
2885
|
+
typer.echo(" (no clients to check)")
|
|
2886
|
+
else:
|
|
2887
|
+
for client in clients:
|
|
2888
|
+
if not client.app_installed:
|
|
2889
|
+
continue
|
|
2890
|
+
if is_client_running(client):
|
|
2891
|
+
typer.echo(f" {client.name:<20} ✓ running")
|
|
2892
|
+
else:
|
|
2893
|
+
typer.echo(f" {client.name:<20} ✗ not running")
|
|
2894
|
+
typer.echo(f" Fix: open {client.name}, then run sourcecode mcp status")
|
|
2895
|
+
|
|
2896
|
+
typer.echo(sep)
|
|
2636
2897
|
typer.echo(" Setup: sourcecode mcp init")
|
|
2637
2898
|
typer.echo(" Remove: sourcecode mcp remove")
|
|
2638
2899
|
|
|
@@ -175,7 +175,7 @@ class ConfidenceAnalyzer:
|
|
|
175
175
|
if dep_summary is None or not dep_summary.requested:
|
|
176
176
|
gaps.append(AnalysisGap(
|
|
177
177
|
area="dependencies",
|
|
178
|
-
reason="Dependencies not analyzed —
|
|
178
|
+
reason="Dependencies not analyzed — use the full analyze command with dependency flags for complete context",
|
|
179
179
|
impact="medium",
|
|
180
180
|
))
|
|
181
181
|
elif dep_summary.requested and dep_summary.total_count == 0:
|
|
@@ -2,25 +2,43 @@
|
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
4
|
import os
|
|
5
|
+
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
from dataclasses import dataclass
|
|
7
8
|
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List
|
|
8
10
|
|
|
9
11
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
|
|
16
|
-
|
|
12
|
+
_CLIENT_REGISTRY: List[Dict[str, Any]] = [
|
|
13
|
+
{
|
|
14
|
+
"name": "Claude Desktop",
|
|
15
|
+
"slug": "claude-desktop",
|
|
16
|
+
"paths": {
|
|
17
|
+
"darwin": "~/Library/Application Support/Claude/claude_desktop_config.json",
|
|
18
|
+
"linux": "~/.config/Claude/claude_desktop_config.json",
|
|
19
|
+
"win32": "{APPDATA}/Claude/claude_desktop_config.json",
|
|
20
|
+
},
|
|
21
|
+
"process": {
|
|
22
|
+
"darwin": "Claude",
|
|
23
|
+
"linux": "claude-desktop",
|
|
24
|
+
"win32": "Claude",
|
|
25
|
+
},
|
|
17
26
|
},
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
27
|
+
{
|
|
28
|
+
"name": "Cursor",
|
|
29
|
+
"slug": "cursor",
|
|
30
|
+
"paths": {
|
|
31
|
+
"darwin": "~/.cursor/mcp.json",
|
|
32
|
+
"linux": "~/.cursor/mcp.json",
|
|
33
|
+
"win32": "{USERPROFILE}/.cursor/mcp.json",
|
|
34
|
+
},
|
|
35
|
+
"process": {
|
|
36
|
+
"darwin": "Cursor",
|
|
37
|
+
"linux": "cursor",
|
|
38
|
+
"win32": "Cursor",
|
|
39
|
+
},
|
|
22
40
|
},
|
|
23
|
-
|
|
41
|
+
]
|
|
24
42
|
|
|
25
43
|
|
|
26
44
|
@dataclass(frozen=True)
|
|
@@ -28,6 +46,8 @@ class MCPClient:
|
|
|
28
46
|
name: str
|
|
29
47
|
config_path: Path
|
|
30
48
|
app_installed: bool # True if the config file (or its parent dir) exists
|
|
49
|
+
process_name: str # OS process name for connectivity check
|
|
50
|
+
slug: str # --target identifier (e.g. "claude-desktop")
|
|
31
51
|
|
|
32
52
|
|
|
33
53
|
def _resolve(template: str) -> Path:
|
|
@@ -44,16 +64,41 @@ def detect_clients() -> list[MCPClient]:
|
|
|
44
64
|
"""Return all known MCP clients with their resolved config paths."""
|
|
45
65
|
plat = sys.platform
|
|
46
66
|
clients: list[MCPClient] = []
|
|
47
|
-
for
|
|
48
|
-
|
|
67
|
+
for entry in _CLIENT_REGISTRY:
|
|
68
|
+
paths: Dict[str, str] = entry["paths"]
|
|
69
|
+
processes: Dict[str, str] = entry["process"]
|
|
70
|
+
template = paths.get(plat) or paths.get("linux", "")
|
|
49
71
|
if not template:
|
|
50
72
|
continue
|
|
51
73
|
config_path = _resolve(template)
|
|
52
|
-
# Consider client "installed" if its config file OR parent app dir exists.
|
|
53
74
|
app_installed = config_path.exists() or config_path.parent.exists()
|
|
75
|
+
process_name = processes.get(plat) or processes.get("linux", "")
|
|
54
76
|
clients.append(MCPClient(
|
|
55
|
-
name=name,
|
|
77
|
+
name=entry["name"],
|
|
56
78
|
config_path=config_path,
|
|
57
79
|
app_installed=app_installed,
|
|
80
|
+
process_name=process_name,
|
|
81
|
+
slug=entry["slug"],
|
|
58
82
|
))
|
|
59
83
|
return clients
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_client_running(client: MCPClient) -> bool:
|
|
87
|
+
"""True if the client process is currently running."""
|
|
88
|
+
if not client.process_name:
|
|
89
|
+
return False
|
|
90
|
+
try:
|
|
91
|
+
if sys.platform == "win32":
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
["tasklist", "/fi", f"imagename eq {client.process_name}.exe"],
|
|
94
|
+
capture_output=True, text=True, timeout=5,
|
|
95
|
+
)
|
|
96
|
+
return client.process_name.lower() in result.stdout.lower()
|
|
97
|
+
else:
|
|
98
|
+
result = subprocess.run(
|
|
99
|
+
["pgrep", "-x", client.process_name],
|
|
100
|
+
capture_output=True, timeout=5,
|
|
101
|
+
)
|
|
102
|
+
return result.returncode == 0
|
|
103
|
+
except Exception:
|
|
104
|
+
return False
|
sourcecode/mcp/runner.py
CHANGED
|
@@ -6,14 +6,18 @@ lookup, no process fork, no stdout encoding issues.
|
|
|
6
6
|
"""
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import json
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
9
12
|
from typer.testing import CliRunner
|
|
10
13
|
|
|
11
14
|
_runner = CliRunner()
|
|
12
15
|
|
|
13
16
|
|
|
14
|
-
def run_command(args: list[str]) ->
|
|
15
|
-
"""Invoke a sourcecode CLI command in-process and return
|
|
17
|
+
def run_command(args: list[str]) -> Any:
|
|
18
|
+
"""Invoke a sourcecode CLI command in-process and return parsed output.
|
|
16
19
|
|
|
20
|
+
Returns parsed JSON dict when output is valid JSON, else the raw string.
|
|
17
21
|
Raises RuntimeError on non-zero exit or empty output.
|
|
18
22
|
"""
|
|
19
23
|
from sourcecode.cli import _detected_path, _preprocess_args, app
|
|
@@ -37,4 +41,7 @@ def run_command(args: list[str]) -> str:
|
|
|
37
41
|
f"Args: {args}"
|
|
38
42
|
)
|
|
39
43
|
|
|
40
|
-
|
|
44
|
+
try:
|
|
45
|
+
return json.loads(output)
|
|
46
|
+
except json.JSONDecodeError:
|
|
47
|
+
return output
|
sourcecode/mcp/server.py
CHANGED
|
@@ -4,27 +4,24 @@ Exposes sourcecode capabilities as MCP tools. Each tool maps to a CLI command
|
|
|
4
4
|
and delegates execution to the in-process runner — no subprocess, no binary
|
|
5
5
|
lookup, same process as the CLI.
|
|
6
6
|
|
|
7
|
-
All tools return
|
|
8
|
-
{"success": bool, "data": str | None, "error": {"code": str, "message": str} | None}
|
|
7
|
+
All tools return:
|
|
8
|
+
{"success": bool, "data": dict | str | None, "error": {"code": str, "message": str} | None}
|
|
9
|
+
data is the parsed JSON object from the CLI output, not a shell string.
|
|
9
10
|
"""
|
|
10
11
|
from __future__ import annotations
|
|
11
12
|
|
|
12
|
-
|
|
13
|
+
import os
|
|
14
|
+
from typing import Any
|
|
13
15
|
|
|
14
16
|
from mcp.server.fastmcp import FastMCP
|
|
15
17
|
|
|
18
|
+
from sourcecode import __version__
|
|
16
19
|
from sourcecode.mcp.runner import run_command
|
|
17
20
|
|
|
18
|
-
mcp = FastMCP("sourcecode")
|
|
21
|
+
mcp = FastMCP("sourcecode", version=__version__)
|
|
19
22
|
|
|
20
|
-
_PREPARE_CONTEXT_TASKS = frozenset({
|
|
21
|
-
"delta", "review-pr", "fix-bug", "onboard",
|
|
22
|
-
"explain", "refactor", "generate-tests",
|
|
23
|
-
})
|
|
24
|
-
_TELEMETRY_ACTIONS = frozenset({"status", "enable", "disable"})
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
def _ok(data: str) -> dict:
|
|
24
|
+
def _ok(data: Any) -> dict:
|
|
28
25
|
return {"success": True, "data": data, "error": None}
|
|
29
26
|
|
|
30
27
|
|
|
@@ -40,108 +37,120 @@ def _execute(args: list[str]) -> dict:
|
|
|
40
37
|
|
|
41
38
|
|
|
42
39
|
@mcp.tool()
|
|
43
|
-
def
|
|
44
|
-
"""
|
|
40
|
+
def get_compact_context(repo_path: str = ".") -> dict:
|
|
41
|
+
"""High-signal summary of a repository (~1000-3000 tokens).
|
|
45
42
|
|
|
46
|
-
Maps to: sourcecode <
|
|
43
|
+
Maps to: sourcecode <repo_path> --compact
|
|
44
|
+
Returns: stacks, entry points, dependency summary, confidence, gaps.
|
|
45
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
47
46
|
"""
|
|
48
|
-
if not isinstance(
|
|
49
|
-
return _err("
|
|
50
|
-
|
|
51
|
-
return _err("git_context must be boolean", "INVALID_ARGUMENT")
|
|
52
|
-
args = [path, "--compact"]
|
|
53
|
-
if git_context:
|
|
54
|
-
args.append("--git-context")
|
|
55
|
-
return _execute(args)
|
|
47
|
+
if not isinstance(repo_path, str):
|
|
48
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
49
|
+
return _execute([repo_path, "--compact"])
|
|
56
50
|
|
|
57
51
|
|
|
58
52
|
@mcp.tool()
|
|
59
|
-
def
|
|
53
|
+
def get_agent_context(repo_path: str = ".") -> dict:
|
|
60
54
|
"""Agent-optimised analysis: identity, entry points, dependencies, gaps.
|
|
61
55
|
|
|
62
|
-
Maps to: sourcecode <
|
|
56
|
+
Maps to: sourcecode <repo_path> --agent
|
|
57
|
+
Returns: structured noise-free JSON for AI agents.
|
|
58
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
63
59
|
"""
|
|
64
|
-
if not isinstance(
|
|
65
|
-
return _err("
|
|
66
|
-
|
|
67
|
-
return _err("git_context must be boolean", "INVALID_ARGUMENT")
|
|
68
|
-
args = [path, "--agent"]
|
|
69
|
-
if git_context:
|
|
70
|
-
args.append("--git-context")
|
|
71
|
-
return _execute(args)
|
|
60
|
+
if not isinstance(repo_path, str):
|
|
61
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
62
|
+
return _execute([repo_path, "--agent"])
|
|
72
63
|
|
|
73
64
|
|
|
74
65
|
@mcp.tool()
|
|
75
|
-
def
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"""Task-specific context for AI coding agents.
|
|
83
|
-
|
|
84
|
-
Maps to: sourcecode prepare-context <task> <path>
|
|
85
|
-
|
|
86
|
-
task must be one of:
|
|
87
|
-
explain Architecture, entry points, key dependencies
|
|
88
|
-
fix-bug Risk-ranked files, suspected areas, annotations
|
|
89
|
-
refactor Structural issues, improvement opportunities
|
|
90
|
-
generate-tests Untested source files, test gap analysis
|
|
91
|
-
onboard Full project context for new agents/developers
|
|
92
|
-
review-pr PR diff with runtime signals and security impact
|
|
93
|
-
delta Incremental context: git-changed files only
|
|
66
|
+
def get_endpoints(repo_path: str = ".") -> dict:
|
|
67
|
+
"""REST API endpoint surface extraction from Java source files.
|
|
68
|
+
|
|
69
|
+
Maps to: sourcecode endpoints <repo_path>
|
|
70
|
+
Returns: endpoints list with method, path, controller, handler, required_permission;
|
|
71
|
+
total count and undocumented count.
|
|
72
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
94
73
|
"""
|
|
95
|
-
if
|
|
96
|
-
return _err(
|
|
97
|
-
|
|
98
|
-
"INVALID_ARGUMENT",
|
|
99
|
-
)
|
|
100
|
-
if not isinstance(path, str):
|
|
101
|
-
return _err("path must be a string", "INVALID_ARGUMENT")
|
|
102
|
-
return _execute(["prepare-context", task, path])
|
|
74
|
+
if not isinstance(repo_path, str):
|
|
75
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
76
|
+
return _execute(["endpoints", repo_path])
|
|
103
77
|
|
|
104
78
|
|
|
105
79
|
@mcp.tool()
|
|
106
|
-
def
|
|
107
|
-
"""
|
|
80
|
+
def get_module_context(repo_path: str = ".", module: str = "") -> dict:
|
|
81
|
+
"""Compact analysis of a specific module or subdirectory within a repository.
|
|
108
82
|
|
|
109
|
-
Maps to: sourcecode
|
|
110
|
-
|
|
83
|
+
Maps to: sourcecode <repo_path>/<module> --compact
|
|
84
|
+
repo_path: absolute path to the repository root.
|
|
85
|
+
module: subdirectory name relative to repo_path (e.g. 'src/auth', 'api', 'core').
|
|
111
86
|
"""
|
|
112
|
-
if not isinstance(
|
|
113
|
-
return _err("
|
|
114
|
-
|
|
87
|
+
if not isinstance(repo_path, str):
|
|
88
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
89
|
+
if not isinstance(module, str) or not module.strip():
|
|
90
|
+
return _err("module must be a non-empty string", "INVALID_ARGUMENT")
|
|
91
|
+
module_path = os.path.join(repo_path, module)
|
|
92
|
+
return _execute([module_path, "--compact"])
|
|
115
93
|
|
|
116
94
|
|
|
117
95
|
@mcp.tool()
|
|
118
|
-
def
|
|
119
|
-
"""
|
|
96
|
+
def get_delta(repo_path: str = ".", since: str = "HEAD~1") -> dict:
|
|
97
|
+
"""Incremental context: git-changed files since a reference commit.
|
|
120
98
|
|
|
121
|
-
Maps to: sourcecode
|
|
99
|
+
Maps to: sourcecode prepare-context delta <repo_path> --since <since>
|
|
100
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
101
|
+
since: git ref to diff against (e.g. HEAD~3, main, origin/main).
|
|
122
102
|
"""
|
|
123
|
-
|
|
103
|
+
if not isinstance(repo_path, str):
|
|
104
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
105
|
+
if not isinstance(since, str) or not since.strip():
|
|
106
|
+
return _err("since must be a non-empty git ref", "INVALID_ARGUMENT")
|
|
107
|
+
return _execute(["prepare-context", "delta", repo_path, "--since", since])
|
|
124
108
|
|
|
125
109
|
|
|
126
110
|
@mcp.tool()
|
|
127
|
-
def
|
|
128
|
-
"""
|
|
111
|
+
def get_ir_summary(repo_path: str = ".") -> dict:
|
|
112
|
+
"""Deterministic symbol-level IR summary for Java repositories.
|
|
129
113
|
|
|
130
|
-
Maps to: sourcecode
|
|
114
|
+
Maps to: sourcecode repo-ir <repo_path> --summary-only
|
|
115
|
+
Returns: analysis summary, impact, and change_set — omits full graph nodes/edges.
|
|
116
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
131
117
|
"""
|
|
132
|
-
|
|
118
|
+
if not isinstance(repo_path, str):
|
|
119
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
120
|
+
return _execute(["repo-ir", repo_path, "--summary-only"])
|
|
133
121
|
|
|
134
122
|
|
|
135
123
|
@mcp.tool()
|
|
136
|
-
def
|
|
137
|
-
"""
|
|
124
|
+
def fix_bug_context(repo_path: str = ".", symptom: str = "") -> dict:
|
|
125
|
+
"""Risk-ranked files for bug investigation, optionally focused by symptom.
|
|
126
|
+
|
|
127
|
+
Maps to: sourcecode prepare-context fix-bug <repo_path> [--symptom <symptom>]
|
|
128
|
+
Includes compact_base: security_surface, transactional_boundaries, spring_profiles.
|
|
129
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
130
|
+
symptom: optional error message or class name to focus the file ranking
|
|
131
|
+
(e.g. "NullPointerException in EstructuraRrHhRestController").
|
|
132
|
+
"""
|
|
133
|
+
if not isinstance(repo_path, str):
|
|
134
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
135
|
+
args = ["prepare-context", "fix-bug", repo_path]
|
|
136
|
+
if symptom and isinstance(symptom, str) and symptom.strip():
|
|
137
|
+
args.extend(["--symptom", symptom.strip()])
|
|
138
|
+
return _execute(args)
|
|
139
|
+
|
|
138
140
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
+
@mcp.tool()
|
|
142
|
+
def review_pr_context(repo_path: str = ".", since: str = "") -> dict:
|
|
143
|
+
"""Execution paths and risk analysis for changed files in a pull request.
|
|
144
|
+
|
|
145
|
+
Maps to: sourcecode prepare-context review-pr <repo_path> [--since <since>]
|
|
146
|
+
Returns: compact_base + execution_paths (diff-scoped) + hotspots for changed files.
|
|
147
|
+
repo_path: absolute path to the repository (default: current working directory).
|
|
148
|
+
since: git ref to diff against (e.g. HEAD~3, main, origin/main).
|
|
149
|
+
If omitted, diffs against uncommitted changes or HEAD~1 fallback.
|
|
141
150
|
"""
|
|
142
|
-
if
|
|
143
|
-
return _err(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
return _execute(
|
|
151
|
+
if not isinstance(repo_path, str):
|
|
152
|
+
return _err("repo_path must be a string", "INVALID_ARGUMENT")
|
|
153
|
+
args = ["prepare-context", "review-pr", repo_path]
|
|
154
|
+
if since and isinstance(since, str) and since.strip():
|
|
155
|
+
args.extend(["--since", since.strip()])
|
|
156
|
+
return _execute(args)
|
sourcecode/prepare_context.py
CHANGED
|
@@ -322,7 +322,7 @@ class TaskOutput:
|
|
|
322
322
|
relevant_files: list[RelevantFile]
|
|
323
323
|
suspected_areas: list[str]
|
|
324
324
|
improvement_opportunities: list[str]
|
|
325
|
-
test_gaps: list[str]
|
|
325
|
+
test_gaps: list # list[str] for non-Java; list[dict] for Java (has path/public_method_count/has_spring_annotations)
|
|
326
326
|
key_dependencies: list[dict[str, Any]]
|
|
327
327
|
code_notes_summary: Optional[dict[str, Any]]
|
|
328
328
|
limitations: list[str]
|
|
@@ -378,6 +378,15 @@ class TaskOutput:
|
|
|
378
378
|
uncommitted_changes: list[dict] = field(default_factory=list)
|
|
379
379
|
# transparency: explicit diff scope for every command
|
|
380
380
|
analysis_scope: dict = field(default_factory=dict)
|
|
381
|
+
# compact_base fields — enriched in all prepare-context tasks (Fix #1)
|
|
382
|
+
security_surface: Optional[dict] = None
|
|
383
|
+
mybatis: Optional[dict] = None
|
|
384
|
+
transactional_boundaries: Optional[dict] = None
|
|
385
|
+
spring_profiles_info: Optional[dict] = None
|
|
386
|
+
angular_analysis: Optional[dict] = None
|
|
387
|
+
deployment_risks: list[str] = field(default_factory=list)
|
|
388
|
+
deployment: Optional[dict] = None
|
|
389
|
+
entry_points_structured: Optional[dict] = None
|
|
381
390
|
|
|
382
391
|
|
|
383
392
|
@dataclass
|
|
@@ -902,6 +911,53 @@ class TaskContextBuilder:
|
|
|
902
911
|
from sourcecode.context_summarizer import ContextSummarizer
|
|
903
912
|
sm.context_summary = ContextSummarizer(self.root).generate(sm)
|
|
904
913
|
|
|
914
|
+
# ── 3b. Compact-base enrichment (Fix #1) ───────────────────────────
|
|
915
|
+
# Enrich sm with Java-specific fields — same as main CLI pipeline
|
|
916
|
+
_java_stack_cb = next((s for s in stacks if s.stack == "java"), None)
|
|
917
|
+
if _java_stack_cb is not None:
|
|
918
|
+
sm.language_version = getattr(_java_stack_cb, "language_version", None) or None
|
|
919
|
+
sm.spring_profiles = getattr(_java_stack_cb, "spring_profiles", []) or []
|
|
920
|
+
sm.app_server_hint = getattr(_java_stack_cb, "app_server_hint", None) or None
|
|
921
|
+
sm.packaging = getattr(_java_stack_cb, "packaging", None) or None
|
|
922
|
+
|
|
923
|
+
# Import serializer helpers — same functions used by --compact
|
|
924
|
+
from sourcecode.serializer import (
|
|
925
|
+
_security_surface_from_eps as _cb_sec_fn,
|
|
926
|
+
_mybatis_pairing as _cb_mybatis_fn,
|
|
927
|
+
_transactional_summary as _cb_trans_fn,
|
|
928
|
+
_spring_profiles_context as _cb_spring_fn,
|
|
929
|
+
_angular_analysis as _cb_angular_fn,
|
|
930
|
+
_project_deployment_risks as _cb_deploy_risks_fn,
|
|
931
|
+
_bootstrap_structured as _cb_bootstrap_fn,
|
|
932
|
+
_spring_boot_version as _cb_sbver_fn,
|
|
933
|
+
_jndi_datasources as _cb_jndi_fn,
|
|
934
|
+
)
|
|
935
|
+
|
|
936
|
+
_cb_security_surface = _cb_sec_fn(entry_points, root=self.root, file_paths=all_paths)
|
|
937
|
+
_cb_mybatis = _cb_mybatis_fn(sm)
|
|
938
|
+
_cb_transactional = _cb_trans_fn(sm)
|
|
939
|
+
_cb_spring_profiles = _cb_spring_fn(sm)
|
|
940
|
+
_cb_angular = _cb_angular_fn(sm)
|
|
941
|
+
_cb_deploy_risks = _cb_deploy_risks_fn(sm)
|
|
942
|
+
_cb_bootstrap = _cb_bootstrap_fn(entry_points)
|
|
943
|
+
|
|
944
|
+
_cb_sb_ver = _cb_sbver_fn(sm)
|
|
945
|
+
_cb_deployment: Optional[dict] = None
|
|
946
|
+
_cb_packaging = getattr(sm, "packaging", None)
|
|
947
|
+
_cb_app_server = getattr(sm, "app_server_hint", None)
|
|
948
|
+
if _cb_sb_ver or _cb_packaging or _cb_app_server:
|
|
949
|
+
_cb_deployment = {}
|
|
950
|
+
if _cb_sb_ver:
|
|
951
|
+
_cb_deployment["spring_boot_version"] = _cb_sb_ver
|
|
952
|
+
if _cb_packaging:
|
|
953
|
+
_cb_deployment["packaging"] = _cb_packaging
|
|
954
|
+
if _cb_app_server:
|
|
955
|
+
_cb_deployment["app_server_hint"] = _cb_app_server
|
|
956
|
+
_cb_jndi = _cb_jndi_fn(sm)
|
|
957
|
+
if _cb_jndi:
|
|
958
|
+
_cb_deployment = _cb_deployment or {}
|
|
959
|
+
_cb_deployment["jndi_datasources"] = _cb_jndi
|
|
960
|
+
|
|
905
961
|
# ── 4. Dependencies ────────────────────────────────────────────────
|
|
906
962
|
key_dependencies: list[dict[str, Any]] = []
|
|
907
963
|
limitations: list[str] = []
|
|
@@ -1647,42 +1703,107 @@ class TaskContextBuilder:
|
|
|
1647
1703
|
symptom_hint = _fe_redirect
|
|
1648
1704
|
|
|
1649
1705
|
# ── 7. Test gaps (generate-tests only) ────────────────────────────
|
|
1650
|
-
test_gaps: list
|
|
1706
|
+
test_gaps: list = []
|
|
1651
1707
|
if task_name == "generate-tests" and not fast:
|
|
1652
|
-
|
|
1653
|
-
# Java
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1708
|
+
if _is_java:
|
|
1709
|
+
# Java-aware algorithm (Fix #2): find Service/RestController/Repository/Mapper
|
|
1710
|
+
# files with no matching test pair in src/test/**
|
|
1711
|
+
_JAVA_TARGET_SUFFIXES = (
|
|
1712
|
+
"Service.java", "RestController.java",
|
|
1713
|
+
"Repository.java", "Mapper.java",
|
|
1714
|
+
)
|
|
1715
|
+
# Build set of test stems (FooTest → Foo, FooIT → Foo, etc.)
|
|
1716
|
+
_java_test_stems: set[str] = set()
|
|
1717
|
+
for _tp in all_paths:
|
|
1718
|
+
if not _tp.endswith(".java"):
|
|
1719
|
+
continue
|
|
1720
|
+
if not self._is_test(_tp):
|
|
1721
|
+
continue
|
|
1722
|
+
_ts = Path(_tp).stem
|
|
1723
|
+
for _suf in ("Test", "IT", "Tests", "Spec"):
|
|
1724
|
+
if _ts.endswith(_suf):
|
|
1725
|
+
_ts = _ts[: -len(_suf)]
|
|
1726
|
+
break
|
|
1727
|
+
if _ts.startswith("Test") and len(_ts) > 4 and _ts[4].isupper():
|
|
1728
|
+
_ts = _ts[4:]
|
|
1729
|
+
_java_test_stems.add(_ts)
|
|
1730
|
+
|
|
1731
|
+
_java_candidates: list[dict] = []
|
|
1732
|
+
for _p in all_paths:
|
|
1733
|
+
if not any(_p.endswith(_s) for _s in _JAVA_TARGET_SUFFIXES):
|
|
1734
|
+
continue
|
|
1735
|
+
if self._is_test(_p):
|
|
1736
|
+
continue
|
|
1737
|
+
_pnorm = _p.replace("\\", "/")
|
|
1738
|
+
if "src/main/resources" in _pnorm or "target/" in _pnorm:
|
|
1739
|
+
continue
|
|
1740
|
+
if Path(_p).stem in _java_test_stems:
|
|
1741
|
+
continue
|
|
1742
|
+
_pub_count = 0
|
|
1743
|
+
_ann_count = 0
|
|
1744
|
+
try:
|
|
1745
|
+
_content = (self.root / _p).read_text(
|
|
1746
|
+
encoding="utf-8", errors="replace"
|
|
1747
|
+
)[:16000]
|
|
1748
|
+
_pub_count = _content.count("public ")
|
|
1749
|
+
_ann_count = (
|
|
1750
|
+
_content.count("@Transactional")
|
|
1751
|
+
+ _content.count("@RequestMapping")
|
|
1752
|
+
+ _content.count("@GetMapping")
|
|
1753
|
+
+ _content.count("@PostMapping")
|
|
1754
|
+
+ _content.count("@PutMapping")
|
|
1755
|
+
+ _content.count("@DeleteMapping")
|
|
1756
|
+
)
|
|
1757
|
+
except OSError:
|
|
1758
|
+
pass
|
|
1759
|
+
_java_candidates.append({
|
|
1760
|
+
"path": _p,
|
|
1761
|
+
"public_method_count": _pub_count,
|
|
1762
|
+
"has_spring_annotations": _ann_count > 0,
|
|
1763
|
+
"_rank": _pub_count + _ann_count * 2,
|
|
1764
|
+
})
|
|
1765
|
+
|
|
1766
|
+
_java_candidates.sort(key=lambda x: -x["_rank"])
|
|
1767
|
+
test_gaps = [
|
|
1768
|
+
{
|
|
1769
|
+
"path": c["path"],
|
|
1770
|
+
"public_method_count": c["public_method_count"],
|
|
1771
|
+
"has_spring_annotations": c["has_spring_annotations"],
|
|
1772
|
+
}
|
|
1773
|
+
for c in _java_candidates
|
|
1774
|
+
]
|
|
1775
|
+
else:
|
|
1776
|
+
# Non-Java algorithm (unchanged)
|
|
1777
|
+
def _normalize_test_stem(stem: str) -> str:
|
|
1778
|
+
if stem.endswith("Tests"):
|
|
1779
|
+
return stem[:-5]
|
|
1780
|
+
if stem.endswith("Test"):
|
|
1781
|
+
return stem[:-4]
|
|
1782
|
+
if stem.startswith("Test") and len(stem) > 4 and stem[4].isupper():
|
|
1783
|
+
return stem[4:]
|
|
1784
|
+
return stem.removeprefix("test_").removesuffix("_test")
|
|
1785
|
+
|
|
1786
|
+
_CONFIG_EXCLUDE_PATTERNS = (
|
|
1787
|
+
".eslintrc", ".prettierrc", "eslint.config",
|
|
1788
|
+
"karma.conf", "jest.config", "babel.config",
|
|
1789
|
+
"webpack.config", "vite.config", "rollup.config",
|
|
1790
|
+
"tsconfig", "angular.json", ".claude/",
|
|
1791
|
+
)
|
|
1671
1792
|
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1793
|
+
def _is_config_file(p: str) -> bool:
|
|
1794
|
+
name = Path(p).name.lower()
|
|
1795
|
+
norm = p.replace("\\", "/")
|
|
1796
|
+
return any(pat in name or pat in norm for pat in _CONFIG_EXCLUDE_PATTERNS)
|
|
1797
|
+
|
|
1798
|
+
test_stems = {_normalize_test_stem(Path(p).stem) for p in test_set}
|
|
1799
|
+
untested = [
|
|
1800
|
+
p for p in source_set
|
|
1801
|
+
if Path(p).stem not in test_stems
|
|
1802
|
+
and not any(pen in p for pen in spec.ranking_penalties)
|
|
1803
|
+
and (include_config or not _is_config_file(p))
|
|
1804
|
+
]
|
|
1805
|
+
untested.sort(key=lambda p: (len(p.split("/")), p))
|
|
1806
|
+
test_gaps = untested[:15]
|
|
1686
1807
|
|
|
1687
1808
|
# ── 8. Confidence + gaps ──────────────────────────────────────────────
|
|
1688
1809
|
from sourcecode.confidence_analyzer import ConfidenceAnalyzer
|
|
@@ -1809,6 +1930,15 @@ class TaskContextBuilder:
|
|
|
1809
1930
|
diff_validation_status=_delta_baseline.get("diff_validation_status") if task_name == "delta" else None,
|
|
1810
1931
|
warnings=_delta_baseline.get("warnings", []) if task_name == "delta" else [],
|
|
1811
1932
|
symptom_hint=symptom_hint if task_name == "fix-bug" else None,
|
|
1933
|
+
# compact_base fields (Fix #1) — superset of --compact for all tasks
|
|
1934
|
+
security_surface=_cb_security_surface,
|
|
1935
|
+
mybatis=_cb_mybatis,
|
|
1936
|
+
transactional_boundaries=_cb_transactional,
|
|
1937
|
+
spring_profiles_info=_cb_spring_profiles,
|
|
1938
|
+
angular_analysis=_cb_angular,
|
|
1939
|
+
deployment_risks=_cb_deploy_risks,
|
|
1940
|
+
deployment=_cb_deployment,
|
|
1941
|
+
entry_points_structured=_cb_bootstrap,
|
|
1812
1942
|
)
|
|
1813
1943
|
|
|
1814
1944
|
def render_prompt(self, output: TaskOutput) -> str:
|
sourcecode/serializer.py
CHANGED
|
@@ -2012,7 +2012,7 @@ def agent_view(sm: SourceMap, *, full: bool = False) -> dict[str, Any]:
|
|
|
2012
2012
|
if not sm.dependency_summary or not sm.dependency_summary.requested:
|
|
2013
2013
|
analysis_gaps.append({
|
|
2014
2014
|
"area": "dependencies",
|
|
2015
|
-
"reason": "Dependencies not analyzed —
|
|
2015
|
+
"reason": "Dependencies not analyzed — use the full analyze command with dependency flags for complete context",
|
|
2016
2016
|
"impact": "medium",
|
|
2017
2017
|
})
|
|
2018
2018
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.31.
|
|
3
|
+
Version: 1.31.2
|
|
4
4
|
Summary: Deterministic codebase context for AI coding agents
|
|
5
5
|
License: Apache License
|
|
6
6
|
Version 2.0, January 2004
|
|
@@ -203,6 +203,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
203
203
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
204
204
|
Classifier: Topic :: Utilities
|
|
205
205
|
Requires-Python: >=3.9
|
|
206
|
+
Requires-Dist: mcp>=1.0.0
|
|
206
207
|
Requires-Dist: pathspec>=1.0
|
|
207
208
|
Requires-Dist: ruamel-yaml>=0.18
|
|
208
209
|
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
@@ -224,7 +225,7 @@ Description-Content-Type: text/markdown
|
|
|
224
225
|
|
|
225
226
|
**Deterministic, behavior-aware codebase context for AI agents and PR review.**
|
|
226
227
|
|
|
227
|
-

|
|
228
229
|

|
|
229
230
|
|
|
230
231
|
---
|
|
@@ -260,7 +261,7 @@ pipx install sourcecode
|
|
|
260
261
|
|
|
261
262
|
```bash
|
|
262
263
|
sourcecode version
|
|
263
|
-
# sourcecode 1.31.
|
|
264
|
+
# sourcecode 1.31.2
|
|
264
265
|
```
|
|
265
266
|
|
|
266
267
|
---
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=BlIGNpIZ2KvAHJhMqzRGPET0plDw5AYOTlGs0F8h4hU,103
|
|
2
2
|
sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
|
|
3
3
|
sourcecode/architecture_analyzer.py,sha256=MyBa0Hf5HmkudZQDLKrjcWDKETXETXl0mQX1swtTwAA,39091
|
|
4
4
|
sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=XgrZg2DcWcUm9r87cRG3KGO7IK2TIL_N-CvhSbUmmh4,49901
|
|
6
6
|
sourcecode/classifier.py,sha256=-0t0HLc9L9UleMLfclfLM3AXhBjUb_AYyBPDbvgWtac,7755
|
|
7
|
-
sourcecode/cli.py,sha256=
|
|
7
|
+
sourcecode/cli.py,sha256=qYMuo7DY6erJD5HwjU1Rot07xR54kXil4YzVytyU1Po,120897
|
|
8
8
|
sourcecode/code_notes_analyzer.py,sha256=y1MJBnPZHYp4i6cQCXUb9ATIyifS_qMQWjw_8lPkpsU,9215
|
|
9
|
-
sourcecode/confidence_analyzer.py,sha256=
|
|
9
|
+
sourcecode/confidence_analyzer.py,sha256=ZUn-Nywi5TEQcuozqK_vfOyPT-a1dYYO42elAtVFV-k,16412
|
|
10
10
|
sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
|
|
11
11
|
sourcecode/context_summarizer.py,sha256=CiQrfBEzun949bWvmLabWoj2HhPn6Lw62ofqnsy0FlQ,6503
|
|
12
12
|
sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
|
|
@@ -22,7 +22,7 @@ sourcecode/git_analyzer.py,sha256=0Gyj-vMpIIN4nfriKXVRouNYBeJ59s6pQDX2Xu9Pq-U,13
|
|
|
22
22
|
sourcecode/graph_analyzer.py,sha256=iUK-7pSV-cvGqqD2hENdYmhnm0wcXFEyK-xnu5ul8OU,62515
|
|
23
23
|
sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
|
|
24
24
|
sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
|
|
25
|
-
sourcecode/prepare_context.py,sha256=
|
|
25
|
+
sourcecode/prepare_context.py,sha256=vmZnoFneipRCE-hP6drvhqNXb3FwLrVub449e0MzT0Y,179908
|
|
26
26
|
sourcecode/progress.py,sha256=qn30sWaHOkjTgXsSBmiPkz7Rsbwc5oSlIe6JNEMYp_k,3149
|
|
27
27
|
sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,12970
|
|
28
28
|
sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
|
|
@@ -33,7 +33,7 @@ sourcecode/runtime_classifier.py,sha256=zWX3r3HCKHc-qtIobErOa8aKMmaoPYREtJKvPcBG
|
|
|
33
33
|
sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
|
|
34
34
|
sourcecode/schema.py,sha256=fj3BZ3IcnNV4j21BFIEvz8Qnw_vZoqIbzzRg-qQ-nd0,24530
|
|
35
35
|
sourcecode/semantic_analyzer.py,sha256=12TwXYkYbDcBdu0heX_EmfPM2EkO8a_r5osf0SaeQbs,88956
|
|
36
|
-
sourcecode/serializer.py,sha256=
|
|
36
|
+
sourcecode/serializer.py,sha256=KHVqwUK53axF10detPzqgmIY2P31rjLLJ_9T9Eyqp-E,111647
|
|
37
37
|
sourcecode/summarizer.py,sha256=lPlKhMh28nueXkPo2xKeD3DUFYVGRlJMIdY-8TSM-ls,17486
|
|
38
38
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
39
39
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -59,12 +59,12 @@ sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5Tc
|
|
|
59
59
|
sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
|
|
60
60
|
sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
|
|
61
61
|
sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
|
|
62
|
-
sourcecode/mcp/runner.py,sha256=
|
|
63
|
-
sourcecode/mcp/server.py,sha256=
|
|
62
|
+
sourcecode/mcp/runner.py,sha256=7PnFjKYbgxFeDnqVeSntXHxZX7ZtK3-krDkEuVjI24M,1386
|
|
63
|
+
sourcecode/mcp/server.py,sha256=01_b0MZnv4Yq2ES3FJSrqp1AJwCVdLVwOzQUVo3BtxY,6322
|
|
64
64
|
sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
|
|
65
65
|
sourcecode/mcp/onboarding/applier.py,sha256=yfSMT0NKdZsjavtLkC8yQ7OtkfepOl5IXGByqg6bdEY,1894
|
|
66
66
|
sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
|
|
67
|
-
sourcecode/mcp/onboarding/detector.py,sha256=
|
|
67
|
+
sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
|
|
68
68
|
sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
|
|
69
69
|
sourcecode/telemetry/__init__.py,sha256=M0eQZFNkmJiLbI_oNP4QEXwVju1dQ2d4P-E1-Bw8PxE,3116
|
|
70
70
|
sourcecode/telemetry/config.py,sha256=Pir0WHp4z-9Qclnn2NDZ3vwitqsMkOAJckmwjUSxrk4,1795
|
|
@@ -72,8 +72,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
|
|
|
72
72
|
sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
|
|
73
73
|
sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
|
|
74
74
|
sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
|
|
75
|
-
sourcecode-1.31.
|
|
76
|
-
sourcecode-1.31.
|
|
77
|
-
sourcecode-1.31.
|
|
78
|
-
sourcecode-1.31.
|
|
79
|
-
sourcecode-1.31.
|
|
75
|
+
sourcecode-1.31.2.dist-info/METADATA,sha256=CaHPC5ti8mv3vU8hOaq1vV5lvAHw8kPIH4IEvbk3x2k,29083
|
|
76
|
+
sourcecode-1.31.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
77
|
+
sourcecode-1.31.2.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
78
|
+
sourcecode-1.31.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
79
|
+
sourcecode-1.31.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|