sourcecode 2.4.0__py3-none-any.whl → 2.5.1__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.
@@ -0,0 +1,351 @@
1
+ """TRANSACTIONS & SECURITY pack step handlers (Fase 6).
2
+
3
+ Six intents (TRANSACTION_FLOW, TRANSACTION_ENTRYPOINTS, TRANSACTION_PROPAGATION,
4
+ SECURITY_SURFACE, SECURITY_CHAIN, VALIDATION_POINTS) expressed purely as compositions of
5
+ these projection steps. NO new analyzer: every step reuses an existing engine and projects
6
+ its already-computed output.
7
+
8
+ * Transactions → `SpringSemanticModel.tx_index` (structural boundaries) + `run_tx_audit`.
9
+ * Security → `run_security_audit` (+ the endpoint model for scoping).
10
+ * Validation → `build_validation_surface`.
11
+
12
+ No step opens source or parses: the ContextGraph / SpringSemanticModel are acquired once by
13
+ the shared `KnowledgeContext`; steps only read them. Ordering is deterministic; ranking
14
+ reuses the engines' own severity/confidence (`spring_findings.SEVERITY_ORDER`).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import TYPE_CHECKING
19
+
20
+ from sourcecode.spring_findings import SEVERITY_ORDER
21
+ from sourcecode.retrieval.query import (
22
+ CollectSecurityEvidence,
23
+ RankByCriticality,
24
+ ResolvePropagation,
25
+ ResolveSecurityChain,
26
+ ResolveSecuritySurface,
27
+ ResolveTransactionEntrypoints,
28
+ ResolveTransactionFlow,
29
+ ResolveValidationPoints,
30
+ Step,
31
+ )
32
+ from sourcecode.retrieval.result import (
33
+ Explanation,
34
+ KnowledgeEntity,
35
+ Observation,
36
+ Provenance,
37
+ RetrievalEvidence,
38
+ )
39
+ from sourcecode.retrieval.steps import (
40
+ SLOT_CONFIDENCE,
41
+ SLOT_ENTITIES,
42
+ SLOT_EVIDENCE,
43
+ SLOT_EXPLANATION,
44
+ SLOT_OBSERVATIONS,
45
+ SLOT_PROVENANCE,
46
+ )
47
+ from sourcecode.retrieval.steps_impact import SLOT_TARGET
48
+
49
+ if TYPE_CHECKING:
50
+ from sourcecode.retrieval.runtime import StepContext, StepRegistry
51
+
52
+ SLOT_SERVICES = "txsec_services"
53
+ _PROPAGATION_PATTERNS = frozenset({"TX-002", "TX-003", "TX-004"})
54
+ _CONF_RANK = {"high": 0, "medium": 1, "low": 2}
55
+
56
+
57
+ # ── transactions (structural: SpringSemanticModel.tx_index) ──────────────────────
58
+ def resolve_transaction_flow(step: Step, ctx: "StepContext") -> None:
59
+ assert isinstance(step, ResolveTransactionFlow)
60
+ tx = ctx.knowledge.spring_model.tx_index
61
+ boundaries = list(tx.all_declared)
62
+ ctx.put(SLOT_ENTITIES, [_boundary_entity(b, "transaction_boundary") for b in boundaries])
63
+ ctx.put(SLOT_EVIDENCE, [_boundary_evidence(b) for b in boundaries])
64
+ ctx.put(SLOT_OBSERVATIONS, _stats_observations(tx))
65
+ ctx.put(SLOT_CONFIDENCE, _agg_conf([b.confidence for b in boundaries]))
66
+ _add_service(ctx, "SpringSemanticModel")
67
+
68
+
69
+ def resolve_transaction_entrypoints(step: Step, ctx: "StepContext") -> None:
70
+ """Transactional boundaries whose enclosing class is a controller — where transactions
71
+ begin at the API surface. Reuses tx_index × the endpoint model's controller set."""
72
+ assert isinstance(step, ResolveTransactionEntrypoints)
73
+ model = ctx.knowledge.spring_model
74
+ controllers = set(model.endpoint_index.controller_fqns)
75
+ boundaries = [b for b in model.tx_index.all_declared if _enclosing_class(b.symbol) in controllers]
76
+ ctx.put(SLOT_ENTITIES, [_boundary_entity(b, "transaction_entrypoint") for b in boundaries])
77
+ ctx.put(SLOT_EVIDENCE, [_boundary_evidence(b) for b in boundaries])
78
+ ctx.put(SLOT_CONFIDENCE, _agg_conf([b.confidence for b in boundaries]))
79
+ _add_service(ctx, "SpringSemanticModel")
80
+
81
+
82
+ def resolve_propagation(step: Step, ctx: "StepContext") -> None:
83
+ """Boundaries with non-default propagation + the propagation-risk tx audit findings.
84
+ Reuses tx_index + `run_tx_audit`."""
85
+ assert isinstance(step, ResolvePropagation)
86
+ from sourcecode.spring_tx_analyzer import run_tx_audit
87
+
88
+ model = ctx.knowledge.spring_model
89
+ non_default = [b for b in model.tx_index.all_declared if b.propagation != "REQUIRED"]
90
+ findings = [
91
+ f
92
+ for f in run_tx_audit(
93
+ ctx.knowledge.context_graph.cir, root=ctx.knowledge.root, model=model
94
+ ).findings
95
+ if f.pattern_id in _PROPAGATION_PATTERNS
96
+ ]
97
+ ctx.put(SLOT_ENTITIES, [_boundary_entity(b, "propagation_point") for b in non_default])
98
+ ctx.put(SLOT_EVIDENCE, [_boundary_evidence(b) for b in non_default] + [_finding_evidence(f) for f in findings])
99
+ ctx.put(SLOT_OBSERVATIONS, _stats_observations(model.tx_index))
100
+ ctx.put(SLOT_CONFIDENCE, _agg_conf([b.confidence for b in non_default] + [f.confidence for f in findings]))
101
+ _add_service(ctx, "SpringSemanticModel")
102
+ _add_service(ctx, "run_tx_audit")
103
+
104
+
105
+ # ── security (reuse run_security_audit) ──────────────────────────────────────────
106
+ def resolve_security_surface(step: Step, ctx: "StepContext") -> None:
107
+ assert isinstance(step, ResolveSecuritySurface)
108
+ findings = _security_findings(ctx)
109
+ ctx.put(SLOT_ENTITIES, [_finding_entity(f) for f in findings])
110
+ ctx.put(SLOT_EVIDENCE, [_finding_evidence(f) for f in findings])
111
+ ctx.put(SLOT_OBSERVATIONS, _severity_observations(findings))
112
+ ctx.put(SLOT_CONFIDENCE, _agg_conf([f.confidence for f in findings]))
113
+ _add_service(ctx, "run_security_audit")
114
+
115
+
116
+ def resolve_security_chain(step: Step, ctx: "StepContext") -> None:
117
+ """Security findings scoped to a target (symbol / related / enclosing class). Reuses
118
+ `run_security_audit`; the target comes from the shared ResolveTarget step."""
119
+ assert isinstance(step, ResolveSecurityChain)
120
+ target = ctx.require(SLOT_TARGET)
121
+ scoped = [f for f in _security_findings(ctx) if _touches(f, target)]
122
+ ctx.put(SLOT_ENTITIES, [_finding_entity(f) for f in scoped])
123
+ ctx.put(SLOT_EVIDENCE, [_finding_evidence(f) for f in scoped])
124
+ ctx.put(SLOT_OBSERVATIONS, _severity_observations(scoped))
125
+ ctx.put(SLOT_CONFIDENCE, _agg_conf([f.confidence for f in scoped]))
126
+ _add_service(ctx, "run_security_audit")
127
+
128
+
129
+ # ── validation (reuse build_validation_surface) ──────────────────────────────────
130
+ def resolve_validation_points(step: Step, ctx: "StepContext") -> None:
131
+ assert isinstance(step, ResolveValidationPoints)
132
+ from sourcecode.validation_surface import build_validation_surface
133
+
134
+ surface = build_validation_surface(ctx.knowledge.root, graph=ctx.knowledge.context_graph)
135
+ endpoints = surface.get("endpoints", []) or []
136
+ gaps = surface.get("gaps", []) or []
137
+ entities = [
138
+ KnowledgeEntity(
139
+ identity=_route_id(ep),
140
+ kind="validation_point",
141
+ score=0.0,
142
+ attributes={k: v for k, v in ep.items() if k not in ("source_file", "line")},
143
+ )
144
+ for ep in endpoints
145
+ ] + [
146
+ KnowledgeEntity(identity=_route_id(g), kind="validation_gap", score=0.0, attributes=dict(g))
147
+ for g in gaps
148
+ ]
149
+ ctx.put(SLOT_ENTITIES, entities)
150
+ ctx.put(
151
+ SLOT_EVIDENCE,
152
+ [
153
+ RetrievalEvidence(
154
+ signal="validation",
155
+ detail=_route_id(ep),
156
+ weight=1.0,
157
+ strength=1.0,
158
+ mass=1.0,
159
+ contribution=1.0,
160
+ provider="build_validation_surface",
161
+ source_file=ep.get("source_file"),
162
+ source_line=ep.get("line"),
163
+ )
164
+ for ep in endpoints
165
+ ],
166
+ )
167
+ summary = surface.get("summary", {}) or {}
168
+ ctx.put(
169
+ SLOT_OBSERVATIONS,
170
+ [Observation(name=str(k), value=v, detail="validation surface summary") for k, v in summary.items()],
171
+ )
172
+ ctx.put(SLOT_CONFIDENCE, "high" if endpoints or gaps else "medium")
173
+ _add_service(ctx, "build_validation_surface")
174
+
175
+
176
+ # ── ranking + terminal evidence (generic across the pack) ────────────────────────
177
+ def rank_by_criticality(step: Step, ctx: "StepContext") -> None:
178
+ """Order entities by the engines' OWN severity then confidence then identity. No new
179
+ score is computed — SEVERITY_ORDER is the audit engines' own severity ranking."""
180
+ assert isinstance(step, RankByCriticality)
181
+ entities = ctx.get(SLOT_ENTITIES, [])
182
+
183
+ def key(e: KnowledgeEntity):
184
+ sev = SEVERITY_ORDER.get(e.attributes.get("severity"), 9)
185
+ conf = _CONF_RANK.get(e.attributes.get("confidence"), 9)
186
+ return (sev, conf, e.identity)
187
+
188
+ ctx.put(SLOT_ENTITIES, sorted(entities, key=key))
189
+
190
+
191
+ def collect_security_evidence(step: Step, ctx: "StepContext") -> None:
192
+ """Assemble confidence, structured explanation (no synthesized prose), and provenance
193
+ from what the resolve steps produced. GENERIC across every tx/security intent."""
194
+ confidence = ctx.get(SLOT_CONFIDENCE, "medium")
195
+ ctx.put(
196
+ SLOT_EXPLANATION,
197
+ Explanation(
198
+ winner=None,
199
+ score=0.0,
200
+ confidence=confidence,
201
+ top_contributions=(),
202
+ runner_up=None,
203
+ engine_explanation="",
204
+ ),
205
+ )
206
+ ctx.put(
207
+ SLOT_PROVENANCE,
208
+ Provenance(
209
+ services=tuple(ctx.get(SLOT_SERVICES, [])),
210
+ signals_used=("tx_index", "endpoint_index", "security_audit", "validation_surface"),
211
+ signals_missing=(),
212
+ knowledge_source="SpringSemanticModel + audit/validation engines",
213
+ ),
214
+ )
215
+
216
+
217
+ # ── helpers ──────────────────────────────────────────────────────────────────────
218
+ def _security_findings(ctx: "StepContext") -> list:
219
+ from sourcecode.spring_security_audit import run_security_audit
220
+
221
+ return run_security_audit(
222
+ ctx.knowledge.context_graph.cir, root=ctx.knowledge.root, model=ctx.knowledge.spring_model
223
+ ).findings
224
+
225
+
226
+ def _boundary_entity(b, kind: str) -> KnowledgeEntity:
227
+ return KnowledgeEntity(
228
+ identity=b.symbol,
229
+ kind=kind,
230
+ score=0.0,
231
+ attributes={
232
+ "scope": b.scope,
233
+ "propagation": b.propagation,
234
+ "isolation": b.isolation,
235
+ "read_only": b.read_only,
236
+ "proxy_bypass_risk": b.is_proxy_bypass_risk,
237
+ "confidence": b.confidence,
238
+ },
239
+ )
240
+
241
+
242
+ def _boundary_evidence(b) -> RetrievalEvidence:
243
+ return RetrievalEvidence(
244
+ signal="transactional",
245
+ detail=f"@Transactional({b.propagation}, readOnly={b.read_only})",
246
+ weight=1.0,
247
+ strength=1.0,
248
+ mass=1.0,
249
+ contribution=1.0,
250
+ provider="SpringSemanticModel",
251
+ source_file=b.source_file or None,
252
+ )
253
+
254
+
255
+ def _finding_entity(f) -> KnowledgeEntity:
256
+ return KnowledgeEntity(
257
+ identity=f.symbol,
258
+ kind="security_finding",
259
+ score=0.0,
260
+ attributes={
261
+ "pattern_id": f.pattern_id,
262
+ "severity": f.severity,
263
+ "confidence": f.confidence,
264
+ "title": f.title,
265
+ },
266
+ )
267
+
268
+
269
+ def _finding_evidence(f) -> RetrievalEvidence:
270
+ file = line = None
271
+ sf = getattr(f, "source_file", "") or ""
272
+ if sf:
273
+ file = sf
274
+ return RetrievalEvidence(
275
+ signal=f.pattern_id,
276
+ detail=f.title,
277
+ weight=1.0,
278
+ strength=1.0,
279
+ mass=1.0,
280
+ contribution=1.0,
281
+ provider=f"audit:{f.category}",
282
+ source_file=file,
283
+ source_line=line,
284
+ )
285
+
286
+
287
+ def _stats_observations(tx) -> list:
288
+ stats = tx.stats() if hasattr(tx, "stats") else {}
289
+ out = []
290
+ for k, v in stats.items():
291
+ if isinstance(v, dict):
292
+ out.append(Observation(name=k, value=sum(v.values()), detail=f"{k}: {v}"))
293
+ else:
294
+ out.append(Observation(name=str(k), value=v, detail="tx_index stats"))
295
+ return out
296
+
297
+
298
+ def _severity_observations(findings) -> list:
299
+ hist: dict[str, int] = {}
300
+ for f in findings:
301
+ hist[f.severity] = hist.get(f.severity, 0) + 1
302
+ return [Observation(name=f"severity_{s}", value=n, detail="security finding count") for s, n in sorted(hist.items())]
303
+
304
+
305
+ def _route_id(d: dict) -> str:
306
+ return (
307
+ d.get("route")
308
+ or d.get("path")
309
+ or f"{d.get('method', '')} {d.get('handler', '')}".strip()
310
+ or d.get("handler", "")
311
+ or "?"
312
+ )
313
+
314
+
315
+ def _touches(f, target: str) -> bool:
316
+ return (
317
+ f.symbol == target
318
+ or target in (getattr(f, "related_symbols", []) or [])
319
+ or _enclosing_class(f.symbol) == target
320
+ or f.symbol.startswith(target)
321
+ )
322
+
323
+
324
+ def _enclosing_class(symbol: str) -> str:
325
+ return symbol.split("#", 1)[0]
326
+
327
+
328
+ def _agg_conf(confs) -> str:
329
+ ranked = [c for c in confs if c in _CONF_RANK]
330
+ if not ranked:
331
+ return "high"
332
+ return max(ranked, key=lambda c: _CONF_RANK[c]) # most conservative (lowest)
333
+
334
+
335
+ def _add_service(ctx: "StepContext", name: str) -> None:
336
+ services = list(ctx.get(SLOT_SERVICES, []))
337
+ if name not in services:
338
+ services.append(name)
339
+ ctx.put(SLOT_SERVICES, services)
340
+
341
+
342
+ def register_txsec_steps(registry: "StepRegistry") -> None:
343
+ """Register the TRANSACTIONS & SECURITY handlers (the sanctioned extension seam)."""
344
+ registry.register(ResolveTransactionFlow.kind, resolve_transaction_flow)
345
+ registry.register(ResolveTransactionEntrypoints.kind, resolve_transaction_entrypoints)
346
+ registry.register(ResolvePropagation.kind, resolve_propagation)
347
+ registry.register(ResolveSecuritySurface.kind, resolve_security_surface)
348
+ registry.register(ResolveSecurityChain.kind, resolve_security_chain)
349
+ registry.register(ResolveValidationPoints.kind, resolve_validation_points)
350
+ registry.register(RankByCriticality.kind, rank_by_criticality)
351
+ registry.register(CollectSecurityEvidence.kind, collect_security_evidence)
@@ -1,7 +1,7 @@
1
1
  """Custom security annotation configuration (BUG-3).
2
2
 
3
3
  Enterprise Spring projects routinely guard endpoints with bespoke authorization
4
- annotations (e.g. ``@M3FiltroSeguridad(nombreRecurso=..., nivelRequerido=...)``)
4
+ annotations (e.g. ``@CustomSecurityAnnotation(nombreRecurso=..., nivelRequerido=...)``)
5
5
  instead of the standard ``@PreAuthorize`` / ``@Secured`` set. Without knowing
6
6
  those names, the endpoint surface reports ``policy: none_detected`` for every
7
7
  protected route, which makes ``endpoints`` / ``spring-audit`` blind in exactly
@@ -18,8 +18,8 @@ Config shape::
18
18
  {
19
19
  "customSecurityAnnotations": [
20
20
  {
21
- "fullyQualifiedName": "com.example.security.M3FiltroSeguridad",
22
- "shortName": "M3FiltroSeguridad",
21
+ "fullyQualifiedName": "com.example.security.CustomSecurityAnnotation",
22
+ "shortName": "CustomSecurityAnnotation",
23
23
  "resourceParam": "nombreRecurso",
24
24
  "levelParam": "nivelRequerido",
25
25
  "riskLevel": "custom"
@@ -41,7 +41,7 @@ CONFIG_FILENAME = "sourcecode.config.json"
41
41
  class CustomSecuritySpec:
42
42
  """One custom security annotation the analyzer should recognize."""
43
43
 
44
- short_name: str # e.g. "M3FiltroSeguridad" (no leading @)
44
+ short_name: str # e.g. "CustomSecurityAnnotation" (no leading @)
45
45
  fqn: str = "" # fully-qualified name (optional)
46
46
  resource_param: str = "" # annotation attribute naming the protected resource
47
47
  level_param: str = "" # annotation attribute naming the required level
@@ -443,6 +443,7 @@ def _bfs_callers(
443
443
  reverse_graph: dict[str, dict[str, list[str]]],
444
444
  max_depth: int,
445
445
  impl_graph: object | None = None,
446
+ method_scoped: bool = False,
446
447
  ) -> tuple[list[str], list[str], bool]:
447
448
  """BFS from seed FQNs through reverse_graph.
448
449
 
@@ -481,8 +482,22 @@ def _bfs_callers(
481
482
  # into direct_callers and inflating the blast radius ~12×. Exclude any caller
482
483
  # whose owning class is one of the seed classes.
483
484
  seed_classes: set[str] = {normalize_owner_fqn(s) for s in seed_fqns}
485
+ seed_set: set[str] = set(seed_fqns)
484
486
  self_excluded: int = 0
485
487
 
488
+ # P0-IMPACT-01 fix: distinguish self-recursion from same-class sibling calls.
489
+ # For a METHOD-scoped query ("Foo#doWork"), a *different* method on the same class
490
+ # (e.g. Foo#callWork → Foo#doWork) is a legitimate caller — the only true
491
+ # "self-reference" is the seed method invoking itself (recursion). The v1.70.0
492
+ # whole-class exclusion is correct ONLY for CLASS-scoped queries, where a class's
493
+ # own members are members, not external callers. Applying it to method queries
494
+ # silently discarded the intra-class caller edges built by _intra_class_call_edges,
495
+ # producing a false "0 callers / isolated" for private helpers.
496
+ def _is_self_reference(caller: str) -> bool:
497
+ if method_scoped:
498
+ return caller in seed_set # only genuine recursion of a seed method
499
+ return normalize_owner_fqn(caller) in seed_classes
500
+
486
501
  # BUG-004: index class FQN → list of method-level keys in reverse_graph.
487
502
  # Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
488
503
  # under reverse_graph["Foo"]. Without this index, BFS silently terminates
@@ -519,7 +534,7 @@ def _bfs_callers(
519
534
  for etype, fqn_list in _edges_for(seed):
520
535
  if etype not in _SKIP_EDGE_TYPES:
521
536
  unique_direct_callers.update(
522
- c for c in fqn_list if normalize_owner_fqn(c) not in seed_classes
537
+ c for c in fqn_list if not _is_self_reference(c)
523
538
  )
524
539
 
525
540
  effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
@@ -534,7 +549,8 @@ def _bfs_callers(
534
549
  if caller in visited:
535
550
  return
536
551
  # BUG #2: a member of a seed class is not an external caller — drop it.
537
- if normalize_owner_fqn(caller) in seed_classes:
552
+ # (Method-scoped queries drop only true self-recursion; see _is_self_reference.)
553
+ if _is_self_reference(caller):
538
554
  visited.add(caller)
539
555
  self_excluded += 1
540
556
  return
@@ -855,6 +871,22 @@ class ImpactOrchestrator:
855
871
  original_seed_classes: set[str] = {_class_of(s) for s in seed_fqns}
856
872
  # Subtype classes surfaced to the caller as the impacted implementation set.
857
873
  subtype_classes_added: set[str] = set()
874
+
875
+ # Method-scoped query (e.g. "Impl#foo"): impl/interface expansion must preserve
876
+ # the queried method's identity. A change to `foo` does not affect callers of a
877
+ # sibling method `bar`, so the expanded SEED symbols are restricted to the same
878
+ # method name(s). Class-level queries (a class node with no '#' in the seed)
879
+ # expand to all members, as before. This bound became observable once precise
880
+ # `field.method(...)` call edges exist: without it a method query inherited the
881
+ # callers of every sibling method on the interface (blast-radius inflation).
882
+ _queried_methods: set[str] = {s.rsplit("#", 1)[1] for s in seed_fqns if "#" in s}
883
+ _method_scoped = bool(_queried_methods) and all("#" in s for s in seed_fqns)
884
+
885
+ def _member_in_scope(sym: str) -> bool:
886
+ if not _method_scoped:
887
+ return True
888
+ return "#" in sym and sym.rsplit("#", 1)[1] in _queried_methods
889
+
858
890
  if impl_graph is not None:
859
891
  seed_classes_ch001 = {_class_of(s) for s in seed_fqns}
860
892
  impl_seeds: list[str] = []
@@ -867,7 +899,8 @@ class ImpactOrchestrator:
867
899
  continue
868
900
  subtype_classes_added.add(impl_class)
869
901
  for sym in cir.symbols:
870
- if _class_of(sym) == impl_class and sym not in set(seed_fqns):
902
+ if (_class_of(sym) == impl_class and sym not in set(seed_fqns)
903
+ and _member_in_scope(sym)):
871
904
  impl_seeds.append(sym)
872
905
  if impl_seeds:
873
906
  seed_fqns = list(dict.fromkeys(seed_fqns + impl_seeds))
@@ -896,7 +929,8 @@ class ImpactOrchestrator:
896
929
  continue
897
930
  iface_classes_added.add(iface_class)
898
931
  for sym in cir.symbols:
899
- if _class_of(sym) == iface_class and sym not in set(seed_fqns):
932
+ if (_class_of(sym) == iface_class and sym not in set(seed_fqns)
933
+ and _member_in_scope(sym)):
900
934
  iface_seeds.append(sym)
901
935
  if iface_seeds:
902
936
  seed_fqns = list(dict.fromkeys(seed_fqns + iface_seeds))
@@ -909,14 +943,22 @@ class ImpactOrchestrator:
909
943
 
910
944
  # ── 2. BFS through reverse graph ─────────────────────────────────
911
945
  direct_callers, indirect_callers, truncated, self_excluded = _bfs_callers(
912
- seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph
946
+ seed_fqns, cir.reverse_graph, depth, impl_graph=impl_graph,
947
+ method_scoped=_method_scoped,
913
948
  )
914
949
  if self_excluded:
915
- warnings.append(
916
- f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
917
- f"analyzed class were dropped from callers — a class's own methods are "
918
- f"members, not external callers (they do not count toward blast radius)."
919
- )
950
+ if _method_scoped:
951
+ warnings.append(
952
+ f"Self-recursion excluded: {self_excluded} recursive self-call(s) of the "
953
+ f"queried method were dropped from callers (a method is not its own "
954
+ f"caller). Same-class sibling methods ARE counted as callers."
955
+ )
956
+ else:
957
+ warnings.append(
958
+ f"Self-referential exclusion (BUG #2): {self_excluded} member(s) of the "
959
+ f"analyzed class were dropped from callers — a class's own methods are "
960
+ f"members, not external callers (they do not count toward blast radius)."
961
+ )
920
962
  if truncated:
921
963
  warnings.append(
922
964
  "Hub-class guard active: symbol has > 500 direct callers — "
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.4.0
3
+ Version: 2.5.1
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
@@ -42,7 +42,7 @@ Description-Content-Type: text/markdown
42
42
 
43
43
  **Context · Impact · Migration · Architecture · Review — everything from one structural model.**
44
44
 
45
- ![Version](https://img.shields.io/badge/version-2.3.0-blue)
45
+ ![Version](https://img.shields.io/badge/version-2.5.1-blue)
46
46
  ![Python](https://img.shields.io/badge/python-3.9%2B-green)
47
47
 
48
48
  > **ASK Engine** is the product. The CLI command is **`ask`**. The legacy **`sourcecode`**
@@ -84,7 +84,7 @@ brew tap haroundominique/sourcecode && brew install sourcecode
84
84
  # pip / pipx
85
85
  pipx install sourcecode # or: pip install sourcecode
86
86
 
87
- ask version # ask 2.3.0
87
+ ask version # ask 2.5.1
88
88
  ```
89
89
 
90
90
  > **Package vs. command.** The install package is named `sourcecode` this release
@@ -218,7 +218,7 @@ repo root (otherwise they report `policy: "none_detected"`):
218
218
  ```json
219
219
  {
220
220
  "customSecurityAnnotations": [
221
- { "fullyQualifiedName": "com.example.security.M3FiltroSeguridad", "shortName": "M3FiltroSeguridad" }
221
+ { "fullyQualifiedName": "com.example.security.CustomSecurityAnnotation", "shortName": "CustomSecurityAnnotation" }
222
222
  ]
223
223
  }
224
224
  ```
@@ -1,6 +1,6 @@
1
- sourcecode/__init__.py,sha256=1iN-frOfJr05mYoED0IYnoxSY-4xnA-sHP9MRe_9Qn4,308
1
+ sourcecode/__init__.py,sha256=KLuqjzw229DS3vhMmuCKlXFinpcH1QG9fhcn7C3J2xg,308
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
- sourcecode/archetype.py,sha256=ezeyCR4E0VeZUUBPwmsWUUPAhIlqwwkc8qDieUx2JWA,31087
3
+ sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
4
4
  sourcecode/architecture_analyzer.py,sha256=6_wC4TYuBYxaqckZS0I1vBSMRPeH2vzlDB48gXwOOd4,46627
5
5
  sourcecode/architecture_summary.py,sha256=pkl6lIOnSy-poa9EzW43U0MkXhx2FDfAFh8pDXLpcTo,26630
6
6
  sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,51351
@@ -10,11 +10,11 @@ sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,
10
10
  sourcecode/canonical_ir.py,sha256=U69m8Vkr0p407xF1q5y1KguHXWEAubYw8NFHR9hDNJk,29178
11
11
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
12
12
  sourcecode/classifier.py,sha256=RhLMDR031FAiUcT3bsf2EL3KrFwl7KsWh_wXXR_Bumo,18729
13
- sourcecode/cli.py,sha256=xYrKelvgspWuus8LM4DcRroXGydbU47CAr-KV-8oysU,299977
13
+ sourcecode/cli.py,sha256=46Chp0JsEWD_j0HUWOx4tOhrsXSWgWV8JhL5G8SyW4I,322078
14
14
  sourcecode/code_notes_analyzer.py,sha256=EJemNCNc9Dn-1RZYu-aNbK0ELzmsyC4s6FdHi3XyNEI,9392
15
15
  sourcecode/confidence_analyzer.py,sha256=_jckZSxksV-OU38vbkxfVNBnWCtlCq8Vwfg23x1uspA,19054
16
16
  sourcecode/context_cache.py,sha256=maws79rLKDwyZ7c9Q2LwlIxqZHRFlaKzErtO3mglWoA,25036
17
- sourcecode/context_graph.py,sha256=8MVDu06bPhvRDTgUqWEvRME2fOw34bFLf4OUILZd13I,43527
17
+ sourcecode/context_graph.py,sha256=WFtUvfxoE8XtQxtRJLvaF4SJuS00ICjNoTk0sH2_V5U,44454
18
18
  sourcecode/context_scorer.py,sha256=QpChSpsmaAYz91rXA4Ue5xzQmNz_ZboZN09YOHScq1U,14679
19
19
  sourcecode/context_summarizer.py,sha256=cI2TZMvEhl0BEma12VtPaX6z03ZVBAetVzuK5GaCOvg,6852
20
20
  sourcecode/contract_model.py,sha256=nRxJKPMs1VHwFTa8AVXhGmaLjti3Lr2sjHDpWgv1bfE,3917
@@ -55,12 +55,12 @@ sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
55
55
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
56
56
  sourcecode/rename_refactor.py,sha256=h6dNFlB9aZ_3q6heeHBkgXQeXaT03nvPSsYH6P8qxFg,12965
57
57
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
58
- sourcecode/repository_ir.py,sha256=3DONydNhy-UU1R_1l-caePlBiUlZxkk9X5h92VXsQ34,284195
58
+ sourcecode/repository_ir.py,sha256=Cyt7avInKNvayaOSgeSjJgWARcGnVXA-4EAmKSve_Jc,290150
59
59
  sourcecode/ris.py,sha256=aG2D4159gtpg968yD3PylYbIXhGFOwIlFBI0DSr84yY,20412
60
60
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
61
61
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
62
62
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
63
- sourcecode/security_config.py,sha256=_m8oQAy2gP7Ho2I-IIMTFFjFVWbJIGlLEOiAWK2TDjs,3503
63
+ sourcecode/security_config.py,sha256=KblMEoRiEjrIE68YsPaUAFebxFp8UM7MS7lAk5CGD8U,3531
64
64
  sourcecode/security_posture.py,sha256=CAJw4oJD0aAW2yRewvd_fonkzi9_HpUjBZlIDZle00s,26034
65
65
  sourcecode/semantic_analyzer.py,sha256=bpgdC6m0_ftVtRf3rSdwhbhWjnZnGxRXaZVcfe4BbcQ,95414
66
66
  sourcecode/semantic_impact_engine.py,sha256=HpgjenY6DLPHPomP7Hw850zA-EBokf1S4O2sh8NegdI,20351
@@ -69,7 +69,7 @@ sourcecode/semantic_services.py,sha256=AO_p7PRfjc-AUh6ThvLMfm4iUGi0issL_frCf8sxG
69
69
  sourcecode/serializer.py,sha256=FmQo7eojA_z3ptLW-Vgze5mrEJ6FabcZqiwbQWWTgFI,130056
70
70
  sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
71
71
  sourcecode/spring_findings.py,sha256=EX7kLZLN74CFyR9iZPm3CI115BfFKNd4WRPuErNljZM,5729
72
- sourcecode/spring_impact.py,sha256=1Eu1vkdwTVsw92iBEJDe_i_FaSf4uGH9Rb0eSgIAAZc,57542
72
+ sourcecode/spring_impact.py,sha256=tjTxKAtmAvBqDVSSsC4ttCzbLaO55Wg4FJdOd_8MtO0,60077
73
73
  sourcecode/spring_model.py,sha256=zOAgFmrRbG4a6KLm1TJl55aWMyPNsz3OS3FSczqPG6A,16594
74
74
  sourcecode/spring_security_audit.py,sha256=Rk-aSohezdc7YDYbSoJquVnwpkDB8ty1BCD-4Hc4R5A,22832
75
75
  sourcecode/spring_semantic.py,sha256=jteQ1PkY9ArFJv0embg_jBIdbOxqrk9mQ2Xz8OF_FKA,14214
@@ -112,14 +112,32 @@ sourcecode/mcp/onboarding/applier.py,sha256=Z2ZqzPh7dTTzT7tfxDbfjDJKiBNQ18CPQCaR
112
112
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
113
113
  sourcecode/mcp/onboarding/detector.py,sha256=kDc0U6kXMuq_GivqwKrgJzIVLVeoLr3RQl63ksW10I8,3327
114
114
  sourcecode/mcp/onboarding/planner.py,sha256=Fopg5f72FDiPfldF7NOxYjcBA_w8hi_jBJpSz39lPb8,1332
115
+ sourcecode/retrieval/__init__.py,sha256=h5pKDzXOlu_NQy-YxOaqMSD6UDNoYsmsYSoNLRRZ5uM,5478
116
+ sourcecode/retrieval/context.py,sha256=hjm_RxReZar6GZvVVg4XNR9sVMOUuilbi1Jt7iUoerU,11116
117
+ sourcecode/retrieval/errors.py,sha256=U-zqxzlzCWe3NYCNOBASTSPn1xS8j9D7LfwpGBiznL4,1452
118
+ sourcecode/retrieval/executor.py,sha256=H2-5by5UCYgX7lgJtmT8JJ73ldxp6xiBFEf6r__j_XA,2048
119
+ sourcecode/retrieval/planner.py,sha256=9DPoutVHEYaGodDTmihZjqeNS6vFwJKtANnmO-4cvlU,13375
120
+ sourcecode/retrieval/query.py,sha256=QoVKsuV-PYuunbw06MvV_HdHLAKbo_r-S9xEWFoAVB4,18406
121
+ sourcecode/retrieval/request.py,sha256=gKK5iL5X7ScMz_HQ53FX4G6lbFdkhh0vNkIuxGDLt74,4023
122
+ sourcecode/retrieval/resolution.py,sha256=boa9ybj3AQK5CEMYJn9jA8e0XRNyKKD9avRM5JAIwyA,7163
123
+ sourcecode/retrieval/result.py,sha256=mx6ioA6y6bkNbF6SC3n0N99NTuNEy1uz_5YZQ2euflA,4726
124
+ sourcecode/retrieval/retriever.py,sha256=YdnbaXaWhFKyShuuE6fj36GfgpuGetqOFzx7_D2A9CY,2654
125
+ sourcecode/retrieval/runtime.py,sha256=ODQlPqyS4OfMtCb1oEA_0t7vK0uKPBLhB4K2CnytPUU,10822
126
+ sourcecode/retrieval/steps.py,sha256=wyOiO84fuDgdd-HdugzGSEriN-lN891PTc3gU7lgcmI,9959
127
+ sourcecode/retrieval/steps_endpoint.py,sha256=yOrTA9mWoIrEBVc_-do8m_AAGqjyFqR7jJ9w5LQXycM,12368
128
+ sourcecode/retrieval/steps_graph.py,sha256=ZWA4i1hwCKiP0AOZ-4PJsLHH84abOYVVh6ERNkdE9v0,10050
129
+ sourcecode/retrieval/steps_impact.py,sha256=M0AwU5Ue7nkj-A7DYgQI2BP0BV1b0TFZRrYIydF-Ajw,11605
130
+ sourcecode/retrieval/steps_intf.py,sha256=Jx5Gnm7UT1jOE7YloD0ee3d8PuliX5FgpdvlHh36OBs,9341
131
+ sourcecode/retrieval/steps_struct.py,sha256=jO9L49hziTlS_xhuhCGUavgyAL3ZAnJSOZB-UA4GaRM,13893
132
+ sourcecode/retrieval/steps_txsec.py,sha256=ltyxFAFLHfUglGLGz7i3tY_bovBoKo1d6R2t1xZTcnk,13608
115
133
  sourcecode/telemetry/__init__.py,sha256=6wsJnXsgsL_BtMK4hr5Ae1GToHeWej53F4o_-Tc23MQ,3406
116
134
  sourcecode/telemetry/config.py,sha256=u_zctgmA1pVT3Ai5jm-pdLY_5kJTRg4Xq2bvIbGBQE4,3727
117
135
  sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i5Q,2194
118
136
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
119
137
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
120
138
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
121
- sourcecode-2.4.0.dist-info/METADATA,sha256=fairNRCqgRAaSicMGWcWQcPral9_3NeVBIDTrv2rHaI,10837
122
- sourcecode-2.4.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
123
- sourcecode-2.4.0.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
124
- sourcecode-2.4.0.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
125
- sourcecode-2.4.0.dist-info/RECORD,,
139
+ sourcecode-2.5.1.dist-info/METADATA,sha256=eQZ2Bo5Okcz-pi14-mRu9KvrxZzIwkq7WExZEz0pjPE,10851
140
+ sourcecode-2.5.1.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
141
+ sourcecode-2.5.1.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
142
+ sourcecode-2.5.1.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
143
+ sourcecode-2.5.1.dist-info/RECORD,,