sourcecode 1.33.3__tar.gz → 1.33.5__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.3 → sourcecode-1.33.5}/PKG-INFO +3 -3
  2. {sourcecode-1.33.3 → sourcecode-1.33.5}/README.md +2 -2
  3. {sourcecode-1.33.3 → sourcecode-1.33.5}/pyproject.toml +1 -1
  4. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/cli.py +38 -12
  6. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/prepare_context.py +4 -0
  7. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/repository_ir.py +59 -6
  8. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/ris.py +31 -2
  9. {sourcecode-1.33.3 → sourcecode-1.33.5}/.github/workflows/build-windows.yml +0 -0
  10. {sourcecode-1.33.3 → sourcecode-1.33.5}/.gitignore +0 -0
  11. {sourcecode-1.33.3 → sourcecode-1.33.5}/.ruff.toml +0 -0
  12. {sourcecode-1.33.3 → sourcecode-1.33.5}/CHANGELOG.md +0 -0
  13. {sourcecode-1.33.3 → sourcecode-1.33.5}/CONTRIBUTING.md +0 -0
  14. {sourcecode-1.33.3 → sourcecode-1.33.5}/LICENSE +0 -0
  15. {sourcecode-1.33.3 → sourcecode-1.33.5}/SECURITY.md +0 -0
  16. {sourcecode-1.33.3 → sourcecode-1.33.5}/raw +0 -0
  17. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/adaptive_scanner.py +0 -0
  18. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/architecture_analyzer.py +0 -0
  19. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/architecture_summary.py +0 -0
  20. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/ast_extractor.py +0 -0
  21. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/cache.py +0 -0
  22. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/cache.tmp_new +0 -0
  23. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/canonical_ir.py +0 -0
  24. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/classifier.py +0 -0
  25. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/code_notes_analyzer.py +0 -0
  26. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/confidence_analyzer.py +0 -0
  27. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/context_scorer.py +0 -0
  28. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/context_summarizer.py +0 -0
  29. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/contract_model.py +0 -0
  30. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/contract_pipeline.py +0 -0
  31. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/coverage_parser.py +0 -0
  32. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/dependency_analyzer.py +0 -0
  33. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/__init__.py +0 -0
  34. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/base.py +0 -0
  35. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/csproj_parser.py +0 -0
  36. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/dart.py +0 -0
  37. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/dotnet.py +0 -0
  38. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/elixir.py +0 -0
  39. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/go.py +0 -0
  40. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/heuristic.py +0 -0
  41. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/hybrid.py +0 -0
  42. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/java.py +0 -0
  43. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/jvm_ext.py +0 -0
  44. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/nodejs.py +0 -0
  45. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/parsers.py +0 -0
  46. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/php.py +0 -0
  47. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/project.py +0 -0
  48. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/python.py +0 -0
  49. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/ruby.py +0 -0
  50. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/rust.py +0 -0
  51. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/systems.py +0 -0
  52. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/terraform.py +0 -0
  53. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/detectors/tooling.py +0 -0
  54. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/doc_analyzer.py +0 -0
  55. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/entrypoint_classifier.py +0 -0
  56. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/env_analyzer.py +0 -0
  57. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/file_classifier.py +0 -0
  58. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/flow_analyzer.py +0 -0
  59. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/git_analyzer.py +0 -0
  60. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/graph_analyzer.py +0 -0
  61. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/license.py +0 -0
  62. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/__init__.py +0 -0
  63. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/onboarding/__init__.py +0 -0
  64. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/onboarding/applier.py +0 -0
  65. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/onboarding/backup.py +0 -0
  66. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/onboarding/detector.py +0 -0
  67. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/onboarding/planner.py +0 -0
  68. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/runner.py +0 -0
  69. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp/server.py +0 -0
  70. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/mcp_nudge.py +0 -0
  71. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/metrics_analyzer.py +0 -0
  72. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/output_budget.py +0 -0
  73. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/path_filters.py +0 -0
  74. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/pr_comment_renderer.py +0 -0
  75. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/progress.py +0 -0
  76. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/ranking_engine.py +0 -0
  77. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/redactor.py +0 -0
  78. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/relevance_scorer.py +0 -0
  79. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/repo_classifier.py +0 -0
  80. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/runtime_classifier.py +0 -0
  81. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/scanner.py +0 -0
  82. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/schema.py +0 -0
  83. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/semantic_analyzer.py +0 -0
  84. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/serializer.py +0 -0
  85. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/summarizer.py +0 -0
  86. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/__init__.py +0 -0
  87. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/config.py +0 -0
  88. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/consent.py +0 -0
  89. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/events.py +0 -0
  90. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/filters.py +0 -0
  91. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/telemetry/transport.py +0 -0
  92. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/tree_utils.py +0 -0
  93. {sourcecode-1.33.3 → sourcecode-1.33.5}/src/sourcecode/workspace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.3
