sourcecode 1.35.5__py3-none-any.whl → 1.35.7__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.35.5"
3
+ __version__ = "1.35.7"
@@ -10,7 +10,7 @@ Internal helpers remain explicitly marked and are not exported to MCP.
10
10
  """
11
11
  from __future__ import annotations
12
12
 
13
- from dataclasses import dataclass, field
13
+ from dataclasses import dataclass, field, replace
14
14
  import inspect
15
15
  from functools import lru_cache
16
16
  from typing import Any, Callable, Mapping
@@ -69,6 +69,7 @@ class ToolSpec:
69
69
  aliases: tuple[str, ...] = ()
70
70
  internal: bool = False
71
71
  not_exposed_to_cli: bool = False
72
+ mcp_hidden: bool = False
72
73
  docstring: str = ""
73
74
  runtime_command: str = ""
74
75
  _argv_builder: Callable[[Mapping[str, Any]], list[str]] | None = field(
@@ -82,6 +83,10 @@ class ToolSpec:
82
83
  def public(self) -> bool:
83
84
  return not self.internal and not self.not_exposed_to_cli
84
85
 
86
+ @property
87
+ def mcp_visible(self) -> bool:
88
+ return self.public and not self.mcp_hidden
89
+
85
90
  def build_argv(self, inputs: Mapping[str, Any]) -> list[str]:
86
91
  if self._argv_builder is None:
87
92
  raise RuntimeError(f"ToolSpec '{self.name}' has no argv builder")
@@ -488,8 +493,11 @@ def _alias_spec(
488
493
  aliases: tuple[str, ...] = (),
489
494
  internal: bool = False,
490
495
  not_exposed_to_cli: bool = False,
496
+ mcp_hidden: bool = False,
497
+ docstring_override: str | None = None,
491
498
  validator: Callable[[Mapping[str, Any]], str | None] | None = None,
492
499
  ) -> ToolSpec:
500
+ doc = docstring_override or _build_contract_doc(description, params, cli_path, supported_targets, unsupported_targets)
493
501
  return ToolSpec(
494
502
  name=name,
495
503
  description=description,
@@ -500,7 +508,8 @@ def _alias_spec(
500
508
  aliases=aliases,
501
509
  internal=internal,
502
510
  not_exposed_to_cli=not_exposed_to_cli,
503
- docstring=_build_contract_doc(description, params, cli_path, supported_targets, unsupported_targets),
511
+ mcp_hidden=mcp_hidden,
512
+ docstring=doc,
504
513
  runtime_command=" ".join(cli_path) if cli_path else "sourcecode",
505
514
  _argv_builder=argv_builder,
506
515
  validator=validator,
@@ -788,17 +797,169 @@ def _internal_specs() -> list[ToolSpec]:
788
797
  ]
789
798
 
790
799
 
800
+ # Canonical CLI tools that are MCP-noise: raw passthroughs, meta-commands, and duplicates
801
+ # superseded by cleaner alias variants. Still tracked by validate_registry(); just not served.
802
+ _MCP_HIDDEN_CANONICAL_TOOLS: frozenset[str] = frozenset({
803
+ # Raw CLI passthroughs (clean alias exists)
804
+ "sourcecode_root", # 40+ flags, no agent guidance; use get_compact_context / get_agent_context
805
+ "prepare_context", # requires knowing subtask string; use task-specific aliases
806
+ "repo_ir", # raw IR dump; use get_ir_summary
807
+ "fix_bug", # raw Pro command; use fix_bug_context for MCP
808
+ "review_pr", # raw Pro command; use review_pr_context for MCP
809
+ "onboard", # raw with llm_prompt/copy flags; use onboard_context
810
+ # Duplicates (inferior params — cleaner alias exists)
811
+ "impact", # path/target order reversed vs get_impact_context; use get_impact_context
812
+ "cold_start", # duplicate of get_cold_start_context
813
+ "cache_freshness", # duplicate of check_freshness
814
+ "modernize", # duplicate of modernize_context
815
+ # Curated overrides — canonical CLI spec replaced by cleaner alias with same name.
816
+ # Listed here so validate_registry() skips CLI param-drift checks on the alias.
817
+ "spring_audit", # curated: repo_path + scope + min_severity only (strips output_path/format/copy)
818
+ "impact_chain", # curated: repo_path + symbol + depth + query_type with choices
819
+ # MCP self-management (an agent is not the MCP client admin)
820
+ "mcp_init",
821
+ "mcp_serve",
822
+ "mcp_status",
823
+ "mcp_remove",
824
+ "mcp_list_tools",
825
+ # Telemetry sub-commands (consolidated into telemetry(action=))
826
+ "telemetry_status",
827
+ "telemetry_enable",
828
+ "telemetry_disable",
829
+ })
830
+
831
+
832
+ def _java_spring_aliases() -> list[ToolSpec]:
833
+ """Curated MCP overrides for Java/Spring tools.
834
+
835
+ These replace the auto-generated canonical specs with cleaner param surfaces:
836
+ - repo_path (not raw `path`) for consistency with all other MCP tools
837
+ - MCP-irrelevant CLI flags (output_path, format, copy) stripped
838
+ - query_type choices documented so agents discover event topology
839
+ - Rich docstrings instead of contract-format stubs
840
+ """
841
+ validate_repo_path = _repo_path_validator()
842
+
843
+ _SPRING_AUDIT_DOC = """\
844
+ Spring semantic audit: TX anomalies + security surface. JAVA/SPRING ONLY.
845
+
846
+ Do NOT call on non-Java repositories — returns spring_detected=false with no findings.
847
+
848
+ Patterns detected:
849
+ TX-001: @Transactional missing rollbackFor for checked exceptions
850
+ TX-002: propagation=NEVER/NOT_SUPPORTED inside @Transactional scope
851
+ TX-003: readOnly=true method calling write operations
852
+ TX-004: REQUIRES_NEW nested inside REQUIRED (TX isolation breach risk)
853
+ TX-005: @Async method called within @Transactional context (TX context lost)
854
+ SEC-001: public endpoint with no security annotation (none_detected policy)
855
+ SEC-002: security annotation on non-endpoint method (misplaced)
856
+ SEC-003: missing auth on admin-pattern operations
857
+
858
+ Returns: schema_version, spring_detected, scope, summary, findings[], limitations, metadata.
859
+ findings fields: id, pattern_id, category, severity, confidence, title, symbol,
860
+ source_file, evidence, explanation, fix_hint.
861
+
862
+ repo_path: absolute path to the Java repository (default: current working directory).
863
+ scope: "all" (default) | "tx" (TX-001..005 only) | "security" (SEC-001..003 only)
864
+ min_severity: "low" (default) | "medium" | "high" | "critical"
865
+ """
866
+
867
+ _IMPACT_CHAIN_DOC = """\
868
+ Spring impact-chain: blast radius of a symbol with TX/SEC semantic enrichment. JAVA/SPRING ONLY.
869
+
870
+ Do NOT call on non-Java repositories — returns resolution=not_found.
871
+
872
+ Two query modes via query_type:
873
+ "impact" (default) — BFS call graph: direct_callers, indirect_callers, endpoints_affected,
874
+ transaction_boundary, security_surfaces, impact_findings (TX/SEC patterns in call chain).
875
+ "events" — event topology: publishers, consumers, propagation graph for an event class
876
+ or event publisher. Use when symbol is an event class (e.g. OrderPlacedEvent).
877
+
878
+ Returns: schema_version, symbol, resolution, direct_callers, indirect_callers,
879
+ endpoints_affected, transaction_boundary, security_surfaces, impact_findings,
880
+ analysis_warnings, risk_level, confidence, metadata.
881
+
882
+ symbol: FQN, class name, or Class#method.
883
+ Examples: "OrderService", "com.example.OrderService#placeOrder",
884
+ "OrderPlacedEvent" (with query_type="events" for event topology)
885
+ repo_path: absolute path to the Java repository (default: current working directory).
886
+ depth: BFS traversal depth 1–8 (default 4).
887
+ query_type: "impact" (default) | "events"
888
+ """
889
+
890
+ spring_audit = _alias_spec(
891
+ "spring_audit",
892
+ "Spring semantic audit: TX anomalies + security surface. JAVA/SPRING ONLY.",
893
+ ("spring-audit",),
894
+ (
895
+ ToolParamSpec("repo_path", "argument", str, required=False, default=".", is_path=True,
896
+ help="Absolute path to the Java repository."),
897
+ ToolParamSpec("scope", "option", str, required=False, default="all",
898
+ option_names=("--scope",), choices=("all", "tx", "security"),
899
+ help="all (default) | tx | security"),
900
+ ToolParamSpec("min_severity", "option", str, required=False, default="low",
901
+ option_names=("--min-severity",), choices=("low", "medium", "high", "critical"),
902
+ help="low (default) | medium | high | critical"),
903
+ ),
904
+ lambda inputs: [
905
+ "spring-audit",
906
+ str(inputs.get("repo_path", ".")),
907
+ "--scope", str(inputs.get("scope", "all")),
908
+ "--min-severity", str(inputs.get("min_severity", "low")),
909
+ ],
910
+ supported_targets=("repo_path",),
911
+ unsupported_targets=("file_path",),
912
+ validator=validate_repo_path,
913
+ docstring_override=_SPRING_AUDIT_DOC,
914
+ )
915
+
916
+ impact_chain = _alias_spec(
917
+ "impact_chain",
918
+ "Spring impact-chain: blast radius + TX/SEC enrichment. JAVA/SPRING ONLY.",
919
+ ("impact-chain",),
920
+ (
921
+ ToolParamSpec("symbol", "argument", str, required=True, default=None,
922
+ help="FQN, class name, or Class#method."),
923
+ ToolParamSpec("repo_path", "argument", str, required=False, default=".", is_path=True,
924
+ help="Absolute path to the Java repository."),
925
+ ToolParamSpec("depth", "option", int, required=False, default=4,
926
+ option_names=("--depth",), help="BFS depth 1–8 (default 4)."),
927
+ ToolParamSpec("query_type", "option", str, required=False, default="impact",
928
+ option_names=("--type",), choices=("impact", "events"),
929
+ help="impact (default) = call-chain blast radius; events = event topology"),
930
+ ),
931
+ lambda inputs: [
932
+ "impact-chain",
933
+ str(inputs["symbol"]),
934
+ str(inputs.get("repo_path", ".")),
935
+ "--depth", str(inputs.get("depth", 4)),
936
+ "--type", str(inputs.get("query_type", "impact")),
937
+ ],
938
+ supported_targets=("repo_path", "class_name"),
939
+ unsupported_targets=("file_path",),
940
+ validator=validate_repo_path,
941
+ docstring_override=_IMPACT_CHAIN_DOC,
942
+ )
943
+
944
+ return [spring_audit, impact_chain]
945
+
946
+
791
947
  @lru_cache(maxsize=1)
792
948
  def build_tool_specs() -> tuple[ToolSpec, ...]:
793
949
  """Build the full MCP registry from the live CLI runtime."""
794
- canonical = [
950
+ canonical_raw = [
795
951
  _canonical_spec_for_runtime_command(runtime)
796
952
  for runtime in discover_runtime_commands()
797
953
  if (runtime.callback is not None or runtime.path == ())
798
954
  and (not runtime.hidden or runtime.path == ("analyze",))
799
955
  ]
956
+ # Mark canonical tools that should not be served via MCP (validate_registry still checks them)
957
+ canonical = [
958
+ replace(spec, mcp_hidden=True) if spec.name in _MCP_HIDDEN_CANONICAL_TOOLS else spec
959
+ for spec in canonical_raw
960
+ ]
800
961
 
801
- aliases = _root_aliases() + _prepare_context_aliases()
962
+ aliases = _root_aliases() + _prepare_context_aliases() + _java_spring_aliases()
802
963
  internals = _internal_specs()
803
964
 
804
965
  merged: dict[str, ToolSpec] = {}
@@ -842,6 +1003,11 @@ def validate_registry() -> list[str]:
842
1003
  spec = registry_by_path.get(path)
843
1004
  if spec is None:
844
1005
  continue
1006
+ # Skip docstring/param drift for mcp_hidden tools and intentional curated overrides.
1007
+ # Curated aliases (e.g. spring_audit, impact_chain) replace canonical CLI specs with
1008
+ # cleaner MCP surfaces — their params diverge from the raw CLI by design.
1009
+ if spec.name in _MCP_HIDDEN_CANONICAL_TOOLS or spec.mcp_hidden:
1010
+ continue
845
1011
  expected_doc = _first_doc_line(runtime.docstring or runtime.help or "")
846
1012
  if expected_doc and expected_doc not in spec.description:
847
1013
  issues.append(f"docstring_mismatch:{spec.name}")
@@ -853,9 +1019,15 @@ def validate_registry() -> list[str]:
853
1019
  return issues
854
1020
 
855
1021
 
1022
+ @lru_cache(maxsize=1)
1023
+ def build_mcp_tool_specs() -> tuple[ToolSpec, ...]:
1024
+ """Tool specs actually served via MCP: public and not mcp_hidden."""
1025
+ return tuple(spec for spec in build_tool_specs() if spec.mcp_visible)
1026
+
1027
+
856
1028
  def mcp_tool_specs() -> tuple[ToolSpec, ...]:
857
- """Public tool specs only."""
858
- return build_public_tool_specs()
1029
+ """Tool specs served via MCP (public, not mcp_hidden)."""
1030
+ return build_mcp_tool_specs()
859
1031
 
860
1032
 
861
1033
  def mcp_internal_tool_specs() -> tuple[ToolSpec, ...]:
sourcecode/mcp/server.py CHANGED
@@ -1179,17 +1179,36 @@ def telemetry(action: str) -> dict:
1179
1179
  return _execute(["telemetry", action])
1180
1180
 
1181
1181
 
1182
+ _NATIVE_MCP_TOOLS: frozenset[str] = frozenset({
1183
+ "start_session",
1184
+ "analyze_task",
1185
+ "run_pr_review_flow",
1186
+ "run_bug_investigation_flow",
1187
+ "run_feature_flow",
1188
+ })
1189
+
1190
+
1182
1191
  def _finalize_mcp_registry() -> None:
1183
- """Replace manual tool registration with the runtime-generated registry."""
1184
- from sourcecode.mcp.registry import build_public_tool_specs, make_tool_callable, validate_registry
1192
+ """Sync the MCP server with the runtime-generated registry.
1193
+
1194
+ Removes every tool except the 5 native orchestration tools (start_session,
1195
+ analyze_task, flow runners) which are registered via @mcp.tool() in this
1196
+ module and have no backing CLI command. All other tools are rebuilt from the
1197
+ runtime-derived registry (build_mcp_tool_specs).
1198
+ """
1199
+ from sourcecode.mcp.registry import build_mcp_tool_specs, make_tool_callable, validate_registry
1200
+
1201
+ mcp_specs = build_mcp_tool_specs()
1185
1202
 
1203
+ # Remove everything except the native orchestration tools
1186
1204
  try:
1187
1205
  for tool in list(mcp._tool_manager.list_tools()): # type: ignore[attr-defined]
1188
- mcp.remove_tool(tool.name)
1206
+ if tool.name not in _NATIVE_MCP_TOOLS:
1207
+ mcp.remove_tool(tool.name)
1189
1208
  except Exception:
1190
1209
  pass
1191
1210
 
1192
- for spec in build_public_tool_specs():
1211
+ for spec in mcp_specs:
1193
1212
  tool_fn = make_tool_callable(spec)
1194
1213
  tool_fn.__doc__ = spec.docstring
1195
1214
  globals()[spec.name] = tool_fn
@@ -323,6 +323,11 @@ _SPRING_OTHER: frozenset[str] = frozenset({
323
323
 
324
324
  _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
325
325
 
326
+ # Two-step publish: SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)
327
+ # Used when event is created before passing to publishEvent (common pattern).
328
+ _PUBLISH_EVENT_CALL_RE = re.compile(r'\.publishEvent\s*\(')
329
+ _NEW_EVENT_INSTANTIATION_RE = re.compile(r'\bnew\s+(\w+Event)\s*[\({]')
330
+
326
331
  # Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
327
332
  _FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
328
333
 
@@ -1195,6 +1200,27 @@ def _build_relations(
1195
1200
  evidence={"type": "method_call", "value": f"publishEvent(new {event_simple})"},
1196
1201
  ))
1197
1202
 
1203
+ # Two-step publish: `SomeEvent var = new SomeEvent(...); publisher.publishEvent(var)`.
1204
+ # The inline regex above only catches publishEvent(new Evt(...)). Many Spring services
1205
+ # instantiate the event first and then pass the variable. When the source contains
1206
+ # publishEvent( (any form) we also scan for new XxxEvent instantiations that the inline
1207
+ # regex would have missed (i.e. not already emitted), using confidence=low.
1208
+ if _PUBLISH_EVENT_CALL_RE.search(_source_no_comments):
1209
+ inline_matched: set[str] = {m.group(1) for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments)}
1210
+ for m in _NEW_EVENT_INSTANTIATION_RE.finditer(_source_no_comments):
1211
+ event_simple = m.group(1)
1212
+ if event_simple in inline_matched:
1213
+ continue # already captured by inline regex at higher confidence
1214
+ event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
1215
+ for cls_sym in _class_syms:
1216
+ edges.append(RelationEdge(
1217
+ from_symbol=cls_sym.symbol,
1218
+ to_symbol=event_fqn,
1219
+ type="publishes_event",
1220
+ confidence="low",
1221
+ evidence={"type": "method_call", "value": f"publishEvent(var) + new {event_simple}"},
1222
+ ))
1223
+
1198
1224
  # Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
1199
1225
  for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
1200
1226
  event_simple = m.group(1)
@@ -244,22 +244,48 @@ def _bfs_callers(
244
244
  X#fieldName), the containing class X is also added to the caller set and BFS
245
245
  continues from X. This resolves the DI traversal gap where contained_in edges
246
246
  (which are skipped) were the only path from a field node back to its class.
247
+
248
+ BUG-004 fix: when BFS reaches a class-level node (no '#'), callers of that
249
+ class live on method-level keys (e.g. 'Foo#doWork') rather than the class key
250
+ ('Foo'). A class→method-key index is built upfront so the BFS also traverses
251
+ method-level entries when processing a class-level FQN.
247
252
  """
248
253
  visited: set[str] = set(seed_fqns)
249
254
  direct: list[str] = []
250
255
  indirect: list[str] = []
251
256
  was_truncated = False
252
257
 
253
- # Hub-class guard: if seeds have > _BFS_CALLER_CAP direct callers combined,
254
- # cap effective depth to 1 to avoid O(n^depth) explosion.
255
- total_direct_count = 0
258
+ # BUG-004: index class FQN list of method-level keys in reverse_graph.
259
+ # Callers of Foo#doWork are stored under reverse_graph["Foo#doWork"], never
260
+ # under reverse_graph["Foo"]. Without this index, BFS silently terminates
261
+ # whenever a class-level node is enqueued (e.g. via CH-002 expansion).
262
+ class_method_index: dict[str, list[str]] = {}
263
+ for rg_key in reverse_graph:
264
+ if "#" in rg_key:
265
+ cls = rg_key.split("#")[0]
266
+ class_method_index.setdefault(cls, []).append(rg_key)
267
+
268
+ def _edges_for(fqn: str) -> list[tuple[str, list[str]]]:
269
+ """Return all (etype, fqn_list) pairs from reverse_graph for fqn.
270
+ For class-level FQNs also includes method-level entries (BUG-004)."""
271
+ edges: list[tuple[str, list[str]]] = list((reverse_graph.get(fqn) or {}).items())
272
+ if "#" not in fqn:
273
+ for mk in class_method_index.get(fqn, []):
274
+ edges.extend((reverse_graph.get(mk) or {}).items())
275
+ return edges
276
+
277
+ # Hub-class guard: cap depth to 1 when the UNIQUE direct caller set exceeds
278
+ # _BFS_CALLER_CAP, to avoid O(n^depth) BFS explosion on high-fanout seeds.
279
+ # Uses unique callers (not raw sum per seed) so that interface-expansion seeds
280
+ # (which add many method-level FQNs sharing the same callers) don't trigger the
281
+ # guard prematurely — a 36-method interface still has ~20 unique calling classes.
282
+ unique_direct_callers: set[str] = set()
256
283
  for seed in seed_fqns:
257
- entry = reverse_graph.get(seed) or {}
258
- for etype, fqn_list in entry.items():
284
+ for etype, fqn_list in _edges_for(seed):
259
285
  if etype not in _SKIP_EDGE_TYPES:
260
- total_direct_count += len(fqn_list)
286
+ unique_direct_callers.update(fqn_list)
261
287
 
262
- effective_depth = 1 if total_direct_count > _BFS_CALLER_CAP else max_depth
288
+ effective_depth = 1 if len(unique_direct_callers) > _BFS_CALLER_CAP else max_depth
263
289
  if effective_depth < max_depth:
264
290
  was_truncated = True
265
291
 
@@ -281,8 +307,7 @@ def _bfs_callers(
281
307
  fqn, depth = queue.pop(0)
282
308
  if depth >= effective_depth:
283
309
  continue
284
- entry = reverse_graph.get(fqn) or {}
285
- for etype, fqn_list in entry.items():
310
+ for etype, fqn_list in _edges_for(fqn):
286
311
  if etype in _SKIP_EDGE_TYPES:
287
312
  continue
288
313
  for caller in fqn_list:
@@ -52,15 +52,16 @@ _WRITE_METHOD_RE = re.compile(
52
52
  re.IGNORECASE,
53
53
  )
54
54
 
55
- # Exception swallowing pattern for TX-005:
56
- # catch block that contains a log/print but no throw/rethrow
57
- _CATCH_SWALLOW_RE = re.compile(
58
- r'catch\s*\([^)]+\)\s*\{[^}]*' # catch(...) {
59
- r'(?:log|logger|LOG|System\.out|e\.print)' # logging call
60
- r'[^}]*\}', # closing brace (no throw inside)
61
- re.DOTALL,
62
- )
55
+ # TX-005 catch-block detection helpers.
56
+ # _CATCH_SWALLOW_RE is retained for reference but replaced by brace-counting
57
+ # extraction in _has_swallowed_exception to avoid false positives from nested
58
+ # braces (nested if/try blocks inside catch terminating the match prematurely).
59
+ _CATCH_HEADER_RE = re.compile(r'\bcatch\s*\([^)]+\)\s*\{')
60
+ _LOG_IN_CATCH_RE = re.compile(r'\b(?:log|logger|LOG|System\.out|e\.print)\b')
63
61
  _RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
62
+ # Non-trivial return (method call) inside a catch block indicates recovery, not
63
+ # silent swallowing — e.g. `return findNextId(idType)` after creating a missing row.
64
+ _RECOVERY_RETURN_RE = re.compile(r'\breturn\s+\w[\w.<>]*\s*\(')
64
65
 
65
66
 
66
67
  def _extract_method_body(source: str, method_name: str) -> str:
@@ -507,6 +508,10 @@ class _TX005ExceptionSwallowing:
507
508
  continue
508
509
  if not boundary.source_file:
509
510
  continue
511
+ # readOnly=true transactions do not write data — swallowed exceptions
512
+ # cannot cause dirty commits, so TX-005 is not applicable.
513
+ if boundary.read_only:
514
+ continue
510
515
 
511
516
  abs_path = root / boundary.source_file
512
517
  try:
@@ -555,21 +560,76 @@ class _TX005ExceptionSwallowing:
555
560
  return findings
556
561
 
557
562
  def _has_swallowed_exception(self, source: str, symbol: str) -> bool:
558
- """Return True if the specific method body has a catch-log-no-rethrow pattern.
563
+ """Return True if a @Transactional overload of method_name has a catch-log-no-rethrow pattern.
559
564
 
560
- Scopes the search to the method body only (not the whole file) to avoid
561
- false positives when other methods in the same file have swallowed exceptions.
565
+ Guards against two false-positive sources:
566
+ 1. Overloaded methods only checks the overload whose declaration is immediately
567
+ preceded by @Transactional (preamble anchored to last '}').
568
+ 2. Call sites — skips any match where ';' appears before the next '{', which
569
+ is true for call expressions (e.g. dao.method(arg);) but not definitions.
562
570
  """
563
571
  method_name = symbol.split("#")[-1] if "#" in symbol else symbol.rsplit(".", 1)[-1]
564
- body = _extract_method_body(source, method_name)
565
- if not body:
566
- return False
567
- for match in _CATCH_SWALLOW_RE.finditer(body):
568
- block = match.group(0)
569
- if not _RETHROW_IN_CATCH_RE.search(block):
572
+ pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
573
+ for m in pattern.finditer(source):
574
+ # Preamble: text between the previous closing brace and this method name.
575
+ # Anchoring to the last '}' prevents @Transactional from a prior method
576
+ # from leaking into this overload's preamble.
577
+ prev_brace = source.rfind('}', 0, m.start())
578
+ preamble_start = prev_brace + 1 if prev_brace >= 0 else 0
579
+ preamble = source[preamble_start:m.start()]
580
+ if '@Transactional' not in preamble:
581
+ continue
582
+ # Guard: if ';' comes before '{', this is a call site, not a definition.
583
+ brace_pos = source.find('{', m.end())
584
+ semi_pos = source.find(';', m.end())
585
+ if brace_pos < 0:
586
+ continue
587
+ if semi_pos >= 0 and semi_pos < brace_pos:
588
+ continue
589
+ # Extract this overload's body
590
+ depth = 1
591
+ i = brace_pos + 1
592
+ while i < len(source) and depth > 0:
593
+ c = source[i]
594
+ if c == '{':
595
+ depth += 1
596
+ elif c == '}':
597
+ depth -= 1
598
+ i += 1
599
+ body = source[brace_pos:i]
600
+ for catch_block in self._extract_catch_blocks(body):
601
+ if not _LOG_IN_CATCH_RE.search(catch_block):
602
+ continue
603
+ if _RETHROW_IN_CATCH_RE.search(catch_block):
604
+ continue
605
+ # Non-trivial return (method call) indicates recovery, not swallowing.
606
+ if _RECOVERY_RETURN_RE.search(catch_block):
607
+ continue
570
608
  return True
571
609
  return False
572
610
 
611
+ @staticmethod
612
+ def _extract_catch_blocks(body: str) -> list[str]:
613
+ """Extract full catch block bodies from a method body using brace counting.
614
+
615
+ Handles nested braces correctly — unlike a simple [^}]* regex which
616
+ terminates at the first nested '}' inside the catch block.
617
+ """
618
+ blocks: list[str] = []
619
+ for m in _CATCH_HEADER_RE.finditer(body):
620
+ brace_pos = m.end() - 1
621
+ depth = 1
622
+ i = brace_pos + 1
623
+ while i < len(body) and depth > 0:
624
+ c = body[i]
625
+ if c == '{':
626
+ depth += 1
627
+ elif c == '}':
628
+ depth -= 1
629
+ i += 1
630
+ blocks.append(body[brace_pos:i])
631
+ return blocks
632
+
573
633
 
574
634
  # ---------------------------------------------------------------------------
575
635
  # Default pattern registry
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.5
3
+ Version: 1.35.7
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=QCdv6kJhqRAzIEK9ewWpwfq2rcWG3YpzJnjAJDMG_pc,103
1
+ sourcecode/__init__.py,sha256=YB4S9jo7LKaihLW3yz-L5Iz_CCvBXd4y36fMz4uaCAM,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
@@ -36,7 +36,7 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
36
36
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
37
37
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
38
38
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
39
- sourcecode/repository_ir.py,sha256=uLIewoLBg4nknI1JlI8bPo_kl9SVyT-GFQqENTeFz1M,167673
39
+ sourcecode/repository_ir.py,sha256=SOACZ4viYG8tpmL_-l4XqoX6bN4GmCeX5ZIr9tnuQyA,169298
40
40
  sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
41
41
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
42
42
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
@@ -45,11 +45,11 @@ sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGp
45
45
  sourcecode/serializer.py,sha256=ooNZW2_fqx__BXII25eAWq-BomodvqQ6opUT_niQYCA,123403
46
46
  sourcecode/spring_event_topology.py,sha256=LvGv5RXtU_O-fVB_OO9eDD2UmZM72Jn2oUHgOo50Qm0,17157
47
47
  sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
48
- sourcecode/spring_impact.py,sha256=-ET4oB9tZQYfcyhfI781mMJCU-przz6x4Ejwr3hejA8,31743
48
+ sourcecode/spring_impact.py,sha256=vb_cOk9dFU7YcGPeqivFcpS4OYSETT72oJYn4WC5al0,33266
49
49
  sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
50
50
  sourcecode/spring_security_audit.py,sha256=RPr491FGAmWNxqe-uN0FS2gmjQ0M5ryRyXjLNMMzKFk,20077
51
51
  sourcecode/spring_semantic.py,sha256=CiAf77p48-RFrUF0zbgww4w2Xigrbo1t5M3ZCDIfV_g,12032
52
- sourcecode/spring_tx_analyzer.py,sha256=eBcYLRKhlUllHl195CVGNWeg2vMext4u1Tezu_Mwrdg,27143
52
+ sourcecode/spring_tx_analyzer.py,sha256=ZI_sG0oQi_iQAbm0hwIzjwGPMV2oS0RSsz2dGYCz__k,30103
53
53
  sourcecode/summarizer.py,sha256=YspHEVeYJVmltq0FMtGZF8kIP3qiR2KLcanGL6Y7uTI,20747
54
54
  sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
55
55
  sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
@@ -76,9 +76,9 @@ sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG6
76
76
  sourcecode/detectors/tooling.py,sha256=8CKbtxwQoABP-WyBRNmdAmHDOvAH57AR1cF4UKuWEdQ,2074
77
77
  sourcecode/mcp/__init__.py,sha256=XU4HfRGbdid8wdUA0x_4f7uKZD1z3mv_XUY_WU_T9Mw,179
78
78
  sourcecode/mcp/orchestrator.py,sha256=BMi1D6liJHI3DXiaC8yeBLLP0wXajpCP3-vnRGqrvnw,26850
79
- sourcecode/mcp/registry.py,sha256=9SdXkwfc89_dkYFNeSbIgO5KDSMoI5sFoIBRX2PLt14,33248
79
+ sourcecode/mcp/registry.py,sha256=MPMhxI7nx8HC48Zo2IXfOexKWW1e6J9Ex1OjW8lGCcg,41819
80
80
  sourcecode/mcp/runner.py,sha256=-Dp2qPGRkfNTVen6bKh7WtzQqpcEtsrXoiuajvshlKk,2866
81
- sourcecode/mcp/server.py,sha256=hNnXB7UcNyYDCqyu47cHsb3vsULaqgFSTQkBj6C-UcA,51611
81
+ sourcecode/mcp/server.py,sha256=lBSQCw3yFe8rZHp2GGVcfua0EJUYZmsIUbvA4GIJv9s,52210
82
82
  sourcecode/mcp/onboarding/__init__.py,sha256=sj2PWqEBmMc4zBNkomg89WtL0M6S7A9yb7_wAuSWNP4,66
83
83
  sourcecode/mcp/onboarding/applier.py,sha256=B9CneieWTpaDSDIyW3S5nrlRlBpvfqUcgi93-mm_ApQ,2135
84
84
  sourcecode/mcp/onboarding/backup.py,sha256=ihqGOR8QTX8HASRSEDyfFyXr5bkXrygPHamv4p9KTmk,1452
@@ -90,8 +90,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
90
90
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
91
91
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
92
92
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
93
- sourcecode-1.35.5.dist-info/METADATA,sha256=ZrHgEgvVfhprWb98X8P4rB5PUJZ7Av4uDsrHfzVgovU,21263
94
- sourcecode-1.35.5.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
95
- sourcecode-1.35.5.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
96
- sourcecode-1.35.5.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
97
- sourcecode-1.35.5.dist-info/RECORD,,
93
+ sourcecode-1.35.7.dist-info/METADATA,sha256=nMUv3kkN_jZJ-dII8704TjO4B_m3en9qjH3D4qjEuJk,21263
94
+ sourcecode-1.35.7.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
95
+ sourcecode-1.35.7.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
96
+ sourcecode-1.35.7.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
97
+ sourcecode-1.35.7.dist-info/RECORD,,