sourcecode 1.33.1__py3-none-any.whl → 1.33.3__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.33.1"
3
+ __version__ = "1.33.3"
sourcecode/cache.py CHANGED
@@ -57,6 +57,7 @@ import hashlib
57
57
  import json
58
58
  import os
59
59
  import re
60
+ import subprocess
60
61
  from datetime import datetime, timezone
61
62
  from pathlib import Path
62
63
  from typing import Any, Optional
@@ -110,6 +111,24 @@ _CORE_RE = re.compile(r"^core-([0-9a-f]+)-[0-9a-f]+\.json\.gz$")
110
111
  _VIEW_RE = re.compile(r"^view-([0-9a-f]{16})-[0-9a-f]+\.json\.gz$")
111
112
 
112
113
 
114
+ # ---------------------------------------------------------------------------
115
+ # Internal helpers
116
+ # ---------------------------------------------------------------------------
117
+
118
+ def _get_git_head(repo_root: Path) -> str:
119
+ """Return short git HEAD SHA, or '' on any error."""
120
+ try:
121
+ r = subprocess.run(
122
+ ["git", "-C", str(repo_root), "rev-parse", "--short", "HEAD"],
123
+ capture_output=True, text=True, timeout=2,
124
+ )
125
+ if r.returncode == 0:
126
+ return r.stdout.strip()
127
+ except Exception:
128
+ pass
129
+ return ""
130
+
131
+
113
132
  # ---------------------------------------------------------------------------
114
133
  # Public API — location helpers
115
134
  # ---------------------------------------------------------------------------
@@ -138,15 +157,51 @@ def cache_dir(repo_root: Path) -> Path:
138
157
  def status(repo_root: Path) -> dict[str, Any]:
139
158
  """Return a stats dict describing the current cache state for *repo_root*.
140
159
 
141
- Keys: ``cache_dir``, ``cores``, ``snapshots``, ``views``, ``cas_blobs``,
142
- ``total_size_bytes``, ``total_size_mb``.
160
+ Keys: ``cache_dir``, ``cores``, ``views``, ``cas_blobs``,
161
+ ``total_size_bytes``, ``total_size_mb``, ``ris_exists``, ``ris_git_head``,
162
+ ``ris_last_updated_at``, ``ris_is_stale``, ``current_git_head``.
163
+
164
+ Note: ``snapshots`` is a legacy v1 field — always 0 in v2 (kept for
165
+ backward compatibility; v2 writes ``core-*`` and ``view-*`` files only).
143
166
  """
144
167
  cache_d = cache_dir(repo_root)
168
+ current_head = _get_git_head(repo_root)
169
+
170
+ # RIS metadata (lazy import to avoid circular dependency)
171
+ ris_fields: dict[str, Any]
172
+ try:
173
+ from sourcecode.ris import load_ris as _load_ris # noqa: PLC0415
174
+ _ris = _load_ris(repo_root)
175
+ if _ris is not None:
176
+ _ris_stale = bool(current_head and _ris.git_head and current_head != _ris.git_head)
177
+ ris_fields = {
178
+ "ris_exists": True,
179
+ "ris_git_head": _ris.git_head,
180
+ "ris_last_updated_at": _ris.last_updated_at,
181
+ "ris_is_stale": _ris_stale,
182
+ }
183
+ else:
184
+ ris_fields = {
185
+ "ris_exists": False,
186
+ "ris_git_head": None,
187
+ "ris_last_updated_at": None,
188
+ "ris_is_stale": False,
189
+ }
190
+ except Exception:
191
+ ris_fields = {
192
+ "ris_exists": False,
193
+ "ris_git_head": None,
194
+ "ris_last_updated_at": None,
195
+ "ris_is_stale": False,
196
+ }
197
+
145
198
  if not cache_d.exists():
146
199
  return {
147
200
  "cache_dir": str(cache_d),
148
201
  "cores": 0, "snapshots": 0, "views": 0, "cas_blobs": 0,
149
202
  "total_size_bytes": 0, "total_size_mb": 0.0,
203
+ "current_git_head": current_head,
204
+ **ris_fields,
150
205
  }
151
206
  cores = list(cache_d.glob("core-*.json.gz"))