3
+ Version: 1.33.5
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.3-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.33.4-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.3
116
+ # sourcecode 1.33.4
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.3-blue)
5
+ ![Version](https://img.shields.io/badge/version-1.33.4-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.3
79
+ # sourcecode 1.33.4
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.3"
7
+ version = "1.33.5"
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.3"
3
+ __version__ = "1.33.5"
@@ -658,7 +658,6 @@ def main(
658
658
  env_map: bool = typer.Option(
659
659
  False,
660
660
  "--env-map",
661
- hidden=True,
662
661
  help="Map environment variables referenced across the codebase.",
663
662
  ),
664
663
  code_notes: bool = typer.Option(
@@ -1226,13 +1225,24 @@ def main(
1226
1225
  _uncommitted = False
1227
1226
  _hit_source = "L2_view" if (_view_key and _core_hash) else "L1_core"
1228
1227
  _data_scope = "COMPACT" if compact else ("AGENT" if agent else "FULL")
1228
+ # Recover generated_at from cached content before overwriting _cache block.
1229
+ _cached_generated_at = None
1230
+ try:
1231
+ import json as _json_ga
1232
+ _cached_generated_at = (
1233
+ _json_ga.loads(_cache_hit_content)
1234
+ .get("_cache", {})
1235
+ .get("generated_at")
1236
+ )
1237
+ except Exception:
1238
+ pass
1229
1239
  _cache_hit_content = _inject_cache_meta(_cache_hit_content, {
1230
1240
  "cache_source": _hit_source,
1231
1241
  "git_head_at_generation": _git_sha,
1232
1242
  "current_git_head": _git_sha,
1233
1243
  "is_stale": False,
1234
1244
  "has_uncommitted_changes": _uncommitted,
1235
- "generated_at": None,
1245
+ "generated_at": _cached_generated_at,
1236
1246
  "data_scope": _data_scope,
1237
1247
  })
1238
1248
  write_output(_cache_hit_content, output=output)
@@ -1880,8 +1890,9 @@ def main(
1880
1890
  if _gc_early and not (_bad_gc & set(_gc_early.limitations)):
1881
1891
  _uc = _gc_early.uncommitted_changes
1882
1892
  if _uc:
1883
- # WORKTREE_UNSTAGED + WORKTREE_STAGED only; untracked excluded
1884
- _allowed_changed_files = set(_uc.staged) | set(_uc.unstaged)
1893
+ # Include untracked (new files not yet staged) so new source files
1894
+ # are analyzed under --changed-only, not silently treated as "clean".
1895
+ _allowed_changed_files = set(_uc.staged) | set(_uc.unstaged) | set(_uc.untracked)
1885
1896
  if not _allowed_changed_files:
1886
1897
  # Git is available and confirms no uncommitted changes.
1887
1898
  # Do NOT fall back to a full scan — that would silently produce
@@ -1900,10 +1911,10 @@ def main(
1900
1911
  changed_only = False
1901
1912
  if _git_confirmed_clean:
1902
1913
  _nc_payload = json.dumps({
1903
- "status": "working_tree_clean",
1904
- "no_changes": True,
1905
1914
  "changed_files": [],
1906
- "message": "No uncommitted changes detected — working tree is clean.",
1915
+ "message": "no uncommitted changes detected",
1916
+ "analysis_scope": "empty",
1917
+ "_meta": {"changed_only": True},
1907
1918
  }, ensure_ascii=False)
1908
1919
  write_output(_nc_payload, output=output)
1909
1920
  raise typer.Exit()
@@ -2626,7 +2637,14 @@ def prepare_context_cmd(
2626
2637
  if _task_include("improvement_opportunities") and output.improvement_opportunities:
2627
2638
  out["improvement_opportunities"] = output.improvement_opportunities
2628
2639
  if _task_include("test_gaps") and output.test_gaps:
2629
- out["test_gaps"] = output.test_gaps
2640
+ # Emit both the canonical name (untested_sources) and the compat alias (test_gaps)
2641
+ # so agents can use either. untested_sources is the correct semantic name.
2642
+ out["untested_sources"] = output.test_gaps
2643
+ out["test_gaps"] = output.test_gaps # backward compat alias
2644
+ if task == "generate-tests":
2645
+ _et_count = getattr(output, "existing_test_count", None)
2646
+ if _et_count is not None:
2647
+ out["existing_test_count"] = _et_count
2630
2648
  # P0-2: fast-mode truncation transparency — always emit when truncated, even if test_gaps is []
2631
2649
  # Use `is True` (strict) so MagicMock objects in tests don't trigger this branch.
2632
2650
  if getattr(output, "truncated", False) is True:
@@ -3543,6 +3561,12 @@ def fix_bug_cmd(
3543
3561
  sourcecode impact <target> — Propagate impact from a specific class
3544
3562
  sourcecode onboard . — Full architecture context first
3545
3563
  """
3564
+ if not symptom:
3565
+ typer.echo(
3566
+ "[fix-bug] Results are significantly better with --symptom. "
3567
+ "Example: --symptom 'NullPointerException in PaymentService'",
3568
+ err=True,
3569
+ )
3546
3570
  ctx.invoke(
3547
3571
  prepare_context_cmd,
3548
3572
  task="fix-bug",
@@ -4298,19 +4322,21 @@ def cache_status_cmd(
4298
4322
  def cache_clear_cmd(
4299
4323
  path: Path = typer.Argument(Path("."), help="Repository path (default: current directory)"),
4300
4324
  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."),
4325
+ include_ris: bool = typer.Option(False, "--include-ris", hidden=True, help="Alias for --all. Preserved for backward compatibility."),
4326
+ all_: bool = typer.Option(False, "--all", help="Also delete the RIS snapshot (ris.json.gz). By default, RIS is preserved across clears."),
4302
4327
  ) -> None:
4303
4328
  """Delete cached snapshots for a repository.
4304
4329
 
4305
4330
  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.
4331
+ index used for cold-start bootstrapping. Use --all to also clear it.
4307
4332
  """
4308
4333
  from sourcecode import cache as _cm
4309
4334
  target = Path(path).resolve()
4335
+ _clear_ris = include_ris or all_
4310
4336
  if not yes:
4311
- _ris_note = " (including RIS)" if include_ris else " (RIS preserved — use --include-ris to also clear it)"
4337
+ _ris_note = " (including RIS)" if _clear_ris else " (RIS preserved — use --all to also clear it)"
4312
4338
  typer.confirm(f"Delete all cache files for {target}{_ris_note}?", abort=True)
4313
- removed = _cm.clear(target, clear_ris=include_ris)
4339
+ removed = _cm.clear(target, clear_ris=_clear_ris)
4314
4340
  typer.echo(f"Removed {removed} file(s).")
4315
4341
 
4316
4342
 
@@ -391,6 +391,8 @@ class TaskOutput:
391
391
  # P0-2: fast-mode truncation transparency
392
392
  truncated: bool = False
393
393
  truncated_reason: Optional[str] = None
394
+ # generate-tests: count of existing test files found (complements untested_sources)
395
+ existing_test_count: Optional[int] = None
394
396
 
395
397
 
396
398
  @dataclass
@@ -2237,6 +2239,8 @@ class TaskContextBuilder:
2237
2239
  # P0-2: fast-mode truncation transparency
2238
2240
  truncated=_fast_truncated,
2239
2241
  truncated_reason=_fast_truncated_reason,
2242
+ # generate-tests: count of test files found alongside untested_sources
2243
+ existing_test_count=len(test_set) if task_name == "generate-tests" else None,
2240
2244
  )
2241
2245
 
2242
2246
  def render_prompt(self, output: TaskOutput) -> str:
@@ -201,6 +201,20 @@ _FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
201
201
  "@EnableGlobalMethodSecurity",
202
202
  })
203
203
 
204
+ # Programmatic security: method-call patterns that indicate runtime auth enforcement.
205
+ _PROGRAMMATIC_SECURITY_RE = re.compile(
206
+ r"\b(?:hasRole|hasAuthority|isAuthenticated|requirePermission|checkPermission"
207
+ r"|assertAuthorized|authenticate)\s*\("
208
+ r"|(?:Authentication|SecurityContext|Principal|AuthorizationManager|AccessDecisionManager)\b"
209
+ r"|throw\s+new\s+(?:AccessDeniedException|UnauthorizedException|ForbiddenException|AuthenticationException)\b",
210
+ re.MULTILINE,
211
+ )
212
+
213
+
214
+ def _has_programmatic_security(source: str) -> bool:
215
+ return bool(_PROGRAMMATIC_SECURITY_RE.search(source))
216
+
217
+
204
218
  _MODIFIER_WORDS: frozenset[str] = frozenset({
205
219
  "public", "private", "protected", "static", "final", "abstract",
206
220
  "synchronized", "native", "strictfp", "transient", "volatile", "default",
@@ -2365,6 +2379,7 @@ def _build_route_surface(
2365
2379
 
2366
2380
  routes: list[dict] = []
2367
2381
  seen: set[tuple] = set()
2382
+ _prog_sec_cache: dict[str, Optional[bool]] = {} # declaring_file → has_programmatic
2368
2383
 
2369
2384
  # Phase 2: emit own endpoint symbols and record them per class.
2370
2385
  # Each method emits one route per resolved effective prefix.
@@ -2414,6 +2429,19 @@ def _build_route_surface(
2414
2429
  _cls_sym_for_sec = class_sym_by_simple.get(cls_simple)
2415
2430
  _sec = _route_security_from_sym(sym, _cls_sym_for_sec)
2416
2431
 
2432
+ # Programmatic security fallback: scan controller file when no annotation found.
2433
+ if _sec is None:
2434
+ _decl_file = sym.declaring_file or ""
2435
+ if _decl_file and _decl_file not in _prog_sec_cache:
2436
+ try:
2437
+ _prog_sec_cache[_decl_file] = _has_programmatic_security(
2438
+ Path(_decl_file).read_text(encoding="utf-8", errors="ignore")
2439
+ )
2440
+ except Exception:
2441
+ _prog_sec_cache[_decl_file] = False
2442
+ if _prog_sec_cache.get(_decl_file):
2443
+ _sec = {"policy": "programmatic"}
2444
+
2417
2445
  for prefix in effective_prefixes:
2418
2446
  # P1 fix: re.sub collapses any number of consecutive slashes (///, //, etc.)
2419
2447
  # Single .replace("//", "/") fails for triple-slash from prefix="/" + suffix="/{id}".
@@ -2434,8 +2462,7 @@ def _build_route_surface(
2434
2462
  "stable_id": sym.stable_id,
2435
2463
  "inheritance_depth": 0,
2436
2464
  }
2437
- if _sec is not None:
2438
- _route_entry["security_annotations"] = _sec
2465
+ _route_entry["security_annotations"] = _sec
2439
2466
  routes.append(_route_entry)
2440
2467
 
2441
2468
  # Phase 3: inheritance projection — subclasses with zero own endpoints
@@ -2966,10 +2993,10 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
2966
2993
  # Use security_annotations already extracted by _build_route_surface
2967
2994
  # via the canonical _route_security_from_sym extractor.
2968
2995
  security_info = route.get("security_annotations")
2996
+ entry["security"] = security_info # always present; None = no security signal
2969
2997
  if security_info:
2970
- entry["security"] = security_info
2971
2998
  # Backward compat: keep required_permission for custom annotation
2972
- if security_info.get("policy") == "custom_permission":
2999
+ if isinstance(security_info, dict) and security_info.get("policy") == "custom_permission":
2973
3000
  entry["required_permission"] = security_info["required_permission"]
2974
3001
  endpoints.append(entry)
2975
3002
 
@@ -2988,7 +3015,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
2988
3015
  # "no_security_signal" = no recognized security annotation at method OR class level.
2989
3016
  # Note: repos may use framework-level security (e.g. Keycloak itself) with no
2990
3017
  # per-endpoint annotations — this count reflects annotation-based coverage only.
2991
- no_security_signal = sum(1 for e in endpoints if "security" not in e)
3018
+ no_security_signal = sum(1 for e in endpoints if not e.get("security"))
2992
3019
 
2993
3020
  # Detect filter-based security: centralized Spring Security config class.
2994
3021
  # When present, high no_security_signal is expected — security is enforced by
@@ -3007,7 +3034,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3007
3034
  for sym in _class_syms
3008
3035
  )
3009
3036
  )
3010
- _has_annotation_security = any("security" in e for e in endpoints)
3037
+ _has_annotation_security = any(e.get("security") for e in endpoints)
3011
3038
  if _filter_based and _has_annotation_security:
3012
3039
  security_model = "mixed"
3013
3040
  elif _filter_based:
@@ -3090,8 +3117,34 @@ def compute_blast_radius(
3090
3117
  subsystems: list[dict] = ir.get("subsystems") or []
3091
3118
 
3092
3119
  # ── 1. Resolve target → one or more FQNs ─────────────────────────────────
3120
+ _path_like = "/" in target or "\\" in target or target.endswith(".java")
3093
3121
  resolution, matched_fqns = _resolve_target(target, reverse_graph, graph_nodes)
3094
3122
 
3123
+ # File-path input with ambiguous resolution: require the user to be specific.
3124
+ if _path_like and len(matched_fqns) > 1:
3125
+ _candidates = sorted(matched_fqns)
3126
+ return {
3127
+ "target": target,
3128
+ "resolution": "ambiguous_path",
3129
+ "message": (
3130
+ f"Path '{target}' matches {len(matched_fqns)} classes in the IR. "
3131
+ "Pass the full FQN to select one."
3132
+ ),
3133
+ "candidates": _candidates,
3134
+ "direct_callers": [],
3135
+ "indirect_callers": [],
3136
+ "endpoints_affected": [],
3137
+ "mappers_affected": [],
3138
+ "security_surface_affected": [],
3139
+ "cross_module_impact": [],
3140
+ "transactional_boundaries_touched": [],
3141
+ "risk_score": 0.0,
3142
+ "risk_level": "unknown",
3143
+ "confidence_score": 0.0,
3144
+ "confidence_level": "low",
3145
+ "explanation": f"Ambiguous path — {len(matched_fqns)} candidates found.",
3146
+ }
3147
+
3095
3148
  if not matched_fqns:
3096
3149
  # Build a short candidate list to help the user
3097
3150
  _candidates = _blast_radius_candidates(target, reverse_graph, graph_nodes)
@@ -393,12 +393,40 @@ def get_cold_start_context(repo_root: Path) -> dict:
393
393
  # An empty list does NOT mean "no endpoints exist" — it means the endpoint
394
394
  # index has not been built yet. Agents must call get_endpoints to populate.
395
395
  _api_complete = not _is_java or bool(endpoints)
396
+
397
+ # Build structural validation for Java/Spring repos.
398
+ # Detects when the RIS snapshot is structurally incomplete (controllers found
399
+ # but endpoint index was never built), so agents can decide whether to rebuild.
400
+ _controllers_in_map = ris.structural_map.get("controllers", [])
401
+ _controllers_in_api = ris.api_surface.get("controllers_index", [])
402
+ _controllers_found = len(_controllers_in_map) or len(_controllers_in_api)
403
+ _endpoints_found = len(endpoints)
404
+ # Spring is detected when controllers exist in structural map or api surface.
405
+ _spring_detected = bool(_controllers_found) or bool(_controllers_in_api)
406
+ _validation_status = (
407
+ "incomplete_snapshot"
408
+ if _is_java and _spring_detected and _endpoints_found == 0
409
+ else "valid"
410
+ )
411
+ _validation: dict = {
412
+ "spring_detected": _spring_detected,
413
+ "controllers_found": _controllers_found,
414
+ "endpoints_found": _endpoints_found,
415
+ "status": _validation_status,
416
+ }
417
+
418
+ # When the snapshot is structurally incomplete, downgrade status so agents
419
+ # don't assume cold_start_ready when critical sections are missing.
420
+ _status_base = "cold_start_stale" if stale else "cold_start_ready"
421
+ if _validation_status == "incomplete_snapshot" and not stale:
422
+ _status_base = "cold_start_incomplete"
423
+
396
424
  result: dict = {
397
- "status": "cold_start_stale" if stale else "cold_start_ready",
425
+ "status": _status_base,
398
426
  "repo_id": ris.repo_id,
399
427
  "git_head": ris.git_head,
400
428
  "current_git_head": current_head,
401
- "stale": stale,
429
+ "stale": stale or (_validation_status == "incomplete_snapshot"),
402
430
  "has_uncommitted_changes": uncommitted,
403
431
  "last_updated_at": ris.last_updated_at,
404
432
  "cache_source": "RIS",
@@ -408,6 +436,7 @@ def get_cold_start_context(repo_root: Path) -> dict:
408
436
  "entrypoints": ris.structural_map.get("entrypoints", []),
409
437
  "endpoints": endpoints,
410
438
  "hotspots": ris.git_context_snapshot.get("hotspots", []),
439
+ "validation": _validation,
411
440
  }
412
441
  if not endpoints and _is_java:
413
442
  result["endpoints_hint"] = (
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes