sourcecode 1.33.14__tar.gz → 1.33.16__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 (95) hide show
  1. {sourcecode-1.33.14 → sourcecode-1.33.16}/PKG-INFO +1 -1
  2. {sourcecode-1.33.14 → sourcecode-1.33.16}/pyproject.toml +1 -1
  3. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/__init__.py +1 -1
  4. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/cli.py +20 -4
  5. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/file_classifier.py +9 -6
  6. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/server.py +50 -1
  7. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/prepare_context.py +31 -15
  8. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/ris.py +12 -0
  9. {sourcecode-1.33.14 → sourcecode-1.33.16}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.33.14 → sourcecode-1.33.16}/.gitignore +0 -0
  11. {sourcecode-1.33.14 → sourcecode-1.33.16}/.ruff.toml +0 -0
  12. {sourcecode-1.33.14 → sourcecode-1.33.16}/CHANGELOG.md +0 -0
  13. {sourcecode-1.33.14 → sourcecode-1.33.16}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.33.14 → sourcecode-1.33.16}/LICENSE +0 -0
  15. {sourcecode-1.33.14 → sourcecode-1.33.16}/README.md +0 -0
  16. {sourcecode-1.33.14 → sourcecode-1.33.16}/SECURITY.md +0 -0
  17. {sourcecode-1.33.14 → sourcecode-1.33.16}/raw +0 -0
  18. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/adaptive_scanner.py +0 -0
  19. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/architecture_analyzer.py +0 -0
  20. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/architecture_summary.py +0 -0
  21. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/ast_extractor.py +0 -0
  22. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/cache.py +0 -0
  23. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/canonical_ir.py +0 -0
  24. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/error_schema.py +0 -0
  58. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/graph_analyzer.py +0 -0
  61. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/license.py +0 -0
  62. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/__init__.py +0 -0
  63. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  64. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  65. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  66. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  67. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  68. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/orchestrator.py +0 -0
  69. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/registry.py +0 -0
  70. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp/runner.py +0 -0
  71. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/mcp_nudge.py +0 -0
  72. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/metrics_analyzer.py +0 -0
  73. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/output_budget.py +0 -0
  74. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/path_filters.py +0 -0
  75. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/pr_comment_renderer.py +0 -0
  76. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/progress.py +0 -0
  77. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/ranking_engine.py +0 -0
  78. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/redactor.py +0 -0
  79. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/relevance_scorer.py +0 -0
  80. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/repo_classifier.py +0 -0
  81. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/repository_ir.py +0 -0
  82. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/runtime_classifier.py +0 -0
  83. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/scanner.py +0 -0
  84. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/schema.py +0 -0
  85. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/semantic_analyzer.py +0 -0
  86. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/serializer.py +0 -0
  87. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/summarizer.py +0 -0
  88. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/__init__.py +0 -0
  89. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/config.py +0 -0
  90. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/consent.py +0 -0
  91. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/events.py +0 -0
  92. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/filters.py +0 -0
  93. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/telemetry/transport.py +0 -0
  94. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/tree_utils.py +0 -0
  95. {sourcecode-1.33.14 → sourcecode-1.33.16}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.14
3
+ Version: 1.33.16
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
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "1.33.14"
7
+ version = "1.33.16"
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.14"
3
+ __version__ = "1.33.16"
@@ -1141,9 +1141,23 @@ def main(
1141
1141
  capture_output=True, text=True, timeout=3,
1142
1142
  )
1143
1143
  _git_sha = _sha_r.stdout.strip()
1144
- # Only cache when target IS the git repo root (not a subdir of one),
1145
- # to avoid polluting sub-project directories used in tests.
1146
- if _git_sha and (target / ".git").exists():
1144
+
1145
+ # Detect actual git root (may be an ancestor of target for monorepos,
1146
+ # multi-project repos, or SVN-migrated trees where .git is in a parent).
1147
+ # The original "(target / '.git').exists()" check broke these layouts.
1148
+ _git_root_str = ""
1149
+ if _git_sha:
1150
+ try:
1151
+ _gr_r = _sub.run(
1152
+ ["git", "-C", str(target), "rev-parse", "--show-toplevel"],
1153
+ capture_output=True, text=True, timeout=3,
1154
+ )
1155
+ if _gr_r.returncode == 0:
1156
+ _git_root_str = _gr_r.stdout.strip()
1157
+ except Exception:
1158
+ pass
1159
+
1160
+ if _git_sha and _git_root_str:
1147
1161
  _excl_key = (
1148
1162
  ",".join(sorted(e.strip() for e in exclude.split(",") if e.strip()))
1149
1163
  if exclude else ""
@@ -2241,7 +2255,9 @@ def main(
2241
2255
  _atexit.register(_cache_write_thread.join, 5.0)
2242
2256
 
2243
2257
  # Update RIS with aggregated snapshot data (non-fatal side-effect).
2244
- if not no_cache and not _pipeline_error and _core_key:
2258
+ # Update RIS whenever git is available, even when L1/L2 cache is skipped
2259
+ # (e.g. target is a subdirectory of the git root — _core_key may be "").
2260
+ if not no_cache and not _pipeline_error and _git_sha:
2245
2261
  try:
2246
2262
  from sourcecode.serializer import core_view as _ris_core_view
2247
2263
  from sourcecode.ris import maybe_update_ris as _ris_update
@@ -187,16 +187,19 @@ class FileClassifier:
187
187
  if java_class is not None:
188
188
  return java_class
189
189
 
190
- if self._has_any_import(imports, _API_IMPORTS):
191
- evidence = self._matched_imports(imports, _API_IMPORTS)
190
+ # Fix 4: call _matched_imports once per category instead of twice
191
+ # (_has_any_import was calling _matched_imports and discarding the result,
192
+ # then the caller invoked it again to get the evidence — halving throughput).
193
+ evidence = self._matched_imports(imports, _API_IMPORTS)
194
+ if evidence:
192
195
  return FileClassification(norm, "api_layer", "high", 0.82, "imports API/server framework", evidence)
193
196
 
194
- if self._has_any_import(imports, _DB_IMPORTS):
195
- evidence = self._matched_imports(imports, _DB_IMPORTS)
197
+ evidence = self._matched_imports(imports, _DB_IMPORTS)
198
+ if evidence:
196
199
  return FileClassification(norm, "database_layer", "high", 0.78, "imports database/persistence dependency", evidence)
197
200
 
198
- if self._has_any_import(imports, _INFRA_IMPORTS):
199
- evidence = self._matched_imports(imports, _INFRA_IMPORTS)
201
+ evidence = self._matched_imports(imports, _INFRA_IMPORTS)
202
+ if evidence:
200
203
  return FileClassification(norm, "infrastructure", "high", 0.72, "imports infrastructure dependency", evidence)
201
204
 
202
205
  role = self._package_role(norm)
@@ -650,7 +650,56 @@ def check_freshness(repo_path: str = ".") -> dict:
650
650
  _path_err = _check_repo_path(repo_path)
651
651
  if _path_err is not None:
652
652
  return _path_err
653
- return _execute(["cache", "freshness", repo_path, "--json"])
653
+
654
+ # Call Python functions directly — avoids CliRunner/subprocess nesting
655
+ # that caused current_git_head to return "" on Windows parent-git repos.
656
+ import subprocess as _sp
657
+ from pathlib import Path as _Path
658
+ from sourcecode.cache import _get_git_head as _cache_head
659
+ from sourcecode.ris import load_ris as _load_ris, _has_uncommitted_changes as _huc
660
+
661
+ target = _Path(repo_path).resolve()
662
+ current_head = _cache_head(target)
663
+ ris = _load_ris(target)
664
+
665
+ if ris is None:
666
+ result: dict = {
667
+ "fresh": False,
668
+ "ris_exists": False,
669
+ "current_git_head": current_head,
670
+ "ris_git_head": None,
671
+ "delta_commits": None,
672
+ "has_uncommitted_changes": _huc(target),
673
+ "ris_last_updated_at": None,
674
+ }
675
+ else:
676
+ ris_head = ris.git_head
677
+ head_matches = bool(current_head and ris_head and current_head == ris_head)
678
+ uncommitted = _huc(target)
679
+ delta = None
680
+ if ris_head and current_head and ris_head != current_head:
681
+ try:
682
+ _r = _sp.run(
683
+ ["git", "-C", str(target), "rev-list", "--count", f"{ris_head}..HEAD"],
684
+ capture_output=True, text=True, timeout=5,
685
+ )
686
+ if _r.returncode == 0:
687
+ delta = int(_r.stdout.strip())
688
+ except Exception:
689
+ pass
690
+ elif head_matches:
691
+ delta = 0
692
+ result = {
693
+ "fresh": head_matches and not uncommitted,
694
+ "ris_exists": True,
695
+ "current_git_head": current_head,
696
+ "ris_git_head": ris_head,
697
+ "delta_commits": delta,
698
+ "has_uncommitted_changes": uncommitted,
699
+ "ris_last_updated_at": ris.last_updated_at,
700
+ }
701
+
702
+ return _ok(result)
654
703
  except Exception as exc:
655
704
  return _err(
656
705
  f"Internal error: {type(exc).__name__}: {exc} — repo_path: {_raw}",
@@ -1750,8 +1750,14 @@ class TaskContextBuilder:
1750
1750
  for _cr in _commits_scanned:
1751
1751
  _msg_lower = (_cr.message or "").lower()
1752
1752
  if _kw_re.search(_msg_lower):
1753
+ _rn_prefix = self.root.name + "/"
1753
1754
  for _cf in (_cr.files_changed or []):
1754
1755
  _cf_norm = _cf.replace("\\", "/")
1756
+ # Git reports paths relative to the git root, which may be
1757
+ # a parent of the analyzed directory (e.g. MSAS/saint-server/).
1758
+ # Strip the analyzed-dir prefix so paths match all_paths.
1759
+ if _cf_norm.startswith(_rn_prefix):
1760
+ _cf_norm = _cf_norm[len(_rn_prefix):]
1755
1761
  _commit_file_hits[_cf_norm] = _commit_file_hits.get(_cf_norm, 0) + 1
1756
1762
  _sx_commits.append({
1757
1763
  "message": (_cr.message or "")[:80],
@@ -1836,6 +1842,15 @@ class TaskContextBuilder:
1836
1842
  # the candidate pool (e.g. AkitaBaseService containing setLoading).
1837
1843
  _src_exts = frozenset({".java", ".py", ".ts", ".js", ".kt", ".go"})
1838
1844
  _frontend_kws = [kw for kw in symptom_keywords if kw in _FRONTEND_SYMPTOM_MAP]
1845
+ # Fix 5: In large repos, skip frontend→backend synonym grep for keywords
1846
+ # that already have direct path matches — those are backend terms (e.g.
1847
+ # "login" in an IAM repo) that don't need UI→service-layer translation.
1848
+ # Prevents "authentication" grep flooding keycloak with SAML adapter files.
1849
+ if _is_large_repo and _frontend_kws:
1850
+ _frontend_kws = [
1851
+ kw for kw in _frontend_kws
1852
+ if not any(kw in p.lower() for p in _sx_direct_path)
1853
+ ]
1839
1854
  _backend_terms_set: list[str] = []
1840
1855
  if _frontend_kws:
1841
1856
  _bt: list[str] = []
@@ -1923,6 +1938,7 @@ class TaskContextBuilder:
1923
1938
  _no_scan_candidates = relevant_files[_CONTENT_SCAN_LIMIT:]
1924
1939
 
1925
1940
  _boosted: list[RelevantFile] = []
1941
+ _raw_signals: dict[str, float] = {} # uncapped accumulated signal per file
1926
1942
  _scanned_body: dict[str, str] = {} # cache for graph expansion (Pass 5)
1927
1943
  for _rf in _scan_candidates:
1928
1944
  _extra = 0.0
@@ -1996,7 +2012,9 @@ class TaskContextBuilder:
1996
2012
  elif _extra_syn > 0:
1997
2013
  _new_reason = _rf.reason + f", synonym-match backend (+{_extra_syn:.2f})"
1998
2014
 
1999
- _final_score = round(min(_rf.score + _total_extra, 1.0), 2)
2015
+ _raw_signal = _rf.score + _total_extra # uncapped for ranking
2016
+ _raw_signals[_rf.path] = _raw_signal
2017
+ _final_score = round(min(_raw_signal, 1.0), 2)
2000
2018
  _boosted.append(RelevantFile(
2001
2019
  path=_rf.path,
2002
2020
  role=_rf.role,
@@ -2005,21 +2023,14 @@ class TaskContextBuilder:
2005
2023
  why=_rf.why,
2006
2024
  ))
2007
2025
 
2008
- # Use total boost as a secondary sort key so symptom-matched files
2009
- # that were boosted from a lower base score rank above structural
2010
- # files that coincidentally reach the same capped score of 1.0.
2011
- # This prevents budget-trimming from discarding the most relevant files.
2012
- _boost_totals: dict[str, float] = {}
2013
- for _rf in _scan_candidates:
2014
- pass # populated below
2015
- _boost_totals = {}
2016
- for _idx, _rf in enumerate(_scan_candidates):
2017
- _b_rf = _boosted[_idx]
2018
- _boost_totals[_b_rf.path] = round(_b_rf.score - _rf.score, 4)
2019
-
2026
+ # Sort by uncapped raw signal so files with more accumulated evidence
2027
+ # (path matches + content hits + commit matches) rank above files that
2028
+ # merely cap at the same display score of 1.0.
2029
+ # _raw_signals holds each file's full sum before the display cap.
2030
+ # Files not content-scanned (_no_scan_candidates) use their base score.
2020
2031
  relevant_files = sorted(
2021
2032
  _boosted + _no_scan_candidates,
2022
- key=lambda rf: (-rf.score, -_boost_totals.get(rf.path, 0)),
2033
+ key=lambda rf: -_raw_signals.get(rf.path, rf.score),
2023
2034
  )
2024
2035
 
2025
2036
  # Pass 5: reverse graph expansion from high-score seed nodes.
@@ -2118,9 +2129,14 @@ class TaskContextBuilder:
2118
2129
  if _gx_new:
2119
2130
  relevant_files = sorted(
2120
2131
  relevant_files + _gx_new,
2121
- key=lambda rf: (-rf.score, -_boost_totals.get(rf.path, 0)),
2132
+ key=lambda rf: -_raw_signals.get(rf.path, rf.score),
2122
2133
  )
2123
2134
 
2135
+ # Fix 2: Cap output for large repos to stay within agent context budgets.
2136
+ # Raw signal sort above ensures highest-signal files survive the cut.
2137
+ if _is_large_repo and len(relevant_files) > 40:
2138
+ relevant_files = relevant_files[:40]
2139
+
2124
2140
  # Synonym note (only when synonyms actually fired)
2125
2141
  if _frontend_kws and _sx_synonyms:
2126
2142
  symptom_note = (
@@ -437,6 +437,18 @@ def get_cold_start_context(repo_root: Path) -> dict:
437
437
  "endpoints": endpoints,
438
438
  "hotspots": ris.git_context_snapshot.get("hotspots", []),
439
439
  "validation": _validation,
440
+ # Fix 3: _cache wrapper for backward compat with CLI schema consumers.
441
+ # CLI outputs inject _cache via _inject_cache_meta; MCP cold-start path
442
+ # skips that step, leaving agents that read _cache.cache_source with None.
443
+ "_cache": {
444
+ "cache_source": "RIS",
445
+ "git_head_at_generation": ris.git_head or "",
446
+ "current_git_head": current_head or "",
447
+ "is_stale": stale,
448
+ "has_uncommitted_changes": uncommitted,
449
+ "generated_at": ris.last_updated_at,
450
+ "data_scope": "RIS_BOOTSTRAP",
451
+ },
440
452
  }
441
453
  if not endpoints and _is_java:
442
454
  result["endpoints_hint"] = (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes