sourcecode 1.33.4__py3-none-any.whl → 1.33.5__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.4"
3
+ __version__ = "1.33.5"
sourcecode/cli.py CHANGED
@@ -1225,13 +1225,24 @@ def main(
1225
1225
  _uncommitted = False
1226
1226
  _hit_source = "L2_view" if (_view_key and _core_hash) else "L1_core"
1227
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
1228
1239
  _cache_hit_content = _inject_cache_meta(_cache_hit_content, {
1229
1240
  "cache_source": _hit_source,
1230
1241
  "git_head_at_generation": _git_sha,
1231
1242
  "current_git_head": _git_sha,
1232
1243
  "is_stale": False,
1233
1244
  "has_uncommitted_changes": _uncommitted,
1234
- "generated_at": None,
1245
+ "generated_at": _cached_generated_at,
1235
1246
  "data_scope": _data_scope,
1236
1247
  })
1237
1248
  write_output(_cache_hit_content, output=output)
@@ -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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.33.4
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
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=oG8RdsJRmrxoLhAnBA3rq7bWu7SPvQTfWedYeipB42k,103
1
+ sourcecode/__init__.py,sha256=1rtjkdHP6M265Dvjzw_Pe1-jjy13_C_oBKGBnxQCbUk,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
@@ -7,7 +7,7 @@ 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=K2FT1wLNZuG_-9nKSOGdYbk-5GQkn4u7C2NR6IxdePY,180950
10
+ sourcecode/cli.py,sha256=EMs-sz83Fpl4evsU56xuU_S6ZL0TMzZp9nW9V1bra-I,181396
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
@@ -35,7 +35,7 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
35
35
  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
- sourcecode/repository_ir.py,sha256=-NjBQUT7zyya4ng8Hq0-ChoiHZkUif9lr-Q878gmj8M,153163
38
+ sourcecode/repository_ir.py,sha256=WtUPRVrvJju6Lrvm_7iTDk57qJFCAh-LJalLtX8jzIk,155595
39
39
  sourcecode/ris.py,sha256=vkIe_v-jjceeb0Adhn-Kw5eFXE_nIkGHflOnQYPycTk,17411
40
40
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
41
41
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -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.4.dist-info/METADATA,sha256=Q9XlsPNiuahPxhrDZUU8OqtH1U_DH77rv22bnvblazA,16440
84
- sourcecode-1.33.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
85
- sourcecode-1.33.4.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
86
- sourcecode-1.33.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
87
- sourcecode-1.33.4.dist-info/RECORD,,
83
+ sourcecode-1.33.5.dist-info/METADATA,sha256=0CKSD6Q2_A9noOoPyeC4I0A-JuYA8bo3aIAu-kxtm1A,16440
84
+ sourcecode-1.33.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
85
+ sourcecode-1.33.5.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
86
+ sourcecode-1.33.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
87
+ sourcecode-1.33.5.dist-info/RECORD,,