sourcecode 2.5.2__py3-none-any.whl → 2.5.3__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
@@ -4,4 +4,4 @@ ASK Engine is the product. ``ask`` is the canonical CLI command; ``sourcecode``
4
4
  the legacy compatibility alias and the Python/PyPI package name. See
5
5
  docs/PRODUCT_IDENTITY.md (normative)."""
6
6
 
7
- __version__ = "2.5.2"
7
+ __version__ = "2.5.3"
@@ -253,8 +253,9 @@ class CanonicalRepositoryIR:
253
253
 
254
254
  @property
255
255
  def field_types(self) -> dict[str, dict[str, str]]:
256
- """{class_fqn -> {field_name -> declared_type_simple_name}} for every field
257
- (annotated or plain). Binds an invocation receiver to a typed dependency
256
+ """{class_fqn -> {field_name -> declared_type}} for every field (annotated or
257
+ plain); the type is import-resolved to an FQN when the declaring file imported
258
+ it, else a simple name. Binds an invocation receiver to a typed dependency —
258
259
  covers plain setter/XML-injected fields the injection graph cannot see. A
259
260
  projection over field declarations, no new symbols/edges. Present only when
260
261
  built via build_canonical_ir; not a cir_hash input."""
@@ -115,6 +115,11 @@ class EvidenceBundle:
115
115
 
116
116
  _PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
117
117
  _IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
118
+ # Single-type imports only (`import a.b.Foo;`) — the declaration that binds a simple
119
+ # name to exactly one type. Static imports bind a member, not a type, and wildcards
120
+ # bind nothing resolvable; both fail to match ([\w.]+ cannot span the space before
121
+ # a static FQN, and `*` is outside the class).
122
+ _SINGLE_TYPE_IMPORT_RE = re.compile(r'^\s*import\s+([\w.]+)\s*;', re.MULTILINE)
118
123
  _ANN_RE = re.compile(r'^(@[\w.]+)')
119
124
  _ANN_WITH_ARGS_RE = re.compile(
120
125
  r'^(@[\w.]+)\s*'
@@ -2000,19 +2005,39 @@ def _strip_leading_annotations(line: str) -> str:
2000
2005
  return line.strip()
2001
2006
 
2002
2007
 
2003
- def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
2004
- """{class_fqn -> {field_name -> declared_type_simple_name}} for EVERY field
2005
- (annotated or plain), the fact that binds an invocation receiver to a typed
2006
- dependency. Complements the injection graph, which only sees @-annotated /
2008
+ def _file_import_map(source: str) -> dict[str, str]:
2009
+ """{simple name -> FQN} for the file's single-type imports — the per-file fact
2010
+ that disambiguates a declared type whose simple name is not unique in the repo
2011
+ (Broadleaf declares two `AdminSecurityService`; only the import says which)."""
2012
+ out: dict[str, str] = {}
2013
+ for fqn in _SINGLE_TYPE_IMPORT_RE.findall(source):
2014
+ if "." in fqn:
2015
+ out[fqn.rsplit(".", 1)[-1]] = fqn
2016
+ return out
2017
+
2018
+
2019
+ def _extract_field_types(
2020
+ source: str, *, imports: Optional[dict[str, str]] = None
2021
+ ) -> dict[str, dict[str, str]]:
2022
+ """{class_fqn -> {field_name -> declared_type}} for EVERY field (annotated or
2023
+ plain), the fact that binds an invocation receiver to a typed dependency.
2024
+ Complements the injection graph, which only sees @-annotated /
2007
2025
  constructor-injected fields — plain setter/XML-injected fields (e.g. openmrs
2008
2026
  `private TemplateDAO templateDAO;`) are invisible to it but common.
2009
2027
 
2028
+ The declared type is the file-import-resolved FQN when the owning file imports
2029
+ it explicitly, else the bare simple name (wildcard import, same package, or
2030
+ JDK-implicit). Consumers must accept both shapes — see `_resolve_declared_type`,
2031
+ which finishes resolution against the repo symbol table.
2032
+
2010
2033
  A hash-neutral side-stream (no new symbols/edges; not a cir_hash input),
2011
2034
  emitted only under emit_body_facts. Class-scope only: a field sits directly in
2012
2035
  a class body (depth == class-decl depth + 1), never inside a method body, so
2013
2036
  method locals are excluded. `_FIELD_DECL_RE` (name followed by ';'/'=') is
2014
2037
  disjoint from method decls (name followed by '(')."""
2015
2038
  out: dict[str, dict[str, str]] = {}
2039
+ if imports is None:
2040
+ imports = _file_import_map(source)
2016
2041
  depth = 0
2017
2042
  class_stack: list[tuple[str, int]] = []
2018
2043
  package = ""
@@ -2060,7 +2085,8 @@ def _extract_field_types(source: str) -> dict[str, dict[str, str]]:
2060
2085
  fname = fld_m.group("name")
2061
2086
  outer, _ = _type_tokens(fld_m.group("type").strip())
2062
2087
  if fname and outer and fname not in _JAVA_KEYWORDS:
2063
- out.setdefault(class_stack[-1][0], {}).setdefault(fname, outer)
2088
+ out.setdefault(class_stack[-1][0], {}).setdefault(
2089
+ fname, imports.get(outer, outer))
2064
2090
  depth += net
2065
2091
  _pop_closed(class_stack, depth)
2066
2092
  return out
@@ -4393,6 +4419,121 @@ def extract_file_ir(
4393
4419
  return _assemble(symbols, relations, changed_symbols, spring_summary, route_diffs)
4394
4420
 
4395
4421
 
4422
+ def _resolve_declared_type(
4423
+ declared: str,
4424
+ owner_cls: str,
4425
+ type_simple_to_fqn: dict[str, list[str]],
4426
+ known_types: set[str],
4427
+ ) -> Optional[str]:
4428
+ """The in-repo class FQN a field's declared type names, or None when it cannot
4429
+ be pinned to exactly one.
4430
+
4431
+ `declared` is what `_extract_field_types` recorded: an import-resolved FQN when
4432
+ the owning file imported the type explicitly, else a bare simple name. Resolution
4433
+ follows Java scoping order — an explicit import shadows a same-package type
4434
+ (JLS 6.4.1), which in turn beats a repo-wide simple-name guess. A simple name
4435
+ that is neither same-package nor unique stays unresolved rather than guessed
4436
+ (INV honesty): the wrong caller edge is worse than a missing one.
4437
+ """
4438
+ if not declared:
4439
+ return None
4440
+ if "." in declared:
4441
+ return declared if declared in known_types else None
4442
+ if "." in owner_cls:
4443
+ scoped = f"{owner_cls.rsplit('.', 1)[0]}.{declared}"
4444
+ if scoped in known_types:
4445
+ return scoped
4446
+ fqns = type_simple_to_fqn.get(declared)
4447
+ return fqns[0] if fqns and len(fqns) == 1 else None
4448
+
4449
+
4450
+ def _type_indices(
4451
+ symbols: list[SymbolRecord],
4452
+ ) -> tuple[dict[str, list[str]], set[str], dict[str, dict[str, list[str]]]]:
4453
+ """(type simple name → in-repo class FQN(s), all class FQNs, class FQN →
4454
+ {method name → [method FQN]}) — the symbol-table views the call-edge post-passes
4455
+ join against."""
4456
+ type_simple_to_fqn: dict[str, list[str]] = {}
4457
+ known_types: set[str] = set()
4458
+ class_methods: dict[str, dict[str, list[str]]] = {}
4459
+ for s in symbols:
4460
+ if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
4461
+ type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
4462
+ known_types.add(s.symbol)
4463
+ elif s.symbol_kind == "method" and "#" in s.symbol:
4464
+ cls = _enclosing_class(s.symbol)
4465
+ name = s.symbol.rsplit("#", 1)[1]
4466
+ class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
4467
+ return type_simple_to_fqn, known_types, class_methods
4468
+
4469
+
4470
+ def _static_typed_call_edges(
4471
+ body_facts: dict[str, list[dict]],
4472
+ class_imports: dict[str, dict[str, str]],
4473
+ field_types: dict[str, dict[str, str]],
4474
+ symbols: list[SymbolRecord],
4475
+ ) -> list[RelationEdge]:
4476
+ """Method-level `calls` edges for a static invocation through a type receiver
4477
+ (`Type.method(...)`).
4478
+
4479
+ The per-file static scan in `_build_relations` throws the callee name away and
4480
+ attributes the call to every class in the file, so it can only say
4481
+ `CallerClass --calls--> TargetClass`. Nothing ever lands on the method node, so
4482
+ `impact-chain Utility#method` resolved 0 callers however many static call sites
4483
+ existed: Broadleaf's `BroadleafCurrencyUtils#getNumberFormatFromCache` has 5+
4484
+ in-repo callers and reported none, which P1-B could only flag as unproven
4485
+ because the edge itself was missing.
4486
+
4487
+ Joins facts already extracted (no new parse — Prime Directive): the invocation
4488
+ atom already carries `receiver`/`callee` for `Type.method(...)`, the owner file's
4489
+ imports resolve the receiver to a type (`_resolve_declared_type`), and the symbol
4490
+ table supplies that type's methods. A receiver that is a field, a lower-case
4491
+ variable, or an unresolvable/library type yields nothing — unresolved stays
4492
+ unresolved (INV honesty).
4493
+
4494
+ These edges add to the coarse class-level ones; see the call site for why
4495
+ substituting them loses reach.
4496
+ """
4497
+ if not body_facts or not class_imports:
4498
+ return []
4499
+
4500
+ type_simple_to_fqn, known_types, class_methods = _type_indices(symbols)
4501
+ edges: list[RelationEdge] = []
4502
+ for owner_fqn in sorted(body_facts):
4503
+ owner_cls = _enclosing_class(owner_fqn)
4504
+ imports = class_imports.get(owner_cls)
4505
+ if imports is None:
4506
+ continue
4507
+ fields = field_types.get(owner_cls) or {}
4508
+ for atom in body_facts[owner_fqn]:
4509
+ if atom.get("fact") != "invocation":
4510
+ continue
4511
+ receiver, callee = atom.get("receiver"), atom.get("callee")
4512
+ if not receiver or not callee or receiver == "this":
4513
+ continue
4514
+ if not receiver[0].isupper():
4515
+ continue # variable/field receiver — `_receiver_typed_call_edges`
4516
+ if receiver in fields:
4517
+ continue # a field shadows the type name → the field pass owns it
4518
+ target_cls = _resolve_declared_type(
4519
+ imports.get(receiver, receiver), owner_cls, type_simple_to_fqn, known_types
4520
+ )
4521
+ if not target_cls or target_cls == owner_cls:
4522
+ continue
4523
+ target_methods = class_methods.get(target_cls, {}).get(callee)
4524
+ if not target_methods:
4525
+ continue # callee not declared on the target type
4526
+ for mfqn in target_methods:
4527
+ edges.append(RelationEdge(
4528
+ from_symbol=owner_fqn,
4529
+ to_symbol=mfqn,
4530
+ type="calls",
4531
+ confidence="medium",
4532
+ evidence={"type": "method_call", "value": f"{receiver}.{callee}(...)"},
4533
+ ))
4534
+ return edges
4535
+
4536
+
4396
4537
  def _receiver_typed_call_edges(
4397
4538
  body_facts: dict[str, list[dict]],
4398
4539
  field_types: dict[str, dict[str, str]],
@@ -4412,30 +4553,22 @@ def _receiver_typed_call_edges(
4412
4553
  Joins facts already extracted (no new parse — Prime Directive):
4413
4554
  - invocation atom (owner method → {receiver, callee}) [body_facts]
4414
4555
  - declared type of `receiver` on the owner's class [field_types]
4415
- - target type resolved to a SINGLE in-repo class FQN [symbols]
4556
+ - target type resolved to a SINGLE in-repo class FQN, by the owner
4557
+ file's import first and only then by simple name [symbols]
4416
4558
  - callee resolved to that class's method(s) of that name [symbols]
4417
4559
 
4418
4560
  Emits `owner_method --calls--> target_class#callee` (every overload of the name —
4419
4561
  arity is not resolved by name alone, matching `_intra_class_call_edges`). Anything
4420
- unresolved — a local variable (no field entry), a JDK/library type, an ambiguous
4421
- simple name, or a callee the target class does not declare — yields nothing: an
4422
- unresolved fact stays unresolved rather than being guessed (INV honesty). Runs as
4423
- a repo-wide post-pass because the target type + its methods live in other files.
4562
+ unresolved — a local variable (no field entry), a JDK/library type, a simple name
4563
+ that is still ambiguous after `_resolve_declared_type`, or a callee the target
4564
+ class does not declare yields nothing: an unresolved fact stays unresolved
4565
+ rather than being guessed (INV honesty). Runs as a repo-wide post-pass because
4566
+ the target type + its methods live in other files.
4424
4567
  """
4425
4568
  if not body_facts or not field_types:
4426
4569
  return []
4427
4570
 
4428
- # type simple name → in-repo class FQN(s); class FQN → {method name → [method FQN]}
4429
- type_simple_to_fqn: dict[str, list[str]] = {}
4430
- class_methods: dict[str, dict[str, list[str]]] = {}
4431
- for s in symbols:
4432
- if s.type in ("class", "interface", "enum", "record") and "#" not in s.symbol:
4433
- type_simple_to_fqn.setdefault(s.symbol.rsplit(".", 1)[-1], []).append(s.symbol)
4434
- elif s.symbol_kind == "method" and "#" in s.symbol:
4435
- cls = _enclosing_class(s.symbol)
4436
- name = s.symbol.rsplit("#", 1)[1]
4437
- class_methods.setdefault(cls, {}).setdefault(name, []).append(s.symbol)
4438
-
4571
+ type_simple_to_fqn, known_types, class_methods = _type_indices(symbols)
4439
4572
  edges: list[RelationEdge] = []
4440
4573
  for owner_fqn in sorted(body_facts):
4441
4574
  owner_cls = _enclosing_class(owner_fqn)
@@ -4448,13 +4581,13 @@ def _receiver_typed_call_edges(
4448
4581
  receiver, callee = atom.get("receiver"), atom.get("callee")
4449
4582
  if not receiver or receiver == "this" or not callee:
4450
4583
  continue
4451
- tsimple = fmap.get(receiver)
4452
- if not tsimple:
4584
+ declared = fmap.get(receiver)
4585
+ if not declared:
4453
4586
  continue # not a typed field (local / unknown)
4454
- tfqns = type_simple_to_fqn.get(tsimple)
4455
- if not tfqns or len(tfqns) != 1:
4587
+ target_cls = _resolve_declared_type(
4588
+ declared, owner_cls, type_simple_to_fqn, known_types)
4589
+ if not target_cls:
4456
4590
  continue # unresolved / ambiguous type → skip (honest)
4457
- target_cls = tfqns[0]
4458
4591
  if target_cls == owner_cls:
4459
4592
  continue # intra-class handled by _intra_class_call_edges
4460
4593
  target_methods = class_methods.get(target_cls, {}).get(callee)
@@ -4508,6 +4641,7 @@ def build_repo_ir(
4508
4641
  _all_class_type_refs: dict[str, list[dict]] = {}
4509
4642
  _all_annotation_arg_values: dict[str, dict[str, dict[str, str]]] = {}
4510
4643
  _all_field_types: dict[str, dict[str, str]] = {}
4644
+ _all_class_imports: dict[str, dict[str, str]] = {}
4511
4645
 
4512
4646
  # H-04: prefetch changed-file list once; avoids O(n) `git show` calls.
4513
4647
  # _since_changed=None means git unavailable → fall back to per-file fetch.
@@ -4580,7 +4714,14 @@ def build_repo_ir(
4580
4714
  _all_guard_facts.update(_extract_guard_facts(symbols, source))
4581
4715
  _all_class_type_refs.update(_extract_class_type_refs(symbols))
4582
4716
  _all_annotation_arg_values.update(_parse_annotation_arg_values(symbols))
4583
- _all_field_types.update(_extract_field_types(source))
4717
+ _f_imports = _file_import_map(source)
4718
+ _all_field_types.update(_extract_field_types(source, imports=_f_imports))
4719
+ # Every type declared in this file resolves simple names through this
4720
+ # file's imports — the fact `_static_typed_call_edges` needs to bind a
4721
+ # `Type.method(...)` receiver to a type (P1-A's rule, applied to statics).
4722
+ for _s in symbols:
4723
+ if _s.type in ("class", "interface", "enum", "record") and "#" not in _s.symbol:
4724
+ _all_class_imports[_s.symbol] = _f_imports
4584
4725
 
4585
4726
  old_source: Optional[str] = None
4586
4727
  if since:
@@ -4626,6 +4767,26 @@ def build_repo_ir(
4626
4767
  _receiver_typed_call_edges(_all_body_facts, _all_field_types, all_symbols)
4627
4768
  )
4628
4769
 
4770
+ # Static calls through a type receiver (`Type.method(...)`). The per-file scan can
4771
+ # only reach the target CLASS (it discards the callee), so no method-level query on
4772
+ # a static utility ever resolved a caller.
4773
+ #
4774
+ # These edges are ADDED, never substituted for the class-level ones. Superseding the
4775
+ # class edge was tried and measurably wrong: body facts cover method bodies only, so
4776
+ # a static call in a field initializer or static block produces no invocation atom,
4777
+ # and dropping the class edge there erased the coupling — Broadleaf's
4778
+ # BroadleafCurrencyUtils lost transitive reach (1325 indirect callers → 561) and all
4779
+ # 20 affected endpoints. The class-level edge is a coarse over-approximation (the
4780
+ # scan attributes a call to every class in the file); this pass is precise but
4781
+ # partial. Keeping both leaves a caller reported at both granularities (`X` and
4782
+ # `X#m`) — redundant, but each statement is true, and no reach is lost.
4783
+ if emit_body_facts:
4784
+ all_relations.extend(
4785
+ _static_typed_call_edges(
4786
+ _all_body_facts, _all_class_imports, _all_field_types, all_symbols
4787
+ )
4788
+ )
4789
+
4629
4790
  spring_summary = _build_spring_summary(all_symbols)
4630
4791
 
4631
4792
  # Deduplicate relations
@@ -28,6 +28,7 @@ from __future__ import annotations
28
28
  from dataclasses import dataclass
29
29
  from typing import TYPE_CHECKING, Optional
30
30
 
31
+ from sourcecode.repository_ir import _resolve_declared_type
31
32
  from sourcecode.semantic_services import SemanticServices
32
33
 
33
34
  if TYPE_CHECKING: # avoid import cycle
@@ -236,13 +237,15 @@ class SemanticImpactEngine:
236
237
  """(field_name, type_fqn) for fields whose declared type resolves to a role
237
238
  in `roles` — the plain setter/XML-injected dependencies the injection graph
238
239
  cannot see (e.g. openmrs `private TemplateDAO templateDAO;`). Uses field-type
239
- facts (#10) resolved to a concrete type by simple name (unambiguous only)."""
240
+ facts (#10), which carry the owning file's import-resolved FQN when it had
241
+ one; a bare simple name still resolves only while unambiguous."""
240
242
  by_simple = self.s.types_by_simple()
243
+ known = {f for fqns in by_simple.values() for f in fqns}
241
244
  out: list[tuple[str, str]] = []
242
- for fname, tsimple in sorted(self.s.field_types_of(owner_fqn).items()):
243
- fqns = by_simple.get(tsimple, [])
244
- if len(fqns) == 1 and self._role(fqns[0]) in roles:
245
- out.append((fname, fqns[0]))
245
+ for fname, declared in sorted(self.s.field_types_of(owner_fqn).items()):
246
+ fqn = _resolve_declared_type(declared, owner_fqn, by_simple, known)
247
+ if fqn and self._role(fqn) in roles:
248
+ out.append((fname, fqn))
246
249
  return out
247
250
 
248
251
  def _called_via_receiver(self, owner_fqn: str, receiver: str) -> Optional[str]:
@@ -209,9 +209,11 @@ class SemanticServices:
209
209
  return self._g.annotation_arg_values_in(fqn)
210
210
 
211
211
  def field_types_of(self, class_fqn: str) -> dict[str, str]:
212
- """{field_name -> declared_type_simple_name} for a class's fields (annotated
213
- or plain). Resolves a body invocation's receiver to its typed dependency —
214
- the fact injection edges miss for plain setter/XML-injected fields."""
212
+ """{field_name -> declared_type} for a class's fields (annotated or plain).
213
+ Resolves a body invocation's receiver to its typed dependency — the fact
214
+ injection edges miss for plain setter/XML-injected fields. The type is the
215
+ FQN when the declaring file imported it explicitly, else a simple name;
216
+ `repository_ir._resolve_declared_type` handles both."""
215
217
  return self._g.field_types_in(class_fqn)
216
218
 
217
219
  def return_type_index(self) -> dict[str, set[str]]:
@@ -744,10 +744,21 @@ def _build_chain_explanation(
744
744
  endpoints_affected: int,
745
745
  findings: int,
746
746
  confidence: str,
747
+ blind_spot: bool = False,
747
748
  ) -> str:
748
749
  """Human-readable rationale, phrased to match the `impact` command's
749
- explanation so both surfaces read consistently."""
750
+ explanation so both surfaces read consistently.
751
+
752
+ `blind_spot` says a positively-detected gap explains the empty result, so it must
753
+ not be narrated as an isolated change: the warnings carry the diagnosis, and this
754
+ sentence is what an agent quotes."""
750
755
  if not direct_callers and not indirect_callers and not endpoints_affected:
756
+ if blind_spot:
757
+ return (
758
+ "No callers or endpoints resolved, but this analysis has a known blind "
759
+ "spot (see analysis_warnings) — an empty result here is NOT evidence of "
760
+ "an isolated or low-risk change."
761
+ )
751
762
  return "No callers or endpoints found in the impact chain. Low-risk isolated change."
752
763
  parts: list[str] = []
753
764
  if direct_callers:
@@ -1130,6 +1141,43 @@ class ImpactOrchestrator:
1130
1141
  "proof this symbol is unused."
1131
1142
  )
1132
1143
 
1144
+ # P1-B: every guard above needs a class-level seed, so a `Class#method` query
1145
+ # never reached one — an empty method blast was reported as a high-confidence
1146
+ # "low-risk isolated change" no matter what was known about the class. When the
1147
+ # declaring class has inbound DI/import edges, the class-level query resolves
1148
+ # those callers correctly (they land on the class node), so the tool used to
1149
+ # contradict itself across granularity — "isolated" for the method, "critical"
1150
+ # for its class — with no signal that the narrower, more natural query was the
1151
+ # less trustworthy one. The method may still be genuinely uncalled; what is
1152
+ # unproven is the *emptiness*, so this downgrades confidence and warns rather
1153
+ # than inventing callers.
1154
+ method_scope_blind_spot = False
1155
+ if empty_blast and not class_level_seed and resolution != "not_found":
1156
+ _crev = cir.reverse_graph.get(_class_of(resolved_symbol)) or {}
1157
+ method_scope_inbound = sorted(
1158
+ {c for et in ("injects", "imports") for c in (_crev.get(et) or [])}
1159
+ )
1160
+ if method_scope_inbound:
1161
+ method_scope_blind_spot = True
1162
+ warnings.append(
1163
+ f"Method-scoped empty blast radius is unproven (P1-B): no call edge "
1164
+ f"resolves to this method, but {len(method_scope_inbound)} in-repo "
1165
+ f"class(es) inject or import the declaring class "
1166
+ f"{_class_of(resolved_symbol)}. The call may travel an edge the "
1167
+ "graph does not model (wildcard-imported or generic receiver, "
1168
+ "reflection, method reference, framework callback). 0 callers is "
1169
+ "NOT proof this method is unused — re-query at class level (drop "
1170
+ "the '#method') to see the declaring class's callers."
1171
+ )
1172
+
1173
+ blind_spots = (
1174
+ # framework_di stops being a blind spot once CH-007 recovers callers
1175
+ (["framework_di"] if framework_di_blind_spot and not di_recovered else [])
1176
+ + (["value_type"] if value_type_blind_spot else [])
1177
+ + (["unresolved_refs"] if unresolved_ref_blind_spot else [])
1178
+ + (["method_scope_unproven"] if method_scope_blind_spot else [])
1179
+ )
1180
+
1133
1181
  confidence: str
1134
1182
  if resolution == "not_found":
1135
1183
  confidence = "low"
@@ -1137,7 +1185,12 @@ class ImpactOrchestrator:
1137
1185
  # Callers recovered structurally via the external-interface DI bridge.
1138
1186
  # Unambiguous binding (single in-repo impl) → medium; ambiguous → low.
1139
1187
  confidence = "low" if di_binding_ambiguous else "medium"
1140
- elif framework_di_blind_spot or value_type_blind_spot or unresolved_ref_blind_spot:
1188
+ elif (
1189
+ framework_di_blind_spot
1190
+ or value_type_blind_spot
1191
+ or unresolved_ref_blind_spot
1192
+ or method_scope_blind_spot
1193
+ ):
1141
1194
  confidence = "low"
1142
1195
  elif resolution == "partial" or confidence_reducing:
1143
1196
  confidence = "medium"
@@ -1168,6 +1221,7 @@ class ImpactOrchestrator:
1168
1221
  len(endpoints_affected),
1169
1222
  len(impact_findings),
1170
1223
  confidence,
1224
+ blind_spot=bool(blind_spots),
1171
1225
  ),
1172
1226
  metadata={
1173
1227
  "analysis_depth": depth,
@@ -1177,12 +1231,7 @@ class ImpactOrchestrator:
1177
1231
  "risk_score": risk_score,
1178
1232
  "model_build_time_ms": model.build_time_ms,
1179
1233
  "query_time_ms": elapsed_ms,
1180
- "blind_spots": (
1181
- # framework_di stops being a blind spot once CH-007 recovers callers
1182
- (["framework_di"] if framework_di_blind_spot and not di_recovered else [])
1183
- + (["value_type"] if value_type_blind_spot else [])
1184
- + (["unresolved_refs"] if unresolved_ref_blind_spot else [])
1185
- ),
1234
+ "blind_spots": blind_spots,
1186
1235
  "external_supertypes": external_supertypes,
1187
1236
  "external_iface_callers_recovered": len(di_recovered_callers),
1188
1237
  "external_iface_binding_ambiguous": di_binding_ambiguous,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 2.5.2
3
+ Version: 2.5.3
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=nbtWf3A6IYQQ1rNky7L2moDkYZ2Uy7Nj9Mb5R7f2-KQ,308
1
+ sourcecode/__init__.py,sha256=amy8rm-VG3PUoGLIDW63Np7SjY6kBcCR5HIop8vdbDE,308
2
2
  sourcecode/adaptive_scanner.py,sha256=XffluXKzJUXrMtjEiAOnSNPZnztdIcts17T9ouHeID0,10521
3
3
  sourcecode/archetype.py,sha256=d7yoN6Tj4OBj-VgSMPEKg4WF9H8FypnZ5A6AkUh5oIE,34484
4
4
  sourcecode/architecture_analyzer.py,sha256=6_wC4TYuBYxaqckZS0I1vBSMRPeH2vzlDB48gXwOOd4,46627
@@ -7,7 +7,7 @@ sourcecode/ast_extractor.py,sha256=FEsYTsMCXbJfSYQZRk1k-I-PjUNGnKqO1IxMftsgA5U,5
7
7
  sourcecode/cache.py,sha256=1V3vsaODAa2UBJAC0xpvxpmRdriCezQx5Q8JCcfgziE,31892
8
8
  sourcecode/call_surface.py,sha256=fiqYfHooxN1fX9oQoysq1LS3LoZcobhyUNjAGEZKpwk,4148
9
9
  sourcecode/caller_metrics.py,sha256=HG8RCOkpzzsEGdnMRob6byrwjoj5__t0TEsBj4yR7ks,2574
10
- sourcecode/canonical_ir.py,sha256=U69m8Vkr0p407xF1q5y1KguHXWEAubYw8NFHR9hDNJk,29178
10
+ sourcecode/canonical_ir.py,sha256=5hLAhhSpXNOdZAvPt-J-rXccJE6M3A6gVhmhU0Tym6Y,29269
11
11
  sourcecode/cir_graphs.py,sha256=9G0HHj1kw2325IDyzo2OpX73BNswEckecf4MZUXB4JM,12078
12
12
  sourcecode/classifier.py,sha256=RhLMDR031FAiUcT3bsf2EL3KrFwl7KsWh_wXXR_Bumo,18729
13
13
  sourcecode/cli.py,sha256=46Chp0JsEWD_j0HUWOx4tOhrsXSWgWV8JhL5G8SyW4I,322078
@@ -55,7 +55,7 @@ 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=bG81ARrPBaPZoGqGT9bXviGCHZ27D7zGli8KShn9-cI,291158
58
+ sourcecode/repository_ir.py,sha256=PPu5_zqGMCNk-D9lLmd8cOuhOgQaFeQDwDaar9tGpas,299121
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
@@ -63,13 +63,13 @@ sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
63
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
- sourcecode/semantic_impact_engine.py,sha256=HpgjenY6DLPHPomP7Hw850zA-EBokf1S4O2sh8NegdI,20351
66
+ sourcecode/semantic_impact_engine.py,sha256=t09IirGC3JjQDy33JZd1_WKzQVKXkoNl3-XEUr5kjis,20563
67
67
  sourcecode/semantic_integration_engine.py,sha256=rxoCuP-uM_Y4-ELeBN0sz1a88e2eI1XTJDgsS57IOLI,15513
68
- sourcecode/semantic_services.py,sha256=AO_p7PRfjc-AUh6ThvLMfm4iUGi0issL_frCf8sxGlI,10637
68
+ sourcecode/semantic_services.py,sha256=nbUuPv-F01USTt_9CHT8iy_ucCIw3fz4W3Aquea_pd4,10782
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=tjTxKAtmAvBqDVSSsC4ttCzbLaO55Wg4FJdOd_8MtO0,60077
72
+ sourcecode/spring_impact.py,sha256=LlcR7PDoXKq93vKBH8ApaxfkpHCZH-xCUwKcVI5-qxY,62770
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
@@ -136,8 +136,8 @@ sourcecode/telemetry/consent.py,sha256=LIAO9ohJZF8OuZwM4u1VWtALlYfTCCKq4wV3Vwc7i
136
136
  sourcecode/telemetry/events.py,sha256=LtzYfaX9Ilckj5PTvAcTpDa9mLqDsYPDUiDkRa58piY,2580
137
137
  sourcecode/telemetry/filters.py,sha256=NHa5T-6DaZduQPFuC34jOqHWQgSizM-Ygq8aZ4j19ng,5834
138
138
  sourcecode/telemetry/transport.py,sha256=4gGHsq0WeY9VywEZXA3vUxykfiYnw9uuqfjAAec7F8o,1681
139
- sourcecode-2.5.2.dist-info/METADATA,sha256=cOmLhyx7sqYDB9m4eU6MkDuV4hfXzK68a2cUC_-N490,10851
140
- sourcecode-2.5.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
141
- sourcecode-2.5.2.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
142
- sourcecode-2.5.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
143
- sourcecode-2.5.2.dist-info/RECORD,,
139
+ sourcecode-2.5.3.dist-info/METADATA,sha256=UF4Kd3rd9jX6KdYFUileDUJOnV8hfWcTud-t5elXcdk,10851
140
+ sourcecode-2.5.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
141
+ sourcecode-2.5.3.dist-info/entry_points.txt,sha256=-JEAdChrK5We51kZcb7OaDcyil-dHBjBPL-NhuO-QY8,89
142
+ sourcecode-2.5.3.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
143
+ sourcecode-2.5.3.dist-info/RECORD,,