sourcecode 1.33.2__tar.gz → 1.33.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. {sourcecode-1.33.2 → sourcecode-1.33.3}/PKG-INFO +3 -3
  2. {sourcecode-1.33.2 → sourcecode-1.33.3}/README.md +2 -2
  3. {sourcecode-1.33.2 → sourcecode-1.33.3}/pyproject.toml +1 -1
  4. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/cli.py +88 -2
  6. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/server.py +61 -6
  7. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/ris.py +15 -9
  8. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/serializer.py +7 -7
  9. {sourcecode-1.33.2 → sourcecode-1.33.3}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.33.2 → sourcecode-1.33.3}/.gitignore +0 -0
  11. {sourcecode-1.33.2 → sourcecode-1.33.3}/.ruff.toml +0 -0
  12. {sourcecode-1.33.2 → sourcecode-1.33.3}/CHANGELOG.md +0 -0
  13. {sourcecode-1.33.2 → sourcecode-1.33.3}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.33.2 → sourcecode-1.33.3}/LICENSE +0 -0
  15. {sourcecode-1.33.2 → sourcecode-1.33.3}/SECURITY.md +0 -0
  16. {sourcecode-1.33.2 → sourcecode-1.33.3}/raw +0 -0
  17. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/cache.tmp_new +0 -0
  23. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/canonical_ir.py +0 -0
  24. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/graph_analyzer.py +0 -0
  61. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/license.py +0 -0
  62. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/__init__.py +0 -0
  63. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  64. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  65. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  66. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  67. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  68. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp/runner.py +0 -0
  69. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/mcp_nudge.py +0 -0
  70. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/metrics_analyzer.py +0 -0
  71. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/output_budget.py +0 -0
  72. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/path_filters.py +0 -0
  73. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/pr_comment_renderer.py +0 -0
  74. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/prepare_context.py +0 -0
  75. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/progress.py +0 -0
  76. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/ranking_engine.py +0 -0
  77. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/redactor.py +0 -0
  78. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/relevance_scorer.py +0 -0
  79. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/repo_classifier.py +0 -0
  80. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/repository_ir.py +0 -0
  81. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/runtime_classifier.py +0 -0
  82. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/scanner.py +0 -0
  83. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/schema.py +0 -0
  84. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/semantic_analyzer.py +0 -0
  85. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/summarizer.py +0 -0
  86. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/__init__.py +0 -0
  87. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/config.py +0 -0
  88. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/consent.py +0 -0
  89. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/events.py +0 -0
  90. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/filters.py +0 -0
  91. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/telemetry/transport.py +0 -0
  92. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/tree_utils.py +0 -0
  93. {sourcecode-1.33.2 → sourcecode-1.33.3}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.2
3
+ Version: 1.33.3
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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
41
 
42
- ![Version](https://img.shields.io/badge/version-1.33.2-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.33.3-blue)
43
43
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
44
 
45
45
  ---
@@ -113,7 +113,7 @@ pipx install sourcecode
113
113
 
114
114
  ```bash
115
115
  sourcecode version
116
- # sourcecode 1.33.2
116
+ # sourcecode 1.33.3
117
117
  ```
118
118
 
119
119
  ---
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
4
4
 
5
- ![Version](https://img.shields.io/badge/version-1.33.2-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.33.3-blue)
6
6
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
7
7
 
8
8
  ---
@@ -76,7 +76,7 @@ pipx install sourcecode
76
76
 
77
77
  ```bash
78
78
  sourcecode version
79
- # sourcecode 1.33.2
79
+ # sourcecode 1.33.3
80
80
  ```
81
81
 
82
82
  ---
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.33.2"
7
+ version = "1.33.3"
8
8
  description = "Persistent structural context and ultra-fast repeated analysis for AI coding agents"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.33.2"
3
+ __version__ = "1.33.3"
@@ -4320,7 +4320,11 @@ def cache_warm_cmd(
4320
4320
  compact: bool = typer.Option(True, "--compact/--no-compact", help="Warm compact view (default: on)."),
4321
4321
  agent: bool = typer.Option(False, "--agent", help="Also warm agent view."),
4322
4322
  ) -> None:
4323
- """Pre-populate the cache by running a fresh analysis."""
4323
+ """Pre-populate the cache by running a fresh analysis.
4324
+
4325
+ Runs a full analysis to populate L1/L2 caches and rebuild the RIS
4326
+ (Repository Intelligence Snapshot). Useful after a merge/pull in CI.
4327
+ """
4324
4328
  import shutil as _shutil
4325
4329
  import subprocess as _sub
4326
4330
  import sys as _sys
@@ -4334,7 +4338,7 @@ def cache_warm_cmd(
4334
4338
  cmd.append("--agent")
4335
4339
  result = _sub.run(cmd, capture_output=True, text=True)
4336
4340
  if result.returncode == 0:
4337
- typer.echo("Cache warmed.", err=True)
4341
+ typer.echo("Cache warmed (L1/L2 + RIS rebuilt).", err=True)
4338
4342
  else:
4339
4343
  typer.echo(f"Warm failed (exit {result.returncode}).", err=True)
4340
4344
  if result.stderr:
@@ -4342,6 +4346,88 @@ def cache_warm_cmd(
4342
4346
  raise typer.Exit(code=result.returncode)
4343
4347
 
4344
4348
 
4349
+ @cache_app.command("freshness")
4350
+ def cache_freshness_cmd(
4351
+ path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
4352
+ json_output: bool = typer.Option(False, "--json", help="Output as JSON."),
4353
+ ) -> None:
4354
+ """Report RIS freshness relative to the current git HEAD.
4355
+
4356
+ Answers: is the cached snapshot current? How many commits behind is it?
4357
+
4358
+ \b
4359
+ Output fields:
4360
+ fresh — True when RIS HEAD matches current HEAD and no uncommitted changes
4361
+ current_git_head — Current repo HEAD (short SHA)
4362
+ ris_git_head — HEAD stored in RIS when it was last built
4363
+ delta_commits — Number of commits between ris_git_head and HEAD (0 = in sync)
4364
+ has_uncommitted_changes — Working tree has staged/unstaged changes
4365
+ ris_exists — False when no RIS has been built yet
4366
+ ris_last_updated_at — ISO-8601 timestamp of last RIS write
4367
+ """
4368
+ import json as _json
4369
+ import subprocess as _sub
4370
+ from sourcecode import cache as _cm
4371
+ from sourcecode.ris import _has_uncommitted_changes as _huc
4372
+ from sourcecode.ris import load_ris as _lris
4373
+
4374
+ target = Path(path).resolve()
4375
+ current_head = _cm._get_git_head(target)
4376
+ ris = _lris(target)
4377
+
4378
+ if ris is None:
4379
+ result: dict = {
4380
+ "fresh": False,
4381
+ "ris_exists": False,
4382
+ "current_git_head": current_head,
4383
+ "ris_git_head": None,
4384
+ "delta_commits": None,
4385
+ "has_uncommitted_changes": _huc(target),
4386
+ "ris_last_updated_at": None,
4387
+ }
4388
+ else:
4389
+ ris_head = ris.git_head
4390
+ head_matches = bool(current_head and ris_head and current_head == ris_head)
4391
+ uncommitted = _huc(target)
4392
+
4393
+ # Count commits between ris_head and current HEAD
4394
+ delta = None
4395
+ if ris_head and current_head and ris_head != current_head:
4396
+ try:
4397
+ _r = _sub.run(
4398
+ ["git", "-C", str(target), "rev-list", "--count", f"{ris_head}..HEAD"],
4399
+ capture_output=True, text=True, timeout=5,
4400
+ )
4401
+ if _r.returncode == 0:
4402
+ delta = int(_r.stdout.strip())
4403
+ except Exception:
4404
+ pass
4405
+ elif head_matches:
4406
+ delta = 0
4407
+
4408
+ result = {
4409
+ "fresh": head_matches and not uncommitted,
4410
+ "ris_exists": True,
4411
+ "current_git_head": current_head,
4412
+ "ris_git_head": ris_head,
4413
+ "delta_commits": delta,
4414
+ "has_uncommitted_changes": uncommitted,
4415
+ "ris_last_updated_at": ris.last_updated_at,
4416
+ }
4417
+
4418
+ if json_output:
4419
+ typer.echo(_json.dumps(result, indent=2, ensure_ascii=False))
4420
+ else:
4421
+ _fresh_tag = "FRESH" if result["fresh"] else "STALE"
4422
+ typer.echo(f"Status: {_fresh_tag}")
4423
+ typer.echo(f"Current HEAD: {result['current_git_head'] or '(unknown)'}")
4424
+ typer.echo(f"RIS HEAD: {result.get('ris_git_head') or '(none)'}")
4425
+ if result.get("delta_commits") is not None:
4426
+ typer.echo(f"Delta: {result['delta_commits']} commit(s) behind")
4427
+ typer.echo(f"Uncommitted: {result['has_uncommitted_changes']}")
4428
+ typer.echo(f"RIS updated: {result.get('ris_last_updated_at') or 'never'}")
4429
+
4430
+
4345
4431
  # ── Entry point ───────────────────────────────────────────────────────────────
4346
4432
 
4347
4433
  def main_entry() -> None:
@@ -215,7 +215,10 @@ def get_agent_context(repo_path: str = ".", git_context: bool = False) -> dict:
215
215
 
216
216
  @mcp.tool()
217
217
  def get_endpoints(repo_path: str = ".") -> dict:
218
- """REST API endpoint surface extraction from Java source files.
218
+ """REST API endpoint surface extraction from Java source files. JAVA ONLY.
219
+
220
+ Do NOT call this on non-Java repositories — it will return empty results.
221
+ Use get_compact_context or get_agent_context for non-Java repos.
219
222
 
220
223
  Maps to: sourcecode endpoints <repo_path>
221
224
  Returns: endpoints list with method, path, controller, handler fields;
@@ -230,7 +233,7 @@ def get_endpoints(repo_path: str = ".") -> dict:
230
233
  Supports Spring MVC (@GetMapping etc.) and JAX-RS (@GET/@POST etc.).
231
234
  Security annotations detected: @RolesAllowed, @PermitAll, @DenyAll,
232
235
  @Authenticated, @PreAuthorize, @Secured, @SecurityRequirement, @M3FiltroSeguridad.
233
- repo_path: absolute path to the repository (default: current working directory).
236
+ repo_path: absolute path to the Java repository (default: current working directory).
234
237
  """
235
238
  _raw = repo_path
236
239
  try:
@@ -275,25 +278,43 @@ def get_module_context(repo_path: str = ".", module: str = "") -> dict:
275
278
  )
276
279
 
277
280
 
281
+ def _auto_since(repo_path: str) -> str:
282
+ """Detect best merge-base for delta: origin/main > origin/master > HEAD~1."""
283
+ import subprocess as _sp
284
+ for base in ("origin/main", "origin/master"):
285
+ try:
286
+ r = _sp.run(
287
+ ["git", "-C", repo_path, "merge-base", "HEAD", base],
288
+ capture_output=True, text=True, timeout=5,
289
+ )
290
+ if r.returncode == 0 and r.stdout.strip():
291
+ return r.stdout.strip()
292
+ except Exception:
293
+ pass
294
+ return "HEAD~1"
295
+
296
+
278
297
  @mcp.tool()
279
- def get_delta(repo_path: str = ".", since: str = "HEAD~1") -> dict:
298
+ def get_delta(repo_path: str = ".", since: str = "") -> dict:
280
299
  """Incremental context: git-changed files since a reference commit.
281
300
 
282
301
  Maps to: sourcecode prepare-context delta <repo_path> --since <since>
283
302
  repo_path: absolute path to the repository (default: current working directory).
284
303
  since: git ref to diff against (e.g. HEAD~3, main, origin/main).
304
+ If empty or omitted, auto-detects merge-base with origin/main (or
305
+ origin/master). Falls back to HEAD~1 if no remote branch found.
306
+ Pass "HEAD~1" explicitly to force single-commit diff.
285
307
  """
286
308
  _raw = repo_path
287
309
  try:
288
310
  if not isinstance(repo_path, str):
289
311
  return _err("repo_path must be a string", "INVALID_ARGUMENT")
290
- if not isinstance(since, str) or not since.strip():
291
- return _err("since must be a non-empty git ref", "INVALID_ARGUMENT")
292
312
  repo_path = _normalize_repo_path(repo_path)
293
313
  _path_err = _check_repo_path(repo_path)
294
314
  if _path_err is not None:
295
315
  return _path_err
296
- return _execute(["prepare-context", "delta", repo_path, "--since", since])
316
+ _since = since.strip() if isinstance(since, str) and since.strip() else _auto_since(repo_path)
317
+ return _execute(["prepare-context", "delta", repo_path, "--since", _since])
297
318
  except Exception as exc:
298
319
  return _err(
299
320
  f"Internal error: {type(exc).__name__}: {exc} — repo_path recibido: {_raw}",
@@ -301,6 +322,40 @@ def get_delta(repo_path: str = ".", since: str = "HEAD~1") -> dict:
301
322
  )
302
323
 
303
324
 
325
+ @mcp.tool()
326
+ def check_freshness(repo_path: str = ".") -> dict:
327
+ """Report RIS freshness relative to the current git HEAD.
328
+
329
+ Answers instantly: is the cached snapshot current? How many commits behind?
330
+ Use before deciding whether to call get_compact_context for a refresh.
331
+
332
+ Returns:
333
+ fresh (bool) — True when RIS HEAD == current HEAD and no uncommitted changes
334
+ current_git_head (str) — Current repo HEAD (short SHA)
335
+ ris_git_head (str|null) — HEAD stored in RIS at last build
336
+ delta_commits (int|null) — Commits between ris_git_head and HEAD (0 = in sync)
337
+ has_uncommitted_changes — Working tree has staged or unstaged changes
338
+ ris_exists (bool) — False when no RIS built yet
339
+ ris_last_updated_at (str) — ISO-8601 timestamp of last RIS write
340
+
341
+ repo_path: absolute path to the repository (default: current working directory).
342
+ """
343
+ _raw = repo_path
344
+ try:
345
+ if not isinstance(repo_path, str):
346
+ return _err("repo_path must be a string", "INVALID_ARGUMENT")
347
+ repo_path = _normalize_repo_path(repo_path)
348
+ _path_err = _check_repo_path(repo_path)
349
+ if _path_err is not None:
350
+ return _path_err
351
+ return _execute(["cache", "freshness", repo_path, "--json"])
352
+ except Exception as exc:
353
+ return _err(
354
+ f"Internal error: {type(exc).__name__}: {exc} — repo_path: {_raw}",
355
+ "INTERNAL_ERROR",
356
+ )
357
+
358
+
304
359
  @mcp.tool()
305
360
  def get_ir_summary(repo_path: str = ".") -> dict:
306
361
  """Deterministic symbol-level IR summary for Java repositories. Java only.
@@ -384,6 +384,15 @@ def get_cold_start_context(repo_root: Path) -> dict:
384
384
  uncommitted = _has_uncommitted_changes(repo_root)
385
385
 
386
386
  endpoints = ris.api_surface.get("endpoints", [])
387
+ _is_java = (
388
+ (repo_root / "pom.xml").exists()
389
+ or (repo_root / "build.gradle").exists()
390
+ or (repo_root / "build.gradle.kts").exists()
391
+ )
392
+ # api_surface_complete: False when this is a Java repo but endpoints are absent.
393
+ # An empty list does NOT mean "no endpoints exist" — it means the endpoint
394
+ # index has not been built yet. Agents must call get_endpoints to populate.
395
+ _api_complete = not _is_java or bool(endpoints)
387
396
  result: dict = {
388
397
  "status": "cold_start_stale" if stale else "cold_start_ready",
389
398
  "repo_id": ris.repo_id,
@@ -394,20 +403,17 @@ def get_cold_start_context(repo_root: Path) -> dict:
394
403
  "last_updated_at": ris.last_updated_at,
395
404
  "cache_source": "RIS",
396
405
  "data_scope": "RIS_BOOTSTRAP",
406
+ "api_surface_complete": _api_complete,
397
407
  "summary": ris.compact_summary,
398
408
  "entrypoints": ris.structural_map.get("entrypoints", []),
399
409
  "endpoints": endpoints,
400
410
  "hotspots": ris.git_context_snapshot.get("hotspots", []),
401
411
  }
402
- if not endpoints:
403
- _is_java = (repo_root / "pom.xml").exists() or \
404
- (repo_root / "build.gradle").exists() or \
405
- (repo_root / "build.gradle.kts").exists()
406
- if _is_java:
407
- result["endpoints_hint"] = (
408
- "Java repo detected but no endpoint index found. "
409
- "Call get_endpoints (or: sourcecode endpoints <path>) to populate."
410
- )
412
+ if not endpoints and _is_java:
413
+ result["endpoints_hint"] = (
414
+ "Java repo detected but no endpoint index found. "
415
+ "Call get_endpoints (or: sourcecode endpoints <path>) to populate."
416
+ )
411
417
  return result
412
418
  except Exception:
413
419
  return {"status": "no_ris"}
@@ -631,9 +631,12 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
631
631
  if security:
632
632
  result["security"] = security
633
633
  if controllers:
634
- # Count unique files (classes) vs total entries (methods/endpoints)
634
+ # Each controller file generates one EntryPoint regardless of how many
635
+ # handler methods it contains. controller_classes == len(controllers)
636
+ # always (deduplicated by path in _scan_java_file_for_entry_points).
637
+ # "methods" is therefore removed from the note — use `sourcecode endpoints`
638
+ # for per-method HTTP surface.
635
639
  controller_classes = len({c["path"] for c in controllers})
636
- controller_methods = len(controllers)
637
640
 
638
641
  # Extract all DDD module names from controller paths and group by domain area.
639
642
  # Path pattern: .../ddd/{module}/infrastructure/rest/*Controller.java
@@ -655,9 +658,8 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
655
658
  module_names.append(module)
656
659
 
657
660
  _ctrl_note = (
658
- f"{controller_methods} detected entry-point methods across "
659
- f"{controller_classes} controller classes"
660
- f" (use 'sourcecode endpoints' for full surface)"
661
+ f"{controller_classes} controller classes detected"
662
+ f" (use 'sourcecode endpoints' for per-method HTTP surface)"
661
663
  )
662
664
  if len(module_names) > 30:
663
665
  # Group by first path segment under ddd/ (inferred domain area)
@@ -678,14 +680,12 @@ def _bootstrap_structured(eps: list) -> "Optional[dict[str, Any]]":
678
680
  domain_groups[domain_prefix or "other"].append(module)
679
681
  result["controllers"] = {
680
682
  "classes": controller_classes,
681
- "methods": controller_methods,
682
683
  "note": _ctrl_note,
683
684
  "modules": {k: sorted(v) for k, v in sorted(domain_groups.items())},
684
685
  }
685
686
  else:
686
687
  result["controllers"] = {
687
688
  "classes": controller_classes,
688
- "methods": controller_methods,
689
689
  "note": _ctrl_note,
690
690
  "modules": sorted(module_names),
691
691
  }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes