sourcecode 1.33.0__tar.gz → 1.33.2__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.0 → sourcecode-1.33.2}/PKG-INFO +3 -3
  2. {sourcecode-1.33.0 → sourcecode-1.33.2}/README.md +2 -2
  3. {sourcecode-1.33.0 → sourcecode-1.33.2}/pyproject.toml +1 -1
  4. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/cache.py +71 -4
  6. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/cli.py +94 -7
  7. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/ris.py +25 -0
  8. {sourcecode-1.33.0 → sourcecode-1.33.2}/.github/workflows/build-windows.yml +0 -0
  9. {sourcecode-1.33.0 → sourcecode-1.33.2}/.gitignore +0 -0
  10. {sourcecode-1.33.0 → sourcecode-1.33.2}/.ruff.toml +0 -0
  11. {sourcecode-1.33.0 → sourcecode-1.33.2}/CHANGELOG.md +0 -0
  12. {sourcecode-1.33.0 → sourcecode-1.33.2}/CONTRIBUTING.md +0 -0
  13. {sourcecode-1.33.0 → sourcecode-1.33.2}/LICENSE +0 -0
  14. {sourcecode-1.33.0 → sourcecode-1.33.2}/SECURITY.md +0 -0
  15. {sourcecode-1.33.0 → sourcecode-1.33.2}/raw +0 -0
  16. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/adaptive_scanner.py +0 -0
  17. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/architecture_analyzer.py +0 -0
  18. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/architecture_summary.py +0 -0
  19. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/ast_extractor.py +0 -0
  20. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/cache.tmp_new +0 -0
  21. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/canonical_ir.py +0 -0
  22. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/classifier.py +0 -0
  23. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/code_notes_analyzer.py +0 -0
  24. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/confidence_analyzer.py +0 -0
  25. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/context_scorer.py +0 -0
  26. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/context_summarizer.py +0 -0
  27. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/contract_model.py +0 -0
  28. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/contract_pipeline.py +0 -0
  29. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/coverage_parser.py +0 -0
  30. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/dependency_analyzer.py +0 -0
  31. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/__init__.py +0 -0
  32. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/base.py +0 -0
  33. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/csproj_parser.py +0 -0
  34. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/dart.py +0 -0
  35. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/dotnet.py +0 -0
  36. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/elixir.py +0 -0
  37. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/go.py +0 -0
  38. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/heuristic.py +0 -0
  39. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/hybrid.py +0 -0
  40. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/java.py +0 -0
  41. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/jvm_ext.py +0 -0
  42. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/nodejs.py +0 -0
  43. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/parsers.py +0 -0
  44. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/php.py +0 -0
  45. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/project.py +0 -0
  46. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/python.py +0 -0
  47. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/ruby.py +0 -0
  48. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/rust.py +0 -0
  49. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/systems.py +0 -0
  50. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/terraform.py +0 -0
  51. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/detectors/tooling.py +0 -0
  52. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/doc_analyzer.py +0 -0
  53. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/entrypoint_classifier.py +0 -0
  54. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/env_analyzer.py +0 -0
  55. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/file_classifier.py +0 -0
  56. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/flow_analyzer.py +0 -0
  57. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/git_analyzer.py +0 -0
  58. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/graph_analyzer.py +0 -0
  59. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/license.py +0 -0
  60. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/__init__.py +0 -0
  61. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  62. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  63. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  64. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  65. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  66. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/runner.py +0 -0
  67. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp/server.py +0 -0
  68. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/mcp_nudge.py +0 -0
  69. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/metrics_analyzer.py +0 -0
  70. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/output_budget.py +0 -0
  71. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/path_filters.py +0 -0
  72. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/pr_comment_renderer.py +0 -0
  73. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/prepare_context.py +0 -0
  74. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/progress.py +0 -0
  75. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/ranking_engine.py +0 -0
  76. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/redactor.py +0 -0
  77. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/relevance_scorer.py +0 -0
  78. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/repo_classifier.py +0 -0
  79. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/repository_ir.py +0 -0
  80. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/runtime_classifier.py +0 -0
  81. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/scanner.py +0 -0
  82. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/schema.py +0 -0
  83. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/semantic_analyzer.py +0 -0
  84. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/serializer.py +0 -0
  85. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/summarizer.py +0 -0
  86. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/__init__.py +0 -0
  87. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/config.py +0 -0
  88. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/consent.py +0 -0
  89. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/events.py +0 -0
  90. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/filters.py +0 -0
  91. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/telemetry/transport.py +0 -0
  92. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/tree_utils.py +0 -0
  93. {sourcecode-1.33.0 → sourcecode-1.33.2}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.0
3
+ Version: 1.33.2
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.0-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.33.2-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.0
116
+ # sourcecode 1.33.2
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.0-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.33.2-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.0
79
+ # sourcecode 1.33.2
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.0"
7
+ version = "1.33.2"
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.0"
3
+ __version__ = "1.33.2"
@@ -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
 
@@ -219,6 +219,8 @@ _SUBCOMMANDS: frozenset[str] = frozenset(
219
219
  "activate",
220
220
  # Cache observability
221
221
  "cache",
222
+ # RIS bootstrap
223
+ "cold-start",
222
224
  }
223
225
  )
224
226
 
@@ -1056,6 +1058,22 @@ def main(
1056
1058
  code_notes = True
1057
1059
  architecture = True
1058
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
+
1059
1077
  # ── Two-layer cache ────────────────────────────────────────────────────────
1060
1078
  # L1 (core): (repo, commit, analysis_flags) → pre-computed view data dict
1061
1079
  # key = core-<git_sha>-<analysis_hash>.json.gz
@@ -1200,6 +1218,23 @@ def main(
1200
1218
 
1201
1219
  if _cache_hit_content is not None:
1202
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
+ })
1203
1238
  write_output(_cache_hit_content, output=output)
1204
1239
  if copy and not output:
1205
1240
  _copy_to_clipboard(_cache_hit_content)
@@ -1838,6 +1873,7 @@ def main(
1838
1873
  _allowed_changed_files: Optional[set[str]] = None
1839
1874
  if changed_only:
1840
1875
  from sourcecode.git_analyzer import GitAnalyzer as _GitAnalyzerEarly
1876
+ _git_confirmed_clean = False
1841
1877
  try:
1842
1878
  _gc_early = _GitAnalyzerEarly().analyze(target, depth=1, days=1)
1843
1879
  _bad_gc = {"no_git_repo", "git_not_found", "git_timeout"}
@@ -1846,15 +1882,31 @@ def main(
1846
1882
  if _uc:
1847
1883
  # WORKTREE_UNSTAGED + WORKTREE_STAGED only; untracked excluded
1848
1884
  _allowed_changed_files = set(_uc.staged) | set(_uc.unstaged)
1849
- if not _allowed_changed_files:
1885
+ if not _allowed_changed_files:
1886
+ # Git is available and confirms no uncommitted changes.
1887
+ # Do NOT fall back to a full scan — that would silently produce
1888
+ # output identical to --compact, making it impossible for the
1889
+ # caller to distinguish "no changes" from "changes found".
1890
+ _git_confirmed_clean = True
1891
+ else:
1892
+ # Git unavailable — fall back gracefully.
1850
1893
  typer.echo(
1851
- "[changed-only] git unavailable or no uncommitted changes — falling back to full scan.",
1894
+ "[changed-only] git unavailable — falling back to full scan.",
1852
1895
  err=True,
1853
1896
  )
1854
1897
  changed_only = False
1855
1898
  except Exception:
1856
1899
  typer.echo("[changed-only] git error — falling back to full scan.", err=True)
1857
1900
  changed_only = False
1901
+ if _git_confirmed_clean:
1902
+ _nc_payload = json.dumps({
1903
+ "status": "working_tree_clean",
1904
+ "no_changes": True,
1905
+ "changed_files": [],
1906
+ "message": "No uncommitted changes detected — working tree is clean.",
1907
+ }, ensure_ascii=False)
1908
+ write_output(_nc_payload, output=output)
1909
+ raise typer.Exit()
1858
1910
 
1859
1911
  # Contract pipeline — runs for mode=contract|standard|deep|hybrid (skip for raw)
1860
1912
  _progress.update("extracting contracts")
@@ -2063,6 +2115,23 @@ def main(
2063
2115
 
2064
2116
  # 6. Write output (CLI-04)
2065
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
+ })
2066
2135
  write_output(content, output=output)
2067
2136
 
2068
2137
  # Persist to two-layer cache (git SHA unchanged → re-use on next run).
@@ -2539,9 +2608,14 @@ def prepare_context_cmd(
2539
2608
  if _task_include("confidence"):
2540
2609
  out["confidence"] = output.confidence
2541
2610
  if task != "review-pr" and _task_include("relevant_files"):
2611
+ _rfs = output.relevant_files
2612
+ if task == "generate-tests":
2613
+ # relevant_files goal: untested SOURCE files. Test files belong in test_gaps.
2614
+ # Without this filter, high-churn test files rank above untested source files.
2615
+ _rfs = [f for f in _rfs if getattr(f, "role", None) != "test"]
2542
2616
  out["relevant_files"] = [
2543
2617
  _serialize_relevant_file(f)
2544
- for f in output.relevant_files
2618
+ for f in _rfs
2545
2619
  ]
2546
2620
  if _task_include("key_dependencies") and output.key_dependencies:
2547
2621
  out["key_dependencies"] = output.key_dependencies
@@ -4207,23 +4281,36 @@ def cache_status_cmd(
4207
4281
  else:
4208
4282
  typer.echo(f"Cache dir: {stats['cache_dir']}")
4209
4283
  typer.echo(f"Cores: {stats['cores']}")
4210
- typer.echo(f"Snapshots: {stats['snapshots']}")
4211
4284
  typer.echo(f"Views: {stats['views']}")
4212
4285
  typer.echo(f"CAS blobs: {stats['cas_blobs']}")
4213
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']}")
4214
4295
 
4215
4296
 
4216
4297
  @cache_app.command("clear")
4217
4298
  def cache_clear_cmd(
4218
4299
  path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
4219
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."),
4220
4302
  ) -> None:
4221
- """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
+ """
4222
4308
  from sourcecode import cache as _cm
4223
4309
  target = Path(path).resolve()
4224
4310
  if not yes:
4225
- typer.confirm(f"Delete all cache files for {target}?", abort=True)
4226
- 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)
4227
4314
  typer.echo(f"Removed {removed} file(s).")
4228
4315
 
4229
4316
 
@@ -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,14 +381,19 @@ 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", [])
366
387
  result: dict = {
367
388
  "status": "cold_start_stale" if stale else "cold_start_ready",
368
389
  "repo_id": ris.repo_id,
369
390
  "git_head": ris.git_head,
391
+ "current_git_head": current_head,
370
392
  "stale": stale,
393
+ "has_uncommitted_changes": uncommitted,
371
394
  "last_updated_at": ris.last_updated_at,
395
+ "cache_source": "RIS",
396
+ "data_scope": "RIS_BOOTSTRAP",
372
397
  "summary": ris.compact_summary,
373
398
  "entrypoints": ris.structural_map.get("entrypoints", []),
374
399
  "endpoints": endpoints,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes