sourcecode 1.31.1__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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.31.1"
3
+ __version__ = "1.31.2"
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")
@@ -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 — run with --dependencies for full context",
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:
sourcecode/mcp/server.py CHANGED
@@ -15,9 +15,10 @@ from typing import Any
15
15
 
16
16
  from mcp.server.fastmcp import FastMCP
17
17
 
18
+ from sourcecode import __version__
18
19
  from sourcecode.mcp.runner import run_command
19
20
 
20
- mcp = FastMCP("sourcecode")
21
+ mcp = FastMCP("sourcecode", version=__version__)
21
22
 
22
23
 
23
24
  def _ok(data: Any) -> dict:
@@ -63,16 +64,16 @@ def get_agent_context(repo_path: str = ".") -> dict:
63
64
 
64
65
  @mcp.tool()
65
66
  def get_endpoints(repo_path: str = ".") -> dict:
66
- """API endpoint surface extraction.
67
+ """REST API endpoint surface extraction from Java source files.
67
68
 
68
- Maps to: sourcecode <repo_path> --endpoints (pending CLI implementation)
69
+ Maps to: sourcecode endpoints <repo_path>
70
+ Returns: endpoints list with method, path, controller, handler, required_permission;
71
+ total count and undocumented count.
69
72
  repo_path: absolute path to the repository (default: current working directory).
70
73
  """
71
- return _err(
72
- "get_endpoints requires --endpoints CLI flag (pending implementation). "
73
- "Use get_compact_context for now — the output includes api_endpoint-classified files.",
74
- "NOT_IMPLEMENTED",
75
- )
74
+ if not isinstance(repo_path, str):
75
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
76
+ return _execute(["endpoints", repo_path])
76
77
 
77
78
 
78
79
  @mcp.tool()
@@ -117,3 +118,39 @@ def get_ir_summary(repo_path: str = ".") -> dict:
117
118
  if not isinstance(repo_path, str):
118
119
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
119
120
  return _execute(["repo-ir", repo_path, "--summary-only"])
121
+
122
+
123
+ @mcp.tool()
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
+
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.
150
+ """
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)
@@ -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[str] = []
1706
+ test_gaps: list = []
1651
1707
  if task_name == "generate-tests" and not fast:
1652
- def _normalize_test_stem(stem: str) -> str:
1653
- # Java: FooTest / FooTests Foo; TestFoo → Foo
1654
- if stem.endswith("Tests"):
1655
- return stem[:-5]
1656
- if stem.endswith("Test"):
1657
- return stem[:-4]
1658
- if stem.startswith("Test") and len(stem) > 4 and stem[4].isupper():
1659
- return stem[4:]
1660
- # Python/JS: test_foo / foo_test
1661
- return stem.removeprefix("test_").removesuffix("_test")
1662
-
1663
- # Patterns excluded from test_gaps by default (IMP-1): tooling config
1664
- # files have no business logic to test. --include-config overrides.
1665
- _CONFIG_EXCLUDE_PATTERNS = (
1666
- ".eslintrc", ".prettierrc", "eslint.config",
1667
- "karma.conf", "jest.config", "babel.config",
1668
- "webpack.config", "vite.config", "rollup.config",
1669
- "tsconfig", "angular.json", ".claude/",
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
- def _is_config_file(p: str) -> bool:
1673
- name = Path(p).name.lower()
1674
- norm = p.replace("\\", "/")
1675
- return any(pat in name or pat in norm for pat in _CONFIG_EXCLUDE_PATTERNS)
1676
-
1677
- test_stems = {_normalize_test_stem(Path(p).stem) for p in test_set}
1678
- untested = [
1679
- p for p in source_set
1680
- if Path(p).stem not in test_stems
1681
- and not any(pen in p for pen in spec.ranking_penalties)
1682
- and (include_config or not _is_config_file(p))
1683
- ]
1684
- untested.sort(key=lambda p: (len(p.split("/")), p))
1685
- test_gaps = untested[:15]
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 — add --dependencies for full context",
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.1
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
@@ -225,7 +225,7 @@ Description-Content-Type: text/markdown
225
225
 
226
226
  **Deterministic, behavior-aware codebase context for AI agents and PR review.**
227
227
 
228
- ![Version](https://img.shields.io/badge/version-1.31.1-blue)
228
+ ![Version](https://img.shields.io/badge/version-1.31.2-blue)
229
229
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
230
230
 
231
231
  ---
@@ -261,7 +261,7 @@ pipx install sourcecode
261
261
 
262
262
  ```bash
263
263
  sourcecode version
264
- # sourcecode 1.31.1
264
+ # sourcecode 1.31.2
265
265
  ```
266
266
 
267
267
  ---
@@ -1,12 +1,12 @@
1
- sourcecode/__init__.py,sha256=6_eP1xYY6TSz7m1YdF9GvL5eOo0J9rRzaYP9OD56okA,103
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=I3h-Do1vFUEJzj7tK19yacZsYQA84t3Nlwfo1hNKjZo,112353
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=H9VHYRzZhqMFlSCZffjtsMUGYLnDvrq1g5FjzyQ1hxE,16381
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=QNCl8uKk9PQpgXxPHBNSDXXkc1s2wTZwU6H0REC5Qms,173487
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=FM4xklb9Ywg9KNdNpo8QXR50izuml5FkbeQgL2uS1HY,111611
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
@@ -60,7 +60,7 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
60
60
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
61
61
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
62
62
  sourcecode/mcp/runner.py,sha256=7PnFjKYbgxFeDnqVeSntXHxZX7ZtK3-krDkEuVjI24M,1386
63
- sourcecode/mcp/server.py,sha256=Za6R1bGI7gQ9BI7M-UdBGK637W9wlowGcwjB0kRHZAs,4454
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
@@ -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.1.dist-info/METADATA,sha256=rNR0kignzicNPRwVkhFULiLqGAbSqzLZgkGckgA0E-Y,29083
76
- sourcecode-1.31.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
77
- sourcecode-1.31.1.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
78
- sourcecode-1.31.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
79
- sourcecode-1.31.1.dist-info/RECORD,,
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,,