sourcecode 1.35.2__py3-none-any.whl → 1.35.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
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Deterministic codebase context maps for AI coding agents."""
2
2
 
3
- __version__ = "1.35.2"
3
+ __version__ = "1.35.3"
sourcecode/cli.py CHANGED
@@ -3917,6 +3917,11 @@ def impact_chain_cmd(
3917
3917
  False, "--copy", "-c",
3918
3918
  help="Copy output to clipboard after a successful run.",
3919
3919
  ),
3920
+ query_type: str = typer.Option(
3921
+ "impact", "--type", "-t",
3922
+ help="Query type: impact (default) or events.",
3923
+ show_default=True,
3924
+ ),
3920
3925
  ) -> None:
3921
3926
  """Spring impact-chain: systemic blast radius of a symbol with TX/SEC enrichment.
3922
3927
 
@@ -3930,6 +3935,14 @@ def impact_chain_cmd(
3930
3935
  - impact_findings — TX/SEC audit findings touching the call chain
3931
3936
  - risk_level — critical | high | medium | low
3932
3937
 
3938
+ \b
3939
+ With --type events, returns event topology:
3940
+ - publishers — FQNs that publish the event class
3941
+ - consumers — listeners with TX phase metadata
3942
+ - event_graph — publisher → event → consumer edges (BFS ≤ 2)
3943
+ - transaction_context — AFTER_COMMIT consumers, BEFORE_COMMIT risks
3944
+ - risk_level — high | medium | low
3945
+
3933
3946
  \b
3934
3947
  Consumes SpringSemanticModel — zero duplicate CIR traversals.
3935
3948
  JAVA/SPRING ONLY.
@@ -3948,6 +3961,19 @@ def impact_chain_cmd(
3948
3961
  from sourcecode.spring_impact import run_impact_chain
3949
3962
  from sourcecode.spring_findings import SpringAuditResult
3950
3963
 
3964
+ _VALID_TYPES = ("impact", "events")
3965
+ if query_type not in _VALID_TYPES:
3966
+ _emit_error_json(
3967
+ INVALID_INPUT_CODE,
3968
+ f"Invalid --type '{query_type}'. Valid values: {', '.join(_VALID_TYPES)}",
3969
+ flag="--type",
3970
+ value=query_type,
3971
+ valid_values=list(_VALID_TYPES),
3972
+ hint="Use --type impact (default) or --type events.",
3973
+ expected="impact | events",
3974
+ )
3975
+ raise typer.Exit(code=1)
3976
+
3951
3977
  target = path.resolve()
3952
3978
  if not target.exists() or not target.is_dir():
3953
3979
  _emit_error_json(
@@ -3970,7 +3996,7 @@ def impact_chain_cmd(
3970
3996
 
3971
3997
  file_list = find_java_files(target)
3972
3998
  if not file_list:
3973
- data = {
3999
+ data: dict = {
3974
4000
  "schema_version": "1.0",
3975
4001
  "symbol": symbol,
3976
4002
  "resolution": "not_found",
@@ -3991,6 +4017,30 @@ def impact_chain_cmd(
3991
4017
 
3992
4018
  cir = build_canonical_ir(file_list, target)
3993
4019
  _model = SpringSemanticModel.build(cir)
4020
+
4021
+ if query_type == "events":
4022
+ from sourcecode.spring_event_topology import run_event_topology
4023
+ evt_result = run_event_topology(cir, symbol, model=_model)
4024
+ data = evt_result.to_dict()
4025
+ output = _serialize_dict(data, format)
4026
+ if output_path is not None:
4027
+ output_path.write_text(output, encoding="utf-8")
4028
+ typer.echo(
4029
+ f"Event topology written to {output_path} "
4030
+ f"(risk: {evt_result.risk_level}, "
4031
+ f"{evt_result.metadata.get('publisher_count', 0)} publishers, "
4032
+ f"{evt_result.metadata.get('consumer_count', 0)} consumers)",
4033
+ err=True,
4034
+ )
4035
+ else:
4036
+ sys.stdout.buffer.write(output.encode("utf-8"))
4037
+ sys.stdout.buffer.write(b"\n")
4038
+ sys.stdout.buffer.flush()
4039
+ if copy:
4040
+ if _copy_to_clipboard(output):
4041
+ typer.echo("✓ copied to clipboard", err=True)
4042
+ return
4043
+
3994
4044
  result = run_impact_chain(cir, symbol, depth=depth, root=target, model=_model)
3995
4045
 
3996
4046
  data = result.to_dict()
@@ -239,7 +239,7 @@ _LOMBOK_CTOR_ANNOTATIONS: frozenset[str] = frozenset({
239
239
  })
240
240
 
241
241
  # Transaction annotations whose args must be captured for semantic analysis.
242
- _TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional"})
242
+ _TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional", "@TransactionalEventListener"})
243
243
 
244
244
  # Combined set used in _extract_symbols annotation-value capture.
245
245
  _CAPTURE_ANN_ARGS: frozenset[str] = (
@@ -307,7 +307,9 @@ _SPRING_OTHER: frozenset[str] = frozenset({
307
307
  "@PutMapping", "@DeleteMapping", "@PatchMapping", "@Autowired",
308
308
  "@Inject", "@Value", "@Qualifier", "@EnableWebSecurity",
309
309
  "@SpringBootApplication", "@EnableAutoConfiguration",
310
- "@EventListener", "@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
310
+ "@EventListener", "@TransactionalEventListener",
311
+ "@KafkaListener", "@RabbitListener",
312
+ "@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
311
313
  # CDI / Jakarta EE
312
314
  "@ApplicationScoped", "@RequestScoped", "@SessionScoped", "@Dependent",
313
315
  "@Named", "@Produces", "@Consumes",
@@ -1101,16 +1103,21 @@ def _build_relations(
1101
1103
  ))
1102
1104
 
1103
1105
  # Event flow edges — listens_to_event and publishes_event.
1104
- # Spring: method with @EventListener → resolved event parameter type(s).
1106
+ # Spring: method with @EventListener or @TransactionalEventListener → resolved event type(s).
1107
+ _LISTENER_ANNOTATIONS: frozenset[str] = frozenset({
1108
+ "@EventListener", "@TransactionalEventListener",
1109
+ })
1105
1110
  for sym in symbols:
1106
- if sym.type == "method" and "@EventListener" in sym.annotations:
1111
+ if sym.type == "method" and (sym.annotations and
1112
+ any(a in _LISTENER_ANNOTATIONS for a in sym.annotations)):
1113
+ ann = next(a for a in sym.annotations if a in _LISTENER_ANNOTATIONS)
1107
1114
  for imp_fqn in sym.imports_used:
1108
1115
  edges.append(RelationEdge(
1109
1116
  from_symbol=sym.symbol,
1110
1117
  to_symbol=imp_fqn,
1111
1118
  type="listens_to_event",
1112
1119
  confidence="high",
1113
- evidence={"type": "annotation", "value": "@EventListener"},
1120
+ evidence={"type": "annotation", "value": ann},
1114
1121
  ))
1115
1122
 
1116
1123
  _class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
@@ -0,0 +1,427 @@
1
+ """spring_event_topology.py — Event Topology: event_class → publishers, consumers, propagation graph.
2
+
3
+ Pipeline:
4
+ 1. Resolve event symbol from CIR
5
+ 2. Find publishers via model.event_graph
6
+ 3. Find Spring consumers via model.event_graph (EventListener + TransactionalEventListener)
7
+ 4. Enrich consumers with TX phase from raw IR annotation_values
8
+ 5. Build event propagation graph (BFS depth ≤ 2)
9
+ 6. Attach TX semantics: AFTER_COMMIT consumers, BEFORE_COMMIT risks
10
+ 7. Compute risk level + confidence
11
+
12
+ Hard constraints:
13
+ - NO guessing / LLM inference
14
+ - ONLY CIR + AST-derived annotation data
15
+ - Deterministic: identical CIR → identical result
16
+ - Reuses model.event_graph + model.tx_index — no duplicate CIR traversal
17
+
18
+ Usage:
19
+ model = SpringSemanticModel.build(cir)
20
+ result = run_event_topology(cir, "com.example.OrderCreatedEvent", model=model)
21
+ output = json.dumps(result.to_dict(), indent=2)
22
+ """
23
+ from __future__ import annotations
24
+
25
+ import re
26
+ import time
27
+ from dataclasses import dataclass, field
28
+ from typing import TYPE_CHECKING, Optional
29
+
30
+ if TYPE_CHECKING:
31
+ from sourcecode.canonical_ir import CanonicalRepositoryIR
32
+ from sourcecode.spring_model import SpringSemanticModel
33
+
34
+ # ---------------------------------------------------------------------------
35
+ # Constants
36
+ # ---------------------------------------------------------------------------
37
+
38
+ _SCHEMA_VERSION = "1.0"
39
+
40
+ # Phase annotation value parser: phase=TransactionPhase.AFTER_COMMIT
41
+ _TX_PHASE_RE = re.compile(r'phase\s*=\s*(?:TransactionPhase\.)?(\w+)')
42
+
43
+ # TransactionalEventListener default phase (Spring default)
44
+ _DEFAULT_TX_PHASE = "AFTER_COMMIT"
45
+
46
+ _RISK_FANOUT_HIGH = 5
47
+ _RISK_FANOUT_MEDIUM = 2
48
+
49
+
50
+ # ---------------------------------------------------------------------------
51
+ # Data model
52
+ # ---------------------------------------------------------------------------
53
+
54
+ @dataclass
55
+ class EventConsumer:
56
+ """Single consumer of a Spring application event."""
57
+ fqn: str
58
+ type: str # "spring_event" | "transactional"
59
+ transactional_phase: str # "" | "BEFORE_COMMIT" | "AFTER_COMMIT" | "AFTER_ROLLBACK" | "AFTER_COMPLETION"
60
+ source_file: str
61
+
62
+ def to_dict(self) -> dict:
63
+ d: dict = {
64
+ "fqn": self.fqn,
65
+ "type": self.type,
66
+ "source_file": self.source_file,
67
+ }
68
+ if self.transactional_phase:
69
+ d["transactional_phase"] = self.transactional_phase
70
+ return d
71
+
72
+
73
+ @dataclass
74
+ class EventTopologyResult:
75
+ """Output contract for event topology query.
76
+
77
+ Stable contract — do not remove or rename fields.
78
+ """
79
+ schema_version: str = _SCHEMA_VERSION
80
+ event_class: str = ""
81
+ resolution: str = "not_found" # "exact" | "class_expanded" | "partial" | "not_found"
82
+ publishers: list[str] = field(default_factory=list)
83
+ consumers: list[dict] = field(default_factory=list)
84
+ event_graph: dict = field(default_factory=dict)
85
+ transaction_context: dict = field(default_factory=dict)
86
+ risk_level: str = "low"
87
+ confidence: str = "high"
88
+ limitations: list[str] = field(default_factory=list)
89
+ metadata: dict = field(default_factory=dict)
90
+
91
+ def to_dict(self) -> dict:
92
+ return {
93
+ "schema_version": self.schema_version,
94
+ "event_class": self.event_class,
95
+ "resolution": self.resolution,
96
+ "publishers": self.publishers,
97
+ "consumers": self.consumers,
98
+ "event_graph": self.event_graph,
99
+ "transaction_context": self.transaction_context,
100
+ "risk_level": self.risk_level,
101
+ "confidence": self.confidence,
102
+ "limitations": self.limitations,
103
+ "metadata": self.metadata,
104
+ }
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Internal helpers
109
+ # ---------------------------------------------------------------------------
110
+
111
+ def _class_of(fqn: str) -> str:
112
+ if "#" in fqn:
113
+ return fqn.split("#")[0]
114
+ return fqn
115
+
116
+
117
+ def _build_fqn_index(cir: "CanonicalRepositoryIR") -> dict[str, dict]:
118
+ """Build FQN → raw IR node dict for fast annotation lookup.
119
+
120
+ Reads once from cir._raw_ir. Returns {} if raw IR not available.
121
+ """
122
+ raw = getattr(cir, "_raw_ir", {}) or {}
123
+ nodes = (raw.get("graph") or {}).get("nodes") or []
124
+ return {n["fqn"]: n for n in nodes if "fqn" in n}
125
+
126
+
127
+ def _extract_tx_phase(annotation_values: dict, annotations: list) -> str:
128
+ """Extract @TransactionalEventListener phase from annotation_values.
129
+
130
+ Returns empty string for plain @EventListener (no TX constraint).
131
+ Returns _DEFAULT_TX_PHASE when annotation present but phase not specified.
132
+ """
133
+ if "@TransactionalEventListener" not in annotations:
134
+ return ""
135
+ raw_args = annotation_values.get("@TransactionalEventListener", "")
136
+ if not raw_args:
137
+ return _DEFAULT_TX_PHASE
138
+ m = _TX_PHASE_RE.search(raw_args)
139
+ if m:
140
+ return m.group(1).upper()
141
+ return _DEFAULT_TX_PHASE
142
+
143
+
144
+ def _resolve_event_symbol(
145
+ event_class: str,
146
+ cir: "CanonicalRepositoryIR",
147
+ ) -> tuple[str, str, list[str]]:
148
+ """Resolve event_class to CIR FQN.
149
+
150
+ Returns (resolved_fqn, resolution, warnings).
151
+ resolution: "exact" | "class_expanded" | "not_found"
152
+ """
153
+ symbols = cir.symbols
154
+ warnings: list[str] = []
155
+
156
+ # 1. Exact FQN match
157
+ if event_class in symbols:
158
+ return event_class, "exact", []
159
+
160
+ # 2. Class-part suffix match (handles simple names like "OrderCreatedEvent")
161
+ candidates = [
162
+ s for s in symbols
163
+ if _class_of(s) == event_class or _class_of(s).endswith("." + event_class)
164
+ ]
165
+ unique_classes = {_class_of(s) for s in candidates}
166
+ if len(unique_classes) == 1:
167
+ return next(iter(unique_classes)), "class_expanded", []
168
+ if len(unique_classes) > 1:
169
+ # Multiple matches — take first alphabetically, warn
170
+ chosen = sorted(unique_classes)[0]
171
+ warnings.append(
172
+ f"Ambiguous event class '{event_class}': matched {sorted(unique_classes)}. "
173
+ f"Using '{chosen}'."
174
+ )
175
+ return chosen, "partial", warnings
176
+
177
+ return "", "not_found", [f"Event class '{event_class}' not found in CIR."]
178
+
179
+
180
+ def _find_kafka_rabbit_counts(
181
+ fqn_index: dict[str, dict],
182
+ ) -> tuple[int, int]:
183
+ """Count @KafkaListener and @RabbitListener methods in raw IR."""
184
+ kafka_count = 0
185
+ rabbit_count = 0
186
+ for node in fqn_index.values():
187
+ anns = node.get("annotations") or []
188
+ if "@KafkaListener" in anns:
189
+ kafka_count += 1
190
+ if "@RabbitListener" in anns:
191
+ rabbit_count += 1
192
+ return kafka_count, rabbit_count
193
+
194
+
195
+ def _compute_event_risk(
196
+ publisher_count: int,
197
+ consumer_count: int,
198
+ before_commit_count: int,
199
+ cross_module: bool,
200
+ ) -> str:
201
+ """Deterministic risk scoring per spec.
202
+
203
+ high: fanout > 5 OR cross-module propagation OR BEFORE_COMMIT consumers
204
+ medium: 2–5 consumers
205
+ low: ≤1 consumer
206
+ """
207
+ if consumer_count > _RISK_FANOUT_HIGH or cross_module or before_commit_count > 0:
208
+ return "high"
209
+ if consumer_count >= _RISK_FANOUT_MEDIUM:
210
+ return "medium"
211
+ return "low"
212
+
213
+
214
+ # ---------------------------------------------------------------------------
215
+ # Orchestrator
216
+ # ---------------------------------------------------------------------------
217
+
218
+ class EventTopologyOrchestrator:
219
+ """Stateless query engine: event_class → EventTopologyResult.
220
+
221
+ Consumes pre-built CIR + SpringSemanticModel. Never re-derives data
222
+ already present in the model.
223
+ """
224
+
225
+ def query(
226
+ self,
227
+ cir: "CanonicalRepositoryIR",
228
+ model: "SpringSemanticModel",
229
+ event_class: str,
230
+ ) -> EventTopologyResult:
231
+ """Execute event topology query.
232
+
233
+ Args:
234
+ cir: CanonicalRepositoryIR from build_canonical_ir().
235
+ model: Pre-built SpringSemanticModel.
236
+ event_class: Event class FQN or simple name.
237
+ """
238
+ t0 = time.monotonic()
239
+ warnings: list[str] = []
240
+ limitations: list[str] = [
241
+ "Only Spring annotations detected (@EventListener, @TransactionalEventListener).",
242
+ "No runtime dynamic event routing — static analysis only.",
243
+ ]
244
+
245
+ # ── 1. Resolve event symbol ────────────────────────────────────────
246
+ resolved_fqn, resolution, sym_warnings = _resolve_event_symbol(event_class, cir)
247
+ warnings.extend(sym_warnings)
248
+
249
+ if resolution == "not_found" or not resolved_fqn:
250
+ elapsed = round((time.monotonic() - t0) * 1000, 2)
251
+ return EventTopologyResult(
252
+ event_class=event_class,
253
+ resolution="not_found",
254
+ limitations=limitations + [f"Event class '{event_class}' not found in CIR."],
255
+ confidence="low",
256
+ metadata={"query_time_ms": elapsed},
257
+ )
258
+
259
+ # ── 2. Build FQN index for annotation lookups ──────────────────────
260
+ fqn_index = _build_fqn_index(cir)
261
+
262
+ # ── 3. Find publishers ─────────────────────────────────────────────
263
+ publishers = sorted(model.event_graph.publishers_of(resolved_fqn))
264
+
265
+ # ── 4. Find Spring consumers, enrich with TX phase ─────────────────
266
+ raw_consumer_fqns = model.event_graph.listeners_of(resolved_fqn)
267
+ consumers: list[EventConsumer] = []
268
+ for fqn in sorted(raw_consumer_fqns):
269
+ node = fqn_index.get(fqn) or {}
270
+ anns = node.get("annotations") or []
271
+ ann_vals = node.get("annotation_values") or {}
272
+ source_file = node.get("source_file") or ""
273
+ tx_phase = _extract_tx_phase(ann_vals, anns)
274
+ consumer_type = "transactional" if tx_phase else "spring_event"
275
+ consumers.append(EventConsumer(
276
+ fqn=fqn,
277
+ type=consumer_type,
278
+ transactional_phase=tx_phase,
279
+ source_file=source_file,
280
+ ))
281
+
282
+ # ── 5. Build event propagation graph (BFS ≤ 2) ────────────────────
283
+ graph_edges: list[dict] = []
284
+
285
+ # Level 0 → 1: publisher → event_class, event_class → consumer
286
+ for pub in publishers:
287
+ graph_edges.append({"from": pub, "to": resolved_fqn, "type": "publishes"})
288
+ for c in consumers:
289
+ graph_edges.append({"from": resolved_fqn, "to": c.fqn, "type": "consumes"})
290
+
291
+ # Level 1 → 2: consumers that also publish other events
292
+ level2_events: dict[str, list[str]] = {} # secondary_event → [l2_consumers]
293
+ for c in consumers:
294
+ # Check if this consumer FQN also publishes events
295
+ # The event_graph publishers dict is indexed by event_type → [publisher_fqns]
296
+ for evt_type, pub_fqns in model.event_graph.publishers.items():
297
+ if evt_type == resolved_fqn:
298
+ continue # same event, skip
299
+ consumer_class = _class_of(c.fqn)
300
+ matching = [p for p in pub_fqns
301
+ if p == c.fqn or _class_of(p) == consumer_class]
302
+ if matching:
303
+ graph_edges.append({
304
+ "from": c.fqn,
305
+ "to": evt_type,
306
+ "type": "re_publishes",
307
+ })
308
+ l2_listeners = model.event_graph.listeners_of(evt_type)
309
+ level2_events[evt_type] = sorted(l2_listeners)
310
+ for l2 in sorted(l2_listeners):
311
+ graph_edges.append({
312
+ "from": evt_type,
313
+ "to": l2,
314
+ "type": "consumes",
315
+ })
316
+
317
+ # ── 6. Kafka/Rabbit counts (metadata only — not linkable to event_class) ──
318
+ kafka_count, rabbit_count = _find_kafka_rabbit_counts(fqn_index)
319
+ if kafka_count > 0 or rabbit_count > 0:
320
+ limitations.append(
321
+ f"Kafka/RabbitMQ consumer binding not supported in v1 — "
322
+ f"{kafka_count} @KafkaListener and {rabbit_count} @RabbitListener "
323
+ f"method(s) found in repo; cannot link to event class without "
324
+ f"explicit topic-to-event mapping."
325
+ )
326
+
327
+ # ── 7. TX context ──────────────────────────────────────────────────
328
+ after_commit = [c.fqn for c in consumers if c.transactional_phase == "AFTER_COMMIT"]
329
+ before_commit_risks = [c.fqn for c in consumers if c.transactional_phase == "BEFORE_COMMIT"]
330
+ tx_context = {
331
+ "after_commit_consumers": after_commit,
332
+ "before_commit_risks": before_commit_risks,
333
+ }
334
+
335
+ # ── 8. Cross-module detection ──────────────────────────────────────
336
+ # Simple heuristic: publisher and consumer in different top-level packages
337
+ pub_packages = {_class_of(p).rsplit(".", 2)[0] for p in publishers if "." in _class_of(p)}
338
+ con_packages = {_class_of(c.fqn).rsplit(".", 2)[0] for c in consumers if "." in _class_of(c.fqn)}
339
+ cross_module = bool(pub_packages and con_packages and not pub_packages.isdisjoint(con_packages) is False)
340
+ # More precise: cross-module if top-2-segment packages differ
341
+ def _top2(fqn: str) -> str:
342
+ parts = _class_of(fqn).split(".")
343
+ return ".".join(parts[:2]) if len(parts) >= 2 else fqn
344
+
345
+ pub_top2 = {_top2(p) for p in publishers}
346
+ con_top2 = {_top2(c.fqn) for c in consumers}
347
+ cross_module = bool(pub_top2 and con_top2 and pub_top2 != con_top2)
348
+
349
+ # ── 9. Risk ────────────────────────────────────────────────────────
350
+ risk_level = _compute_event_risk(
351
+ publisher_count=len(publishers),
352
+ consumer_count=len(consumers),
353
+ before_commit_count=len(before_commit_risks),
354
+ cross_module=cross_module,
355
+ )
356
+
357
+ # ── 10. Confidence ─────────────────────────────────────────────────
358
+ if resolution == "partial" or warnings:
359
+ confidence = "medium"
360
+ elif not publishers and not consumers:
361
+ confidence = "low"
362
+ else:
363
+ confidence = "high"
364
+
365
+ elapsed_ms = round((time.monotonic() - t0) * 1000, 2)
366
+
367
+ return EventTopologyResult(
368
+ schema_version=_SCHEMA_VERSION,
369
+ event_class=resolved_fqn,
370
+ resolution=resolution,
371
+ publishers=publishers,
372
+ consumers=[c.to_dict() for c in consumers],
373
+ event_graph={
374
+ "edges": graph_edges,
375
+ "level2_events": level2_events,
376
+ },
377
+ transaction_context=tx_context,
378
+ risk_level=risk_level,
379
+ confidence=confidence,
380
+ limitations=limitations,
381
+ metadata={
382
+ "query_time_ms": elapsed_ms,
383
+ "publisher_count": len(publishers),
384
+ "consumer_count": len(consumers),
385
+ "kafka_listeners_in_repo": kafka_count,
386
+ "rabbit_listeners_in_repo": rabbit_count,
387
+ "before_commit_risk_count": len(before_commit_risks),
388
+ "level2_events": list(level2_events.keys()),
389
+ "cross_module": cross_module,
390
+ "model_build_time_ms": model.build_time_ms,
391
+ },
392
+ )
393
+
394
+
395
+ # ---------------------------------------------------------------------------
396
+ # Convenience entry point
397
+ # ---------------------------------------------------------------------------
398
+
399
+ def run_event_topology(
400
+ cir: "CanonicalRepositoryIR",
401
+ event_class: str,
402
+ *,
403
+ model: "Optional[SpringSemanticModel]" = None,
404
+ ) -> EventTopologyResult:
405
+ """Run event topology query from a CIR.
406
+
407
+ Args:
408
+ cir: CanonicalRepositoryIR from build_canonical_ir().
409
+ event_class: Event class FQN or simple name.
410
+ model: Pre-built SpringSemanticModel. Built internally if None.
411
+
412
+ Returns EventTopologyResult — always JSON-serializable, never raises.
413
+ """
414
+ from sourcecode.spring_model import SpringSemanticModel as _SSM
415
+
416
+ try:
417
+ if model is None:
418
+ model = _SSM.build(cir)
419
+ orchestrator = EventTopologyOrchestrator()
420
+ return orchestrator.query(cir, model, event_class)
421
+ except Exception as exc:
422
+ return EventTopologyResult(
423
+ event_class=event_class,
424
+ resolution="not_found",
425
+ limitations=[f"Internal error: {type(exc).__name__}: {exc}"],
426
+ confidence="low",
427
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.2
3
+ Version: 1.35.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
@@ -39,7 +39,7 @@ Description-Content-Type: text/markdown
39
39
 
40
40
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
41
41
 
42
- ![Version](https://img.shields.io/badge/version-1.35.2-blue)
42
+ ![Version](https://img.shields.io/badge/version-1.35.3-blue)
43
43
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
44
44
 
45
45
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=e_12xn8iAOn0Ld38ig-U9tbnikpJi5jHz6llETh6nDk,103
1
+ sourcecode/__init__.py,sha256=l4fbYaAaQU-7wY0KjeGVwAwIoc3EKyw8SNnqZYuRfP8,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/canonical_ir.py,sha256=uwpwCnJxMh_eiIVg4cOLv7-aZthvmDFcG4azCOycLkw,24281
8
8
  sourcecode/cir_graphs.py,sha256=6CBwPoFCbHrqW8Bq_9fZUMi2IgcQBmoaiXiJjlOY9QE,7740
9
9
  sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
10
- sourcecode/cli.py,sha256=D6QbdECWLdGENhugefKVBEmVzBg3YP2DOMoIX60zlj8,214843
10
+ sourcecode/cli.py,sha256=UI80E2S1g6Ui300-vICvn4B7NXIuGYWE4a0FXrBa-8E,216875
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
@@ -36,13 +36,14 @@ 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=54L2tN0e_fEhAspMAaz8ft43Y5RiRNEZ5CtpO7HQwqA,162600
39
+ sourcecode/repository_ir.py,sha256=LI_kJzdz-iQOb0e0LuHnEzXRjUdoUnoRlicEk46rwgA,162975
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
43
43
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
44
44
  sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
45
45
  sourcecode/serializer.py,sha256=ooNZW2_fqx__BXII25eAWq-BomodvqQ6opUT_niQYCA,123403
46
+ sourcecode/spring_event_topology.py,sha256=LvGv5RXtU_O-fVB_OO9eDD2UmZM72Jn2oUHgOo50Qm0,17157
46
47
  sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
47
48
  sourcecode/spring_impact.py,sha256=ACmkbQMWgoJqZ9QLdHJLB1qNuFLgjEVv2r624X9Y6Y4,29004
48
49
  sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
@@ -89,8 +90,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
89
90
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
90
91
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
91
92
  sourcecode/telemetry/transport.py,sha256=KJeIPCPWMdmbCP3ySGs2iUlia34U6vWne2dZsUezesw,1560
92
- sourcecode-1.35.2.dist-info/METADATA,sha256=XdJipgxGDt6qiU1emmUPj1VS2ti9uOKsgB-f2VFq3jk,16440
93
- sourcecode-1.35.2.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
94
- sourcecode-1.35.2.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
95
- sourcecode-1.35.2.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
96
- sourcecode-1.35.2.dist-info/RECORD,,
93
+ sourcecode-1.35.3.dist-info/METADATA,sha256=3tFt-FeyMB5EsxTzF2Jw2DY8Up3A0iHat59uNWsylT8,16440
94
+ sourcecode-1.35.3.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
95
+ sourcecode-1.35.3.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
96
+ sourcecode-1.35.3.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
97
+ sourcecode-1.35.3.dist-info/RECORD,,