sourcecode 1.35.2__py3-none-any.whl → 1.35.4__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 +1 -1
- sourcecode/cir_graphs.py +20 -4
- sourcecode/cli.py +51 -1
- sourcecode/repository_ir.py +112 -13
- sourcecode/spring_event_topology.py +427 -0
- sourcecode/spring_impact.py +54 -10
- sourcecode/spring_tx_analyzer.py +34 -2
- {sourcecode-1.35.2.dist-info → sourcecode-1.35.4.dist-info}/METADATA +99 -6
- {sourcecode-1.35.2.dist-info → sourcecode-1.35.4.dist-info}/RECORD +12 -11
- {sourcecode-1.35.2.dist-info → sourcecode-1.35.4.dist-info}/WHEEL +0 -0
- {sourcecode-1.35.2.dist-info → sourcecode-1.35.4.dist-info}/entry_points.txt +0 -0
- {sourcecode-1.35.2.dist-info → sourcecode-1.35.4.dist-info}/licenses/LICENSE +0 -0
sourcecode/__init__.py
CHANGED
sourcecode/cir_graphs.py
CHANGED
|
@@ -68,11 +68,23 @@ class ImplementationGraph:
|
|
|
68
68
|
dependencies: cir.dependencies — list of edge dicts with 'from'/'to'/'type'
|
|
69
69
|
known_symbols: set(cir.symbols) — only in-repo FQNs
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
(e.g.
|
|
71
|
+
The Java parser stores 'implements' edges with the simple class name in the 'to'
|
|
72
|
+
field (e.g. 'OrderService') rather than the FQN. We resolve these via a
|
|
73
|
+
precomputed simple-name → FQN map built from known_symbols. Only unambiguous
|
|
74
|
+
resolutions are accepted; external framework interfaces and ambiguous names are
|
|
75
|
+
excluded.
|
|
76
|
+
|
|
73
77
|
Includes edges where the implementing class (from_fqn) is NOT in known_symbols
|
|
74
78
|
only when the interface IS known — this handles partial-parse edge cases.
|
|
75
79
|
"""
|
|
80
|
+
# Pre-build simple-name → [FQN] lookup for class-level symbols only (no '#').
|
|
81
|
+
# Used to resolve unqualified interface names (BUG-IC-001).
|
|
82
|
+
_simple_to_fqn: dict[str, list[str]] = {}
|
|
83
|
+
for sym in known_symbols:
|
|
84
|
+
if "#" not in sym and "." in sym:
|
|
85
|
+
simple = sym.rsplit(".", 1)[1]
|
|
86
|
+
_simple_to_fqn.setdefault(simple, []).append(sym)
|
|
87
|
+
|
|
76
88
|
impl_of: dict[str, list[str]] = {}
|
|
77
89
|
ifaces_of: dict[str, list[str]] = {}
|
|
78
90
|
|
|
@@ -83,9 +95,13 @@ class ImplementationGraph:
|
|
|
83
95
|
to_fqn = (edge.get("to") or "").strip()
|
|
84
96
|
if not from_fqn or not to_fqn:
|
|
85
97
|
continue
|
|
86
|
-
#
|
|
98
|
+
# Resolve to_fqn: prefer exact known-symbol match, then try simple-name lookup.
|
|
99
|
+
# Rejects external interfaces (java.*, org.springframework.*) and ambiguous names.
|
|
87
100
|
if to_fqn not in known_symbols:
|
|
88
|
-
|
|
101
|
+
candidates = _simple_to_fqn.get(to_fqn, [])
|
|
102
|
+
if len(candidates) != 1:
|
|
103
|
+
continue
|
|
104
|
+
to_fqn = candidates[0]
|
|
89
105
|
# Ignore malformed FQNs (e.g. generic type fragments like "Long>")
|
|
90
106
|
if ">" in to_fqn or "<" in to_fqn:
|
|
91
107
|
continue
|
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()
|
sourcecode/repository_ir.py
CHANGED
|
@@ -202,10 +202,13 @@ _FILTER_SECURITY_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
202
202
|
})
|
|
203
203
|
|
|
204
204
|
# Programmatic security: method-call patterns that indicate runtime auth enforcement.
|
|
205
|
+
# Requires method-call or field-access context — bare class name mentions (imports,
|
|
206
|
+
# type declarations) must NOT match or IAM/auth-domain repos generate false positives.
|
|
205
207
|
_PROGRAMMATIC_SECURITY_RE = re.compile(
|
|
206
208
|
r"\b(?:hasRole|hasAuthority|isAuthenticated|requirePermission|checkPermission"
|
|
207
209
|
r"|assertAuthorized|authenticate)\s*\("
|
|
208
|
-
r"|
|
|
210
|
+
r"|SecurityContextHolder\."
|
|
211
|
+
r"|\.(?:getAuthentication|getSecurityContext|getPrincipal|isAuthorized|checkAccess)\s*\("
|
|
209
212
|
r"|throw\s+new\s+(?:AccessDeniedException|UnauthorizedException|ForbiddenException|AuthenticationException)\b",
|
|
210
213
|
re.MULTILINE,
|
|
211
214
|
)
|
|
@@ -239,7 +242,7 @@ _LOMBOK_CTOR_ANNOTATIONS: frozenset[str] = frozenset({
|
|
|
239
242
|
})
|
|
240
243
|
|
|
241
244
|
# Transaction annotations whose args must be captured for semantic analysis.
|
|
242
|
-
_TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional"})
|
|
245
|
+
_TX_ANNOTATIONS: frozenset[str] = frozenset({"@Transactional", "@TransactionalEventListener"})
|
|
243
246
|
|
|
244
247
|
# Combined set used in _extract_symbols annotation-value capture.
|
|
245
248
|
_CAPTURE_ANN_ARGS: frozenset[str] = (
|
|
@@ -307,7 +310,9 @@ _SPRING_OTHER: frozenset[str] = frozenset({
|
|
|
307
310
|
"@PutMapping", "@DeleteMapping", "@PatchMapping", "@Autowired",
|
|
308
311
|
"@Inject", "@Value", "@Qualifier", "@EnableWebSecurity",
|
|
309
312
|
"@SpringBootApplication", "@EnableAutoConfiguration",
|
|
310
|
-
"@EventListener", "@
|
|
313
|
+
"@EventListener", "@TransactionalEventListener",
|
|
314
|
+
"@KafkaListener", "@RabbitListener",
|
|
315
|
+
"@Async", "@Scheduled", "@Cacheable", "@CacheEvict",
|
|
311
316
|
# CDI / Jakarta EE
|
|
312
317
|
"@ApplicationScoped", "@RequestScoped", "@SessionScoped", "@Dependent",
|
|
313
318
|
"@Named", "@Produces", "@Consumes",
|
|
@@ -321,6 +326,30 @@ _PUBLISH_EVENT_RE = re.compile(r'\.publishEvent\s*\(\s*new\s+(\w+)\s*[(\{]')
|
|
|
321
326
|
# Keycloak SPI event fire pattern: XxxEvent.fire(session, ...)
|
|
322
327
|
_FIRE_EVENT_RE = re.compile(r'\b(\w+Event)\.fire\s*\(')
|
|
323
328
|
|
|
329
|
+
# Class-level consumer detection from class signature (not annotations).
|
|
330
|
+
# Pattern 1: implements [Prefix]ApplicationListener<EventType>
|
|
331
|
+
# Matches both the standard Spring interface (ApplicationListener<E>) and
|
|
332
|
+
# framework-specific subinterfaces (BroadleafApplicationListener<E>,
|
|
333
|
+
# SmartApplicationListener<E>, etc.). Uses \w* prefix instead of \b so
|
|
334
|
+
# that "Broadleaf" prefix does not break the word boundary. (BUG-EVT-001)
|
|
335
|
+
_APP_LISTENER_RE = re.compile(r'\w*ApplicationListener\s*<\s*(\w+)\s*>')
|
|
336
|
+
# Pattern 2: extends AbstractXxxEventListener<EventType> — abstract base class pattern
|
|
337
|
+
# (Broadleaf's AbstractBroadleafApplicationEventListener and similar).
|
|
338
|
+
# Matches any parent class name that contains "EventListener".
|
|
339
|
+
_ABSTRACT_LISTENER_RE = re.compile(r'\bextends\s+\w+EventListener\w*\s*<\s*(\w+)\s*>')
|
|
340
|
+
|
|
341
|
+
# Block comment stripper — removes /* ... */ (including Javadoc) to prevent
|
|
342
|
+
# _PUBLISH_EVENT_RE / _FIRE_EVENT_RE from matching example code in comments.
|
|
343
|
+
_BLOCK_COMMENT_RE = re.compile(r'/\*.*?\*/', re.DOTALL)
|
|
344
|
+
_LINE_COMMENT_RE = re.compile(r'//[^\n]*')
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _strip_java_comments(source: str) -> str:
|
|
348
|
+
"""Remove // line comments and /* */ block comments from Java source."""
|
|
349
|
+
source = _BLOCK_COMMENT_RE.sub(' ', source)
|
|
350
|
+
source = _LINE_COMMENT_RE.sub(' ', source)
|
|
351
|
+
return source
|
|
352
|
+
|
|
324
353
|
# Edge types used for subsystem grouping — semantic hierarchy only, not imports
|
|
325
354
|
_SUBSYSTEM_STRUCTURAL_EDGES: frozenset[str] = frozenset({
|
|
326
355
|
"extends", "implements", "injects", "contained_in",
|
|
@@ -544,9 +573,37 @@ def _extract_symbols(source: str, rel_path: str) -> tuple[str, list[SymbolRecord
|
|
|
544
573
|
pending_ann_values: dict[str, str] = {}
|
|
545
574
|
in_block_comment = False
|
|
546
575
|
|
|
576
|
+
# BUG-PARSER-001: normalize multi-line class declarations where the opening brace
|
|
577
|
+
# appears on a continuation line (e.g. "implements A,\n B, C {").
|
|
578
|
+
# _CLASS_DECL_RE requires '{' on the same line as 'class' — joining the continuation
|
|
579
|
+
# here makes the regex work without changing the per-line brace-depth counter.
|
|
580
|
+
_raw_lines = source.splitlines()
|
|
581
|
+
_joined: list[str] = []
|
|
582
|
+
_i = 0
|
|
583
|
+
_CLASS_KW_RE = re.compile(r'\b(?:class|interface|enum)\s+[A-Z]')
|
|
584
|
+
while _i < len(_raw_lines):
|
|
585
|
+
_line = _raw_lines[_i]
|
|
586
|
+
_stripped = _line.strip()
|
|
587
|
+
if (_CLASS_KW_RE.search(_stripped) and '{' not in _stripped
|
|
588
|
+
and not _stripped.startswith('//')
|
|
589
|
+
and not _stripped.startswith('*')):
|
|
590
|
+
# Continuation: join until we hit a line containing '{'
|
|
591
|
+
_buf = _line
|
|
592
|
+
_i += 1
|
|
593
|
+
while _i < len(_raw_lines):
|
|
594
|
+
_cont = _raw_lines[_i]
|
|
595
|
+
_buf = _buf.rstrip() + ' ' + _cont.strip()
|
|
596
|
+
_i += 1
|
|
597
|
+
if '{' in _cont:
|
|
598
|
+
break
|
|
599
|
+
_joined.append(_buf)
|
|
600
|
+
else:
|
|
601
|
+
_joined.append(_line)
|
|
602
|
+
_i += 1
|
|
603
|
+
|
|
547
604
|
# P1 fix: normalize multiline annotations (e.g. @RequestMapping(\n value="..."\n))
|
|
548
605
|
# into single lines so the per-line regex can capture annotation args correctly.
|
|
549
|
-
_normalized_lines = _normalize_multiline_annotations(
|
|
606
|
+
_normalized_lines = _normalize_multiline_annotations(_joined)
|
|
550
607
|
|
|
551
608
|
for line in _normalized_lines:
|
|
552
609
|
stripped = line.strip()
|
|
@@ -1101,24 +1158,34 @@ def _build_relations(
|
|
|
1101
1158
|
))
|
|
1102
1159
|
|
|
1103
1160
|
# Event flow edges — listens_to_event and publishes_event.
|
|
1104
|
-
# Spring: method with @EventListener → resolved event
|
|
1161
|
+
# Spring: method with @EventListener or @TransactionalEventListener → resolved event type(s).
|
|
1162
|
+
_LISTENER_ANNOTATIONS: frozenset[str] = frozenset({
|
|
1163
|
+
"@EventListener", "@TransactionalEventListener",
|
|
1164
|
+
})
|
|
1105
1165
|
for sym in symbols:
|
|
1106
|
-
if sym.type == "method" and
|
|
1166
|
+
if sym.type == "method" and (sym.annotations and
|
|
1167
|
+
any(a in _LISTENER_ANNOTATIONS for a in sym.annotations)):
|
|
1168
|
+
ann = next(a for a in sym.annotations if a in _LISTENER_ANNOTATIONS)
|
|
1107
1169
|
for imp_fqn in sym.imports_used:
|
|
1108
1170
|
edges.append(RelationEdge(
|
|
1109
1171
|
from_symbol=sym.symbol,
|
|
1110
1172
|
to_symbol=imp_fqn,
|
|
1111
1173
|
type="listens_to_event",
|
|
1112
1174
|
confidence="high",
|
|
1113
|
-
evidence={"type": "annotation", "value":
|
|
1175
|
+
evidence={"type": "annotation", "value": ann},
|
|
1114
1176
|
))
|
|
1115
1177
|
|
|
1116
1178
|
_class_syms = [s for s in symbols if s.type in ("class", "interface") and "#" not in s.symbol]
|
|
1117
1179
|
|
|
1180
|
+
# Strip comments before event scanning to prevent Javadoc examples from
|
|
1181
|
+
# generating false publisher edges (BUG-003).
|
|
1182
|
+
_source_no_comments = _strip_java_comments(source)
|
|
1183
|
+
|
|
1118
1184
|
# Spring: class that calls publishEvent(new XxxEvent(...)) → event type FQN.
|
|
1119
|
-
for m in _PUBLISH_EVENT_RE.finditer(
|
|
1185
|
+
for m in _PUBLISH_EVENT_RE.finditer(_source_no_comments):
|
|
1120
1186
|
event_simple = m.group(1)
|
|
1121
|
-
|
|
1187
|
+
# BUG-004: try import_map first, then same-package map, then keep simple name.
|
|
1188
|
+
event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
|
|
1122
1189
|
for cls_sym in _class_syms:
|
|
1123
1190
|
edges.append(RelationEdge(
|
|
1124
1191
|
from_symbol=cls_sym.symbol,
|
|
@@ -1129,9 +1196,9 @@ def _build_relations(
|
|
|
1129
1196
|
))
|
|
1130
1197
|
|
|
1131
1198
|
# Keycloak SPI: XxxEvent.fire(...) static dispatch → publishes_event.
|
|
1132
|
-
for m in _FIRE_EVENT_RE.finditer(
|
|
1199
|
+
for m in _FIRE_EVENT_RE.finditer(_source_no_comments):
|
|
1133
1200
|
event_simple = m.group(1)
|
|
1134
|
-
event_fqn = import_map.get(event_simple
|
|
1201
|
+
event_fqn = import_map.get(event_simple) or _same_pkg.get(event_simple) or event_simple
|
|
1135
1202
|
for cls_sym in _class_syms:
|
|
1136
1203
|
edges.append(RelationEdge(
|
|
1137
1204
|
from_symbol=cls_sym.symbol,
|
|
@@ -1154,6 +1221,34 @@ def _build_relations(
|
|
|
1154
1221
|
evidence={"type": "signature", "value": f"implements {_ELP_IFACE}"},
|
|
1155
1222
|
))
|
|
1156
1223
|
|
|
1224
|
+
# Class-level consumer detection via class signature (EVT-003 / EVT-004).
|
|
1225
|
+
# Pattern A: class Foo implements ApplicationListener<XxxEvent>
|
|
1226
|
+
# → standard Spring interface, event type = generic param.
|
|
1227
|
+
# Pattern B: class Foo extends AbstractXxxEventListener<XxxEvent>
|
|
1228
|
+
# → abstract base class pattern (Broadleaf and similar frameworks),
|
|
1229
|
+
# event type = generic param of the parent class.
|
|
1230
|
+
for sym in _class_syms:
|
|
1231
|
+
sig = sym.signature or ""
|
|
1232
|
+
for pattern, ev_label in (
|
|
1233
|
+
(_APP_LISTENER_RE, "implements ApplicationListener"),
|
|
1234
|
+
(_ABSTRACT_LISTENER_RE, "extends *EventListener"),
|
|
1235
|
+
):
|
|
1236
|
+
m = pattern.search(sig)
|
|
1237
|
+
if m:
|
|
1238
|
+
event_simple = m.group(1)
|
|
1239
|
+
event_fqn = (
|
|
1240
|
+
import_map.get(event_simple)
|
|
1241
|
+
or _same_pkg.get(event_simple)
|
|
1242
|
+
or event_simple
|
|
1243
|
+
)
|
|
1244
|
+
edges.append(RelationEdge(
|
|
1245
|
+
from_symbol=sym.symbol,
|
|
1246
|
+
to_symbol=event_fqn,
|
|
1247
|
+
type="listens_to_event",
|
|
1248
|
+
confidence="high",
|
|
1249
|
+
evidence={"type": "signature", "value": f"{ev_label}<{event_simple}>"},
|
|
1250
|
+
))
|
|
1251
|
+
|
|
1157
1252
|
seen: set[tuple[str, str, str]] = set()
|
|
1158
1253
|
unique: list[RelationEdge] = []
|
|
1159
1254
|
for e in edges:
|
|
@@ -2314,8 +2409,12 @@ def _assemble(
|
|
|
2314
2409
|
for sym in _class_syms_asm
|
|
2315
2410
|
)
|
|
2316
2411
|
)
|
|
2412
|
+
# Only real annotation-based policies count (not "programmatic" fallback).
|
|
2413
|
+
# Programmatic security does not mean every unannotated endpoint is unsecured.
|
|
2317
2414
|
_has_ann_sec_asm = any(
|
|
2318
|
-
r.get("security_annotations")
|
|
2415
|
+
isinstance(r.get("security_annotations"), dict)
|
|
2416
|
+
and r["security_annotations"].get("policy") not in (None, "programmatic", "none_detected")
|
|
2417
|
+
for r in _route_surface
|
|
2319
2418
|
if isinstance(r, dict)
|
|
2320
2419
|
)
|
|
2321
2420
|
if _filter_based_asm and _has_ann_sec_asm:
|
|
@@ -3165,7 +3264,7 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
|
|
|
3165
3264
|
)
|
|
3166
3265
|
)
|
|
3167
3266
|
_has_annotation_security = any(
|
|
3168
|
-
e.get("security", {}).get("policy")
|
|
3267
|
+
e.get("security", {}).get("policy") not in (None, "none_detected", "programmatic")
|
|
3169
3268
|
for e in endpoints
|
|
3170
3269
|
)
|
|
3171
3270
|
if _filter_based and _has_annotation_security:
|
|
@@ -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
|
+
)
|
sourcecode/spring_impact.py
CHANGED
|
@@ -289,9 +289,16 @@ def _bfs_callers(
|
|
|
289
289
|
_add_caller(caller, depth)
|
|
290
290
|
# CH-002: injects edge to a field/constructor node → also traverse
|
|
291
291
|
# the containing class, bypassing the skipped contained_in edge.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
292
|
+
# Two formats emitted by the CIR parser:
|
|
293
|
+
# Constructor injection: pkg.Class#<init> (hash separator)
|
|
294
|
+
# Field injection: pkg.Class.field (dot, lowercase last segment)
|
|
295
|
+
if etype == "injects":
|
|
296
|
+
if "#" in caller:
|
|
297
|
+
_add_caller(caller.rsplit("#", 1)[0], depth)
|
|
298
|
+
elif "." in caller:
|
|
299
|
+
last_seg = caller.rsplit(".", 1)[1]
|
|
300
|
+
if last_seg and last_seg[0].islower():
|
|
301
|
+
_add_caller(caller.rsplit(".", 1)[0], depth)
|
|
295
302
|
|
|
296
303
|
return direct, indirect, was_truncated
|
|
297
304
|
|
|
@@ -316,10 +323,16 @@ def _collect_endpoints(
|
|
|
316
323
|
"""
|
|
317
324
|
all_fqns = set(seed_fqns) | set(all_callers)
|
|
318
325
|
|
|
319
|
-
# Class-level
|
|
320
|
-
#
|
|
326
|
+
# Class-level controller FQNs (no '#') that appear anywhere in the chain.
|
|
327
|
+
# Two cases produce a class-level controller node in the chain:
|
|
328
|
+
# 1. Seed is the controller class itself (user queried the whole controller).
|
|
329
|
+
# 2. Caller is the controller class node — happens when a service/repository
|
|
330
|
+
# is injected into a controller: the BFS reverse edge lands on the class
|
|
331
|
+
# node (e.g. "OwnerController" with no '#'), not a specific method.
|
|
332
|
+
# All endpoints of that controller are affected because any change to the
|
|
333
|
+
# injected dependency impacts every handler that uses it.
|
|
321
334
|
class_level_controllers: set[str] = {
|
|
322
|
-
fqn for fqn in
|
|
335
|
+
fqn for fqn in all_fqns
|
|
323
336
|
if "#" not in fqn and fqn in model.endpoint_index.controller_fqns
|
|
324
337
|
}
|
|
325
338
|
|
|
@@ -327,7 +340,7 @@ def _collect_endpoints(
|
|
|
327
340
|
seen_ep_ids: set[str] = set()
|
|
328
341
|
|
|
329
342
|
# Collect candidate controllers: those whose handler_symbol is in the chain
|
|
330
|
-
# OR whose class node
|
|
343
|
+
# OR whose class node appears in the chain (class-level).
|
|
331
344
|
candidate_controllers: set[str] = set(class_level_controllers)
|
|
332
345
|
for fqn in all_fqns:
|
|
333
346
|
cls = _class_of(fqn)
|
|
@@ -339,8 +352,8 @@ def _collect_endpoints(
|
|
|
339
352
|
handler = getattr(ep, "handler_symbol", "") or ""
|
|
340
353
|
ep_id = getattr(ep, "id", "") or ""
|
|
341
354
|
|
|
342
|
-
# Include if: handler is
|
|
343
|
-
# class-level
|
|
355
|
+
# Include if: handler method is in call chain OR the controller's class
|
|
356
|
+
# node appears at class-level in the chain (seed or DI-injected class).
|
|
344
357
|
if handler not in all_fqns and controller not in class_level_controllers:
|
|
345
358
|
continue
|
|
346
359
|
|
|
@@ -561,6 +574,32 @@ class ImpactOrchestrator:
|
|
|
561
574
|
f"added {n_syms} symbol(s) from {n_classes} implementation(s)."
|
|
562
575
|
)
|
|
563
576
|
|
|
577
|
+
# CH-001b: expand impl seeds to include their interfaces for BFS (BUG-IC-002).
|
|
578
|
+
# Callers typically inject the interface type, so reverse-graph edges live on
|
|
579
|
+
# the interface node, not on the implementation node. Without this expansion,
|
|
580
|
+
# querying 'OrderServiceImpl' finds 0 callers even though 36 classes inject it.
|
|
581
|
+
if impl_graph is not None:
|
|
582
|
+
current_seed_classes = {_class_of(s) for s in seed_fqns}
|
|
583
|
+
iface_seeds: list[str] = []
|
|
584
|
+
iface_classes_added: set[str] = set()
|
|
585
|
+
for seed_class in sorted(current_seed_classes):
|
|
586
|
+
ifaces = impl_graph.interfaces_of(seed_class)
|
|
587
|
+
for iface_class in ifaces:
|
|
588
|
+
if iface_class in iface_classes_added or iface_class in current_seed_classes:
|
|
589
|
+
continue
|
|
590
|
+
iface_classes_added.add(iface_class)
|
|
591
|
+
for sym in cir.symbols:
|
|
592
|
+
if _class_of(sym) == iface_class and sym not in set(seed_fqns):
|
|
593
|
+
iface_seeds.append(sym)
|
|
594
|
+
if iface_seeds:
|
|
595
|
+
seed_fqns = list(dict.fromkeys(seed_fqns + iface_seeds))
|
|
596
|
+
n_classes = len(iface_classes_added)
|
|
597
|
+
n_syms = len(iface_seeds)
|
|
598
|
+
warnings.append(
|
|
599
|
+
f"Implementation-to-interface expansion (CH-001b): "
|
|
600
|
+
f"added {n_syms} symbol(s) from {n_classes} interface(s) for caller BFS."
|
|
601
|
+
)
|
|
602
|
+
|
|
564
603
|
# ── 2. BFS through reverse graph ─────────────────────────────────
|
|
565
604
|
direct_callers, indirect_callers, truncated = _bfs_callers(
|
|
566
605
|
seed_fqns, cir.reverse_graph, depth
|
|
@@ -580,8 +619,13 @@ class ImpactOrchestrator:
|
|
|
580
619
|
try:
|
|
581
620
|
boundary = model.tx_index.effective_boundary(resolved_symbol)
|
|
582
621
|
if boundary is None and "#" not in resolved_symbol:
|
|
583
|
-
# Class-level symbol — try class_level directly
|
|
622
|
+
# Class-level symbol — try class_level directly, then fall back
|
|
623
|
+
# to first method-level boundary if class has only method-level TX.
|
|
584
624
|
boundary = model.tx_index.class_level.get(resolved_symbol)
|
|
625
|
+
if boundary is None:
|
|
626
|
+
method_boundaries = model.tx_index.by_class.get(resolved_symbol, [])
|
|
627
|
+
if method_boundaries:
|
|
628
|
+
boundary = method_boundaries[0]
|
|
585
629
|
if boundary is not None:
|
|
586
630
|
tx_boundary = boundary.to_dict()
|
|
587
631
|
except Exception:
|
sourcecode/spring_tx_analyzer.py
CHANGED
|
@@ -63,6 +63,30 @@ _CATCH_SWALLOW_RE = re.compile(
|
|
|
63
63
|
_RETHROW_IN_CATCH_RE = re.compile(r'\bthrow\b')
|
|
64
64
|
|
|
65
65
|
|
|
66
|
+
def _extract_method_body(source: str, method_name: str) -> str:
|
|
67
|
+
"""Extract the first method body matching method_name using brace counting.
|
|
68
|
+
|
|
69
|
+
Returns the text from '{' to the matching '}', or empty string if not found.
|
|
70
|
+
Needed to scope TX-005 regex to the specific method instead of the whole file.
|
|
71
|
+
"""
|
|
72
|
+
pattern = re.compile(r'\b' + re.escape(method_name) + r'\s*\(')
|
|
73
|
+
for m in pattern.finditer(source):
|
|
74
|
+
brace_pos = source.find('{', m.end())
|
|
75
|
+
if brace_pos < 0:
|
|
76
|
+
continue
|
|
77
|
+
depth = 1
|
|
78
|
+
i = brace_pos + 1
|
|
79
|
+
while i < len(source) and depth > 0:
|
|
80
|
+
c = source[i]
|
|
81
|
+
if c == '{':
|
|
82
|
+
depth += 1
|
|
83
|
+
elif c == '}':
|
|
84
|
+
depth -= 1
|
|
85
|
+
i += 1
|
|
86
|
+
return source[brace_pos:i]
|
|
87
|
+
return ""
|
|
88
|
+
|
|
89
|
+
|
|
66
90
|
# ---------------------------------------------------------------------------
|
|
67
91
|
# Pattern protocol
|
|
68
92
|
# ---------------------------------------------------------------------------
|
|
@@ -531,8 +555,16 @@ class _TX005ExceptionSwallowing:
|
|
|
531
555
|
return findings
|
|
532
556
|
|
|
533
557
|
def _has_swallowed_exception(self, source: str, symbol: str) -> bool:
|
|
534
|
-
"""Return True if
|
|
535
|
-
|
|
558
|
+
"""Return True if the specific method body has a catch-log-no-rethrow pattern.
|
|
559
|
+
|
|
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.
|
|
562
|
+
"""
|
|
563
|
+
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):
|
|
536
568
|
block = match.group(0)
|
|
537
569
|
if not _RETHROW_IN_CATCH_RE.search(block):
|
|
538
570
|
return True
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sourcecode
|
|
3
|
-
Version: 1.35.
|
|
3
|
+
Version: 1.35.4
|
|
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
|
-

|
|
43
43
|

|
|
44
44
|
|
|
45
45
|
---
|
|
@@ -113,7 +113,7 @@ pipx install sourcecode
|
|
|
113
113
|
|
|
114
114
|
```bash
|
|
115
115
|
sourcecode version
|
|
116
|
-
# sourcecode 1.
|
|
116
|
+
# sourcecode 1.35.4
|
|
117
117
|
```
|
|
118
118
|
|
|
119
119
|
---
|
|
@@ -133,6 +133,15 @@ sourcecode --agent
|
|
|
133
133
|
# Blast radius: what breaks if this class changes?
|
|
134
134
|
sourcecode impact OrderService /path/to/repo
|
|
135
135
|
|
|
136
|
+
# Spring semantic audit: TX anomalies + security surface (free)
|
|
137
|
+
sourcecode spring-audit /path/to/repo
|
|
138
|
+
|
|
139
|
+
# Impact chain: systemic blast radius with TX/SEC enrichment (free)
|
|
140
|
+
sourcecode impact-chain OrderService /path/to/repo
|
|
141
|
+
|
|
142
|
+
# Event topology: publisher → event → consumer graph (free)
|
|
143
|
+
sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
144
|
+
|
|
136
145
|
# REST endpoint surface
|
|
137
146
|
sourcecode endpoints /path/to/repo
|
|
138
147
|
|
|
@@ -187,15 +196,21 @@ sourcecode /repo --agent # ~4,500–5,500 tokens — more detail
|
|
|
187
196
|
sourcecode onboard /repo # task-structured: entry points, key files, gaps
|
|
188
197
|
```
|
|
189
198
|
|
|
190
|
-
### Before every change — blast radius check
|
|
199
|
+
### Before every change — blast radius + TX/SEC check
|
|
191
200
|
|
|
192
201
|
```bash
|
|
193
202
|
# Always target the INTERFACE in Spring projects, not the implementation:
|
|
194
203
|
sourcecode impact OrderService /repo # ✓ 30 callers, 11 endpoints
|
|
195
204
|
sourcecode impact OrderServiceImpl /repo # ✗ 0 callers (Spring DI blindness)
|
|
196
205
|
|
|
197
|
-
#
|
|
198
|
-
sourcecode impact
|
|
206
|
+
# Impact chain: blast radius enriched with TX boundary and security surfaces
|
|
207
|
+
sourcecode impact-chain OrderService /repo
|
|
208
|
+
|
|
209
|
+
# Event topology: who publishes/consumes this event, and in what TX phase?
|
|
210
|
+
sourcecode impact-chain OrderPlacedEvent /repo --type events
|
|
211
|
+
|
|
212
|
+
# Spring audit: catch TX anomalies before they hit production
|
|
213
|
+
sourcecode spring-audit /repo --scope tx
|
|
199
214
|
```
|
|
200
215
|
|
|
201
216
|
### Continuous agent loop — delta context
|
|
@@ -262,6 +277,9 @@ Specifically:
|
|
|
262
277
|
- Endpoint recall for JAX-RS subresource locator pattern is ~65%
|
|
263
278
|
- `impact` on implementation classes (e.g. `OrderServiceImpl`) returns 0 callers in Spring Boot — callers inject the interface via `@Autowired`. Always target the interface. When `direct_callers: []` with `confidence_level: high` for a `@Service` class, re-query the interface.
|
|
264
279
|
- `no_security_signal` on endpoints means no method-level annotations found — does **not** mean the endpoint is unsecured. Projects using Spring Security filter chains show 100% `no_security_signal` even when fully secured.
|
|
280
|
+
- `spring-audit` and `impact-chain` are **Java/Spring only** — non-Java repos return `spring_detected: false`
|
|
281
|
+
- Event topology via `--type events` does not resolve Kafka/RabbitMQ/Redis message routes — only Spring ApplicationEvent and `@EventListener` chains
|
|
282
|
+
- Self-invocation TX bypass (calling `@Transactional` method from the same class without going through the proxy) is not detected
|
|
265
283
|
|
|
266
284
|
---
|
|
267
285
|
|
|
@@ -312,6 +330,81 @@ sourcecode endpoints /path/to/repo --output endpoints.json
|
|
|
312
330
|
|
|
313
331
|
Extracts all Spring MVC (`@GetMapping`, `@PostMapping`, `@RequestMapping`, etc.) and JAX-RS (`@GET`, `@POST`, `@Path`) endpoint methods. Returns HTTP method, path, controller class, and handler method.
|
|
314
332
|
|
|
333
|
+
### `spring-audit` — Spring semantic audit [free]
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
sourcecode spring-audit /path/to/repo
|
|
337
|
+
sourcecode spring-audit /path/to/repo --scope tx # TX anomalies only
|
|
338
|
+
sourcecode spring-audit /path/to/repo --scope security # security surface only
|
|
339
|
+
sourcecode spring-audit /path/to/repo --min-severity high
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Detects structural Spring anomalies that survive code review and tests, but cause production failures:
|
|
343
|
+
|
|
344
|
+
| Pattern | Description |
|
|
345
|
+
|---------|-------------|
|
|
346
|
+
| `TX-001` | `@Transactional` on private/final method — CGLIB proxy bypass, TX silently ignored |
|
|
347
|
+
| `TX-002` | `REQUIRES_NEW` nested inside `REQUIRED` call chain — unexpected transaction nesting |
|
|
348
|
+
| `TX-003` | `readOnly=true` boundary propagating to write operation |
|
|
349
|
+
| `TX-004` | `NOT_SUPPORTED`/`NEVER` called within active TX chain |
|
|
350
|
+
| `TX-005` | Exception swallowing inside `@Transactional` — silent TX rollback suppression |
|
|
351
|
+
| `SEC-001` | Unsecured endpoint in annotation-based security model |
|
|
352
|
+
| `SEC-002` | CVE-2025-41248: `@PreAuthorize` on inherited method from generic supertype |
|
|
353
|
+
| `SEC-003` | `@Transactional` on `@Controller`/`@RestController` — TX in wrong layer |
|
|
354
|
+
|
|
355
|
+
Returns structured findings with `severity`, `confidence`, `symbol`, `source_file`, `evidence`, `explanation`, and `fix_hint`. JAVA/SPRING ONLY.
|
|
356
|
+
|
|
357
|
+
### `impact-chain` — systemic blast radius with TX/SEC enrichment [free]
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
sourcecode impact-chain OrderService /path/to/repo
|
|
361
|
+
sourcecode impact-chain com.example.OrderService#placeOrder /path/to/repo
|
|
362
|
+
sourcecode impact-chain PaymentService . --depth 6
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Unlike `impact` (which traces the caller graph), `impact-chain` builds on the SpringSemanticModel to enrich every step of the blast cone with transaction and security context:
|
|
366
|
+
|
|
367
|
+
| Field | Description |
|
|
368
|
+
|-------|-------------|
|
|
369
|
+
| `direct_callers` | Symbols that directly call the target |
|
|
370
|
+
| `indirect_callers` | Transitive callers (BFS up to `--depth` hops, default: 4) |
|
|
371
|
+
| `endpoints_affected` | HTTP endpoints reachable through the call chain |
|
|
372
|
+
| `transaction_boundary` | `@Transactional` semantics on the target: propagation, isolation, readOnly |
|
|
373
|
+
| `security_surfaces` | Per-endpoint security policy + SEC finding IDs |
|
|
374
|
+
| `impact_findings` | TX-001..005 and SEC-001..003 findings that touch the call chain |
|
|
375
|
+
| `risk_level` | `critical` \| `high` \| `medium` \| `low` |
|
|
376
|
+
|
|
377
|
+
**Event topology** — query the publisher/consumer graph for a Spring event class:
|
|
378
|
+
|
|
379
|
+
```bash
|
|
380
|
+
sourcecode impact-chain OrderPlacedEvent /path/to/repo --type events
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
| Field | Description |
|
|
384
|
+
|-------|-------------|
|
|
385
|
+
| `publishers` | FQNs that publish this event class |
|
|
386
|
+
| `consumers` | Listeners with TX phase metadata (`AFTER_COMMIT`, `BEFORE_COMMIT`, etc.) |
|
|
387
|
+
| `event_graph` | Publisher → event → consumer edges (BFS ≤ 2 hops) |
|
|
388
|
+
| `transaction_context` | `AFTER_COMMIT` consumers, `BEFORE_COMMIT` risks |
|
|
389
|
+
| `risk_level` | Derived from TX phase and consumer count |
|
|
390
|
+
|
|
391
|
+
**Limitations of event topology:**
|
|
392
|
+
- Resolves Spring `ApplicationEvent` / `@EventListener` chains only
|
|
393
|
+
- Does not trace Kafka, RabbitMQ, Redis, or other message brokers
|
|
394
|
+
- Does not detect self-invocation proxy bypass
|
|
395
|
+
- Conditional beans (`@ConditionalOnProperty`) are not evaluated at analysis time
|
|
396
|
+
|
|
397
|
+
### `cold-start` — RIS bootstrap context
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
sourcecode cold-start /path/to/repo
|
|
401
|
+
sourcecode cold-start /path/to/repo --compact # ~10K token subset
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Returns the Repository Intelligence Snapshot (RIS) instantly — zero re-analysis. The RIS is built by a prior warm cache pass and includes stacks, entry points, endpoint surface, and Spring semantic signals. Status field: `cold_start_ready` | `cold_start_stale` | `no_ris`.
|
|
405
|
+
|
|
406
|
+
Use `--compact` to get a ~10K token subset safe for direct LLM injection. Full snapshot can exceed 100K tokens on medium repos — use `--output FILE` for local search tooling.
|
|
407
|
+
|
|
315
408
|
### `repo-ir` — symbol-level IR
|
|
316
409
|
|
|
317
410
|
```bash
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
sourcecode/__init__.py,sha256=
|
|
1
|
+
sourcecode/__init__.py,sha256=HvT-OD6BpIxm8uMM8dF8IJUh4mkn6RKbB0__ghb_vHI,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
|
|
5
5
|
sourcecode/ast_extractor.py,sha256=_btmeOJIe3t-NicF94D5ZAesa2YIJ0_QNExGnbHxGFE,50578
|
|
6
6
|
sourcecode/cache.py,sha256=wAyPrXN5DqiGivnMpeEuun2xHDKfBer2_oBsh6kj_vc,30447
|
|
7
7
|
sourcecode/canonical_ir.py,sha256=uwpwCnJxMh_eiIVg4cOLv7-aZthvmDFcG4azCOycLkw,24281
|
|
8
|
-
sourcecode/cir_graphs.py,sha256=
|
|
8
|
+
sourcecode/cir_graphs.py,sha256=wuRjW0MjSr9KLq03L1TzYqkIeY_phUmIbVA3FQDfHKk,8603
|
|
9
9
|
sourcecode/classifier.py,sha256=2lYoSH3vOTkXZYPU7Go2WIet1-IuNzTWVhc-ULnXtgw,8024
|
|
10
|
-
sourcecode/cli.py,sha256=
|
|
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,19 +36,20 @@ 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=
|
|
39
|
+
sourcecode/repository_ir.py,sha256=uLIewoLBg4nknI1JlI8bPo_kl9SVyT-GFQqENTeFz1M,167673
|
|
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
|
-
sourcecode/spring_impact.py,sha256
|
|
48
|
+
sourcecode/spring_impact.py,sha256=-ET4oB9tZQYfcyhfI781mMJCU-przz6x4Ejwr3hejA8,31743
|
|
48
49
|
sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
|
|
49
50
|
sourcecode/spring_security_audit.py,sha256=RPr491FGAmWNxqe-uN0FS2gmjQ0M5ryRyXjLNMMzKFk,20077
|
|
50
51
|
sourcecode/spring_semantic.py,sha256=CiAf77p48-RFrUF0zbgww4w2Xigrbo1t5M3ZCDIfV_g,12032
|
|
51
|
-
sourcecode/spring_tx_analyzer.py,sha256=
|
|
52
|
+
sourcecode/spring_tx_analyzer.py,sha256=eBcYLRKhlUllHl195CVGNWeg2vMext4u1Tezu_Mwrdg,27143
|
|
52
53
|
sourcecode/summarizer.py,sha256=YspHEVeYJVmltq0FMtGZF8kIP3qiR2KLcanGL6Y7uTI,20747
|
|
53
54
|
sourcecode/tree_utils.py,sha256=8GAkIfQAsvtEudIeW1l4ooH_oRtrWR8cpJQJsEa_Pfw,2093
|
|
54
55
|
sourcecode/workspace.py,sha256=X_6NmNnitvT3_38V-JDChydo_sR68s249hLFlrQskU0,8271
|
|
@@ -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.
|
|
93
|
-
sourcecode-1.35.
|
|
94
|
-
sourcecode-1.35.
|
|
95
|
-
sourcecode-1.35.
|
|
96
|
-
sourcecode-1.35.
|
|
93
|
+
sourcecode-1.35.4.dist-info/METADATA,sha256=Pl9s6-m8pSLDCwawxAneFDVHpQvJLxehMZvruYMWnr0,21263
|
|
94
|
+
sourcecode-1.35.4.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
95
|
+
sourcecode-1.35.4.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
|
|
96
|
+
sourcecode-1.35.4.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
|
|
97
|
+
sourcecode-1.35.4.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|