152
207
  snapshots = list(cache_d.glob("snapshot-*.json.gz"))
@@ -162,11 +217,18 @@ def status(repo_root: Path) -> dict[str, Any]:
162
217
  "cas_blobs": len(cas_blobs),
163
218
  "total_size_bytes": total_bytes,
164
219
  "total_size_mb": round(total_bytes / (1024 * 1024), 2),
220
+ "current_git_head": current_head,
221
+ **ris_fields,
165
222
  }
166
223
 
167
224
 
168
- def clear(repo_root: Path) -> int:
169
- """Delete all cache files for *repo_root*. Returns the number of files removed."""
225
+ def clear(repo_root: Path, *, clear_ris: bool = False) -> int:
226
+ """Delete cache files for *repo_root*. Returns the number of files removed.
227
+
228
+ By default, RIS (``ris.json.gz``) is preserved across clears — it is the
229
+ persistent structural index used by cold-start bootstrapping. Pass
230
+ ``clear_ris=True`` (CLI: ``--include-ris``) to also delete the RIS.
231
+ """
170
232
  cache_d = cache_dir(repo_root)
171
233
  if not cache_d.exists():
172
234
  return 0
@@ -180,6 +242,11 @@ def clear(repo_root: Path) -> int:
180
242
  for f in cas_d.glob("*.gz"):
181
243
  _safe_unlink(f)
182
244
  removed += 1
245
+ if clear_ris:
246
+ ris_file = cache_d / "ris.json.gz"
247
+ if ris_file.exists():
248
+ _safe_unlink(ris_file)
249
+ removed += 1
183
250
  return removed
184
251
 
185
252
 
sourcecode/cli.py CHANGED
@@ -1058,6 +1058,22 @@ def main(
1058
1058
  code_notes = True
1059
1059
  architecture = True
1060
1060
 
1061
+ def _inject_cache_meta(raw: str, meta: dict) -> str:
1062
+ """Inject ``_cache`` provenance block into a JSON dict string.
1063
+
1064
+ Parses *raw* as JSON, adds ``_cache`` key, re-serialises. Returns *raw*
1065
+ unchanged on any parse failure or non-dict JSON (YAML pass-through, etc.).
1066
+ """
1067
+ try:
1068
+ import json as _jm
1069
+ obj = _jm.loads(raw)
1070
+ if isinstance(obj, dict):
1071
+ obj["_cache"] = meta
1072
+ return _jm.dumps(obj, indent=2, ensure_ascii=False)
1073
+ except Exception:
1074
+ pass
1075
+ return raw
1076
+
1061
1077
  # ── Two-layer cache ────────────────────────────────────────────────────────
1062
1078
  # L1 (core): (repo, commit, analysis_flags) → pre-computed view data dict
1063
1079
  # key = core-<git_sha>-<analysis_hash>.json.gz
@@ -1202,6 +1218,23 @@ def main(
1202
1218
 
1203
1219
  if _cache_hit_content is not None:
1204
1220
  from sourcecode.serializer import write_output
1221
+ if format == "json":
1222
+ try:
1223
+ from sourcecode.ris import _has_uncommitted_changes as _huc
1224
+ _uncommitted = _huc(target)
1225
+ except Exception:
1226
+ _uncommitted = False
1227
+ _hit_source = "L2_view" if (_view_key and _core_hash) else "L1_core"
1228
+ _data_scope = "COMPACT" if compact else ("AGENT" if agent else "FULL")
1229
+ _cache_hit_content = _inject_cache_meta(_cache_hit_content, {
1230
+ "cache_source": _hit_source,
1231
+ "git_head_at_generation": _git_sha,
1232
+ "current_git_head": _git_sha,
1233
+ "is_stale": False,
1234
+ "has_uncommitted_changes": _uncommitted,
1235
+ "generated_at": None,
1236
+ "data_scope": _data_scope,
1237
+ })
1205
1238
  write_output(_cache_hit_content, output=output)
1206
1239
  if copy and not output:
1207
1240
  _copy_to_clipboard(_cache_hit_content)
@@ -2082,6 +2115,23 @@ def main(
2082
2115
 
2083
2116
  # 6. Write output (CLI-04)
2084
2117
  _progress.finish()
2118
+ if format == "json":
2119
+ try:
2120
+ from sourcecode.ris import _has_uncommitted_changes as _huc_fresh
2121
+ _uncommitted_fresh = _huc_fresh(target)
2122
+ except Exception:
2123
+ _uncommitted_fresh = False
2124
+ import datetime as _dt
2125
+ _data_scope_fresh = "COMPACT" if compact else ("AGENT" if agent else "FULL")
2126
+ content = _inject_cache_meta(content, {
2127
+ "cache_source": "fresh",
2128
+ "git_head_at_generation": _git_sha,
2129
+ "current_git_head": _git_sha,
2130
+ "is_stale": False,
2131
+ "has_uncommitted_changes": _uncommitted_fresh,
2132
+ "generated_at": _dt.datetime.now(_dt.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"),
2133
+ "data_scope": _data_scope_fresh,
2134
+ })
2085
2135
  write_output(content, output=output)
2086
2136
 
2087
2137
  # Persist to two-layer cache (git SHA unchanged → re-use on next run).
@@ -4231,23 +4281,36 @@ def cache_status_cmd(
4231
4281
  else:
4232
4282
  typer.echo(f"Cache dir: {stats['cache_dir']}")
4233
4283
  typer.echo(f"Cores: {stats['cores']}")
4234
- typer.echo(f"Snapshots: {stats['snapshots']}")
4235
4284
  typer.echo(f"Views: {stats['views']}")
4236
4285
  typer.echo(f"CAS blobs: {stats['cas_blobs']}")
4237
4286
  typer.echo(f"Total size: {stats['total_size_mb']} MB")
4287
+ # RIS section
4288
+ if stats.get("ris_exists"):
4289
+ _stale_tag = " [STALE]" if stats.get("ris_is_stale") else ""
4290
+ typer.echo(f"RIS: exists HEAD={stats.get('ris_git_head', '?')}{_stale_tag} updated={stats.get('ris_last_updated_at', '?')}")
4291
+ else:
4292
+ typer.echo("RIS: none (run analysis to build)")
4293
+ if stats.get("current_git_head"):
4294
+ typer.echo(f"Current HEAD:{stats['current_git_head']}")
4238
4295
 
4239
4296
 
4240
4297
  @cache_app.command("clear")
4241
4298
  def cache_clear_cmd(
4242
4299
  path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
4243
4300
  yes: bool = typer.Option(False, "--yes", "-y", help="Skip confirmation prompt."),
4301
+ include_ris: bool = typer.Option(False, "--include-ris", help="Also delete the RIS snapshot (ris.json.gz). By default, RIS is preserved across clears."),
4244
4302
  ) -> None:
4245
- """Delete all cached snapshots for a repository."""
4303
+ """Delete cached snapshots for a repository.
4304
+
4305
+ By default, RIS (ris.json.gz) is preserved — it is the persistent structural
4306
+ index used for cold-start bootstrapping. Use --include-ris to also clear it.
4307
+ """
4246
4308
  from sourcecode import cache as _cm
4247
4309
  target = Path(path).resolve()
4248
4310
  if not yes:
4249
- typer.confirm(f"Delete all cache files for {target}?", abort=True)
4250
- removed = _cm.clear(target)
4311
+ _ris_note = " (including RIS)" if include_ris else " (RIS preserved — use --include-ris to also clear it)"
4312
+ typer.confirm(f"Delete all cache files for {target}{_ris_note}?", abort=True)
4313
+ removed = _cm.clear(target, clear_ris=include_ris)
4251
4314
  typer.echo(f"Removed {removed} file(s).")
4252
4315
 
4253
4316
 
@@ -4257,7 +4320,11 @@ def cache_warm_cmd(
4257
4320
  compact: bool = typer.Option(True, "--compact/--no-compact", help="Warm compact view (default: on)."),
4258
4321
  agent: bool = typer.Option(False, "--agent", help="Also warm agent view."),
4259
4322
  ) -> None:
4260
- """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
+ """
4261
4328
  import shutil as _shutil
4262
4329
  import subprocess as _sub
4263
4330
  import sys as _sys
@@ -4271,7 +4338,7 @@ def cache_warm_cmd(
4271
4338
  cmd.append("--agent")
4272
4339
  result = _sub.run(cmd, capture_output=True, text=True)
4273
4340
  if result.returncode == 0:
4274
- typer.echo("Cache warmed.", err=True)
4341
+ typer.echo("Cache warmed (L1/L2 + RIS rebuilt).", err=True)
4275
4342
  else:
4276
4343
  typer.echo(f"Warm failed (exit {result.returncode}).", err=True)
4277
4344
  if result.stderr:
@@ -4279,6 +4346,88 @@ def cache_warm_cmd(
4279
4346
  raise typer.Exit(code=result.returncode)
4280
4347
 
4281
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
+
4282
4431
  # ── Entry point ───────────────────────────────────────────────────────────────
4283
4432
 
4284
4433
  def main_entry() -> None:
sourcecode/mcp/server.py CHANGED
@@ -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.
sourcecode/ris.py CHANGED
@@ -349,6 +349,26 @@ def _current_git_head(repo_root: Path) -> str:
349
349
  return ""
350
350
 
351
351
 
352
+ def _has_uncommitted_changes(repo_root: Path) -> bool:
353
+ """Return True if working tree has staged or unstaged changes.
354
+
355
+ Uses ``git status --porcelain`` — any non-empty output means the working
356
+ tree diverges from HEAD. Returns False on any error (non-git dirs, etc.).
357
+ """
358
+ try:
359
+ result = subprocess.run(
360
+ ["git", "-C", str(repo_root), "status", "--porcelain"],
361
+ capture_output=True,
362
+ text=True,
363
+ timeout=2,
364
+ )
365
+ if result.returncode == 0:
366
+ return bool(result.stdout.strip())
367
+ except Exception:
368
+ pass
369
+ return False
370
+
371
+
352
372
  def get_cold_start_context(repo_root: Path) -> dict:
353
373
  """Return a lightweight bootstrap object from the persisted RIS.
354
374
 
@@ -361,28 +381,39 @@ def get_cold_start_context(repo_root: Path) -> dict:
361
381
 
362
382
  current_head = _current_git_head(repo_root)
363
383
  stale = bool(current_head and ris.git_head and current_head != ris.git_head)
384
+ uncommitted = _has_uncommitted_changes(repo_root)
364
385
 
365
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)
366
396
  result: dict = {
367
397
  "status": "cold_start_stale" if stale else "cold_start_ready",
368
398
  "repo_id": ris.repo_id,
369
399
  "git_head": ris.git_head,
400
+ "current_git_head": current_head,
370
401
  "stale": stale,
402
+ "has_uncommitted_changes": uncommitted,
371
403
  "last_updated_at": ris.last_updated_at,
404
+ "cache_source": "RIS",
405
+ "data_scope": "RIS_BOOTSTRAP",
406
+ "api_surface_complete": _api_complete,
372
407
  "summary": ris.compact_summary,
373
408
  "entrypoints": ris.structural_map.get("entrypoints", []),
374
409
  "endpoints": endpoints,
375
410
  "hotspots": ris.git_context_snapshot.get("hotspots", []),
376
411
  }
377
- if not endpoints:
378
- _is_java = (repo_root / "pom.xml").exists() or \
379
- (repo_root / "build.gradle").exists() or \
380
- (repo_root / "build.gradle.kts").exists()
381
- if _is_java:
382
- result["endpoints_hint"] = (
383
- "Java repo detected but no endpoint index found. "
384
- "Call get_endpoints (or: sourcecode endpoints <path>) to populate."
385
- )
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
+ )
386
417
  return result
387
418
  except Exception:
388
419
  return {"status": "no_ris"}
sourcecode/serializer.py CHANGED
@@ -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
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.1
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.1-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.1
116
+ # sourcecode 1.33.3
117
117
  ```
118
118
 
119
119
  ---
@@ -1,13 +1,13 @@
1
- sourcecode/__init__.py,sha256=etbaHEAFq4Y5ytZacDtpQYJt_hteBOpOxKlJl-EIGJY,103
1
+ sourcecode/__init__.py,sha256=qhL6fTfXMRERDWiwrW3g08_u6w-wnYzM6RnzJHpyq2M,103
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/architecture_analyzer.py,sha256=qh749a7ykPtGmQI1MR9y6j8TtL_jBdVYFx9YRsLqOMw,44121
4
4
  sourcecode/architecture_summary.py,sha256=z34_6v7cSwy98cof2UVciGho7SCrZ93tiqMmq5WNzRQ,20405
5
5
  sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,50578
6
- sourcecode/cache.py,sha256=h1BT-9PG_7HK---ZzH0j5u3PN0dz2s6IRAUOjQIPYH4,28055
6
+ sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
7
7
  sourcecode/cache.tmp_new,sha256=-IvV7CojiZjqeKMln1m-lqI0QVA2uFGWmYir4XRFOUk,27970
8
8
  sourcecode/canonical_ir.py,sha256=_HM3AUmKSdna9u4dCoU6rpgSA6HdF8gzOKZykIUCNGY,23277
9
9
  sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
10
- sourcecode/cli.py,sha256=aoy3H0QhffBj6CLMGHDMlhtW2TQOwrJfK8LFEsajJFM,173536
10
+ sourcecode/cli.py,sha256=ithP9KPwvJsoSH1nOPYw6LDXQqHa5sIP6Xpt8G1MgK8,180063
11
11
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
12
12
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
13
13
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
@@ -36,12 +36,12 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
36
36
  sourcecode/relevance_scorer.py,sha256=MYF4FFkveAQps9SmTeTlh6ODiBz2F--_hWNeHMLtUHQ,8405
37
37
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
38
38
  sourcecode/repository_ir.py,sha256=-NjBQUT7zyya4ng8Hq0-ChoiHZkUif9lr-Q878gmj8M,153163
39
- sourcecode/ris.py,sha256=2pQcNN-5akweoBjjWAk_x3QpJ06wukQInmMlMj7WxwI,14737
39
+ sourcecode/ris.py,sha256=G89xhjg-JAQXsi4ox2Z3HGwWiOd6sWNPQqWRbUT-MPk,15927
40
40
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
41
41
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
42
42
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
43
43
  sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
44
- sourcecode/serializer.py,sha256=3JBvsMDj5pt1RXpi1zJpk5DGWUKbeb2Jl622-kmYWD4,123312
44
+ sourcecode/serializer.py,sha256=ooNZW2_fqx__BXII25eAWq-BomodvqQ6opUT_niQYCA,123403
45
45
  sourcecode/summarizer.py,sha256=YspHEVeYJVmltq0FMtGZF8kIP3qiR2KLcanGL6Y7uTI,20747
46
46
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
47
47
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -68,7 +68,7 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
68
68
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
69
69
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
70
70
  sourcecode/mcp/runner.py,sha256=YSw2DXEICau6mCBr3Gfia3D_tKxMbRvIIXEh4cHC1SY,1390
71
- sourcecode/mcp/server.py,sha256=jUCDNjJKUIhse52M8-Lp63CDxlm21xOYUOVlv3z6_uk,25556
71
+ sourcecode/mcp/server.py,sha256=dAmoUbnu4kPPWeRH2tcOn9WJPG8Zfzvlu0nRmyiok70,27925
72
72
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
73
73
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
74
74
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -80,8 +80,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
80
80
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
81
81
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
82
82
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
83
- sourcecode-1.33.1.dist-info/METADATA,sha256=NqcQIRQo2sldQvv32_3mF9QUqPAxJR5LO4kgdBaojY0,16440
84
- sourcecode-1.33.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
85
- sourcecode-1.33.1.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
86
- sourcecode-1.33.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
87
- sourcecode-1.33.1.dist-info/RECORD,,
83
+ sourcecode-1.33.3.dist-info/METADATA,sha256=2PnPfTRRy6IDMJajZkRqfE8M_j0HJjYjOVuwRBlKfYg,16440
84
+ sourcecode-1.33.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
85
+ sourcecode-1.33.3.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
86
+ sourcecode-1.33.3.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
87
+ sourcecode-1.33.3.dist-info/RECORD,,