sourcecode 1.35.25__py3-none-any.whl → 1.35.26__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.25"
3
+ __version__ = "1.35.26"
@@ -868,9 +868,34 @@ def _find_build_files(root: Path) -> list[tuple[Path, str]]:
868
868
  return results
869
869
 
870
870
 
871
+ def _resolve_maven_properties(text: str) -> str:
872
+ """Substitute ${prop} references with values from the <properties> block.
873
+
874
+ Handles single-level property references that appear in the same pom.xml.
875
+ Multi-level references (${a} where a=${b}) are resolved up to 3 passes.
876
+ """
877
+ props: dict[str, str] = {}
878
+ for m in re.finditer(r'<([A-Za-z][\w.\-]*)>\s*([^<${}]+?)\s*</\1>', text):
879
+ props[m.group(1)] = m.group(2).strip()
880
+ if not props:
881
+ return text
882
+
883
+ resolved = text
884
+ for _ in range(3):
885
+ def _sub(m: re.Match) -> str: # noqa: E306
886
+ return props.get(m.group(1), m.group(0))
887
+ resolved_new = re.sub(r'\$\{([\w.\-]+)\}', _sub, resolved)
888
+ if resolved_new == resolved:
889
+ break
890
+ resolved = resolved_new
891
+ return resolved
892
+
893
+
871
894
  def _scan_dep_file(text: str, rel_path: str) -> list["MigrationFinding"]:
872
895
  """Apply dependency rules to a build file. Returns one finding per matched rule."""
873
896
  is_gradle = rel_path.endswith((".gradle", ".gradle.kts"))
897
+ if not is_gradle and rel_path.endswith(".xml"):
898
+ text = _resolve_maven_properties(text)
874
899
  findings: list[MigrationFinding] = []
875
900
  for rule in _DEP_RULES:
876
901
  if rule.quick_filter is not None and rule.quick_filter not in text:
@@ -1198,6 +1223,7 @@ def run_migrate_check(
1198
1223
  limitations.append(f"{xml_read_errors} XML file(s) could not be read and were skipped.")
1199
1224
 
1200
1225
  dep_read_errors = 0
1226
+ raw_dep_findings: list[MigrationFinding] = []
1201
1227
  for abs_path, rel_path in build_files:
1202
1228
  try:
1203
1229
  text = abs_path.read_text(encoding="utf-8", errors="replace")
@@ -1206,7 +1232,20 @@ def run_migrate_check(
1206
1232
  continue
1207
1233
  dep_findings = _scan_dep_file(text, rel_path)
1208
1234
  filtered = [f for f in dep_findings if SEVERITY_ORDER.get(f.severity, 3) <= min_order]
1209
- all_findings.extend(filtered)
1235
+ raw_dep_findings.extend(filtered)
1236
+
1237
+ # Deduplicate dep findings by rule_id: same dependency in parent + child poms
1238
+ # is one logical finding. Keep the first occurrence (root pom sorts first).
1239
+ _seen_dep_rules: dict[str, int] = {} # rule_id → count
1240
+ for f in raw_dep_findings:
1241
+ _seen_dep_rules[f.rule_id] = _seen_dep_rules.get(f.rule_id, 0) + 1
1242
+ _dedup_dep: list[MigrationFinding] = []
1243
+ _emitted: set[str] = set()
1244
+ for f in raw_dep_findings:
1245
+ if f.rule_id not in _emitted:
1246
+ _dedup_dep.append(f)
1247
+ _emitted.add(f.rule_id)
1248
+ all_findings.extend(_dedup_dep)
1210
1249
 
1211
1250
  if dep_read_errors:
1212
1251
  limitations.append(f"{dep_read_errors} build file(s) could not be read and were skipped.")
@@ -105,7 +105,12 @@ class EvidenceBundle:
105
105
  _PKG_RE = re.compile(r'^package\s+([\w.]+)\s*;', re.MULTILINE)
106
106
  _IMPORT_RE = re.compile(r'^import\s+(?:static\s+)?([\w.]+(?:\.\*)?)\s*;', re.MULTILINE)
107
107
  _ANN_RE = re.compile(r'^(@[\w.]+)')
108
- _ANN_WITH_ARGS_RE = re.compile(r'^(@[\w.]+)\s*(?:\(([^)]*)\))?')
108
+ _ANN_WITH_ARGS_RE = re.compile(
109
+ r'^(@[\w.]+)\s*'
110
+ r'(?:\('
111
+ r'((?:[^()"\']*|"[^"]*"|\'[^\']*\'|\((?:[^()"\']*|"[^"]*"|\'[^\']*\')*\))*)'
112
+ r'\))?'
113
+ )
109
114
 
110
115
  _CLASS_DECL_RE = re.compile(
111
116
  r'(?:^|(?<=\s))'
@@ -3303,6 +3308,52 @@ def extract_java_endpoints(root: Path) -> "dict[str, Any]":
3303
3308
  else:
3304
3309
  security_model = "unknown"
3305
3310
 
3311
+ # Detect XML-based Spring Security config. When present, per-endpoint
3312
+ # none_detected is expected and does NOT mean the endpoint is unsecured —
3313
+ # security is declared in XML (HttpSecurity rules, filter chains, web.xml
3314
+ # security constraints). Update security_model and re-tag affected endpoints
3315
+ # so the output cannot be misread as "unprotected".
3316
+ _XML_SECURITY_RE = re.compile(
3317
+ r'(?:xmlns(?::[a-z]+)?="http://www\.springframework\.org/schema/security"'
3318
+ r'|<security:http\b'
3319
+ r'|<http\s[^>]*use-expressions'
3320
+ r'|spring-security-[2345]'
3321
+ r'|xmlns:security="http://www\.springframework\.org/schema/security")',
3322
+ re.IGNORECASE,
3323
+ )
3324
+ _xml_security_detected = False
3325
+ _XML_GLOBS = (
3326
+ "*security*.xml", "*Security*.xml",
3327
+ "*applicationContext*.xml", "*-context.xml", "*Context.xml",
3328
+ "*spring*.xml", "*Spring*.xml",
3329
+ )
3330
+ for _glob in _XML_GLOBS:
3331
+ for _xf in root.rglob(_glob):
3332
+ if "target/" in str(_xf).replace("\\", "/"):
3333
+ continue
3334
+ try:
3335
+ _xt = _xf.read_text(encoding="utf-8", errors="replace")
3336
+ except OSError:
3337
+ continue
3338
+ if _XML_SECURITY_RE.search(_xt):
3339
+ _xml_security_detected = True
3340
+ break
3341
+ if _xml_security_detected:
3342
+ break
3343
+
3344
+ if _xml_security_detected and security_model == "unknown":
3345
+ security_model = "xml_or_filter_chain"
3346
+ # Re-tag per-endpoint none_detected → xml_or_filter_chain so the output
3347
+ # cannot be misread as "endpoint is unprotected".
3348
+ for ep in endpoints:
3349
+ if ep.get("security", {}).get("policy") == "none_detected":
3350
+ ep["security"] = {"policy": "xml_or_filter_chain"}
3351
+ # Recompute no_security_signal (now counts only truly unknown endpoints)
3352
+ no_security_signal = sum(
3353
+ 1 for e in endpoints
3354
+ if e.get("security", {}).get("policy") == "none_detected"
3355
+ )
3356
+
3306
3357
  return {
3307
3358
  "endpoints": endpoints,
3308
3359
  "total": len(endpoints),
@@ -197,14 +197,17 @@ def _compute_event_risk(
197
197
  consumer_count: int,
198
198
  before_commit_count: int,
199
199
  cross_module: bool,
200
+ sync_in_tx_count: int = 0,
200
201
  ) -> str:
201
202
  """Deterministic risk scoring per spec.
202
203
 
203
204
  high: fanout > 5 OR cross-module propagation OR BEFORE_COMMIT consumers
205
+ OR sync @EventListener inside @Transactional publisher
204
206
  medium: 2–5 consumers
205
207
  low: ≤1 consumer
206
208
  """
207
- if consumer_count > _RISK_FANOUT_HIGH or cross_module or before_commit_count > 0:
209
+ if (consumer_count > _RISK_FANOUT_HIGH or cross_module
210
+ or before_commit_count > 0 or sync_in_tx_count > 0):
208
211
  return "high"
209
212
  if consumer_count >= _RISK_FANOUT_MEDIUM:
210
213
  return "medium"
@@ -327,9 +330,23 @@ class EventTopologyOrchestrator:
327
330
  # ── 7. TX context ──────────────────────────────────────────────────
328
331
  after_commit = [c.fqn for c in consumers if c.transactional_phase == "AFTER_COMMIT"]
329
332
  before_commit_risks = [c.fqn for c in consumers if c.transactional_phase == "BEFORE_COMMIT"]
333
+
334
+ # Detect sync @EventListener inside @Transactional publisher.
335
+ # Plain @EventListener fires synchronously; if the publisher method is
336
+ # @Transactional the listener runs inside that TX — listener exception
337
+ # rolls back the outer TX, and DB state may be partially committed.
338
+ tx_publishers = [
339
+ p for p in publishers
340
+ if "@Transactional" in ((fqn_index.get(p) or {}).get("annotations") or [])
341
+ ]
342
+ sync_in_tx_risks = [
343
+ c.fqn for c in consumers
344
+ if c.type == "spring_event" and tx_publishers
345
+ ]
330
346
  tx_context = {
331
347
  "after_commit_consumers": after_commit,
332
348
  "before_commit_risks": before_commit_risks,
349
+ "sync_in_tx_risks": sync_in_tx_risks,
333
350
  }
334
351
 
335
352
  # ── 8. Cross-module detection ──────────────────────────────────────
@@ -352,6 +369,7 @@ class EventTopologyOrchestrator:
352
369
  consumer_count=len(consumers),
353
370
  before_commit_count=len(before_commit_risks),
354
371
  cross_module=cross_module,
372
+ sync_in_tx_count=len(sync_in_tx_risks),
355
373
  )
356
374
 
357
375
  # ── 10. Confidence ─────────────────────────────────────────────────
@@ -385,6 +403,7 @@ class EventTopologyOrchestrator:
385
403
  "kafka_listeners_in_repo": kafka_count,
386
404
  "rabbit_listeners_in_repo": rabbit_count,
387
405
  "before_commit_risk_count": len(before_commit_risks),
406
+ "sync_in_tx_risk_count": len(sync_in_tx_risks),
388
407
  "level2_events": list(level2_events.keys()),
389
408
  "cross_module": cross_module,
390
409
  "model_build_time_ms": model.build_time_ms,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 1.35.25
3
+ Version: 1.35.26
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
@@ -40,7 +40,7 @@ Description-Content-Type: text/markdown
40
40
 
41
41
  **Persistent structural context and ultra-fast repeated analysis for AI coding agents.**
42
42
 
43
- ![Version](https://img.shields.io/badge/version-1.35.25-blue)
43
+ ![Version](https://img.shields.io/badge/version-1.35.26-blue)
44
44
  ![Python](https://img.shields.io/badge/python-3.10%2B-green)
45
45
 
46
46
  ---
@@ -114,7 +114,7 @@ pipx install sourcecode
114
114
 
115
115
  ```bash
116
116
  sourcecode version
117
- # sourcecode 1.35.25
117
+ # sourcecode 1.35.26
118
118
  ```
119
119
 
120
120
  ---
@@ -1,4 +1,4 @@
1
- sourcecode/__init__.py,sha256=1fGjuVJyBU95TOEnJBXLcVgGYs_QYMvaKwPypDXVP9g,104
1
+ sourcecode/__init__.py,sha256=HYA42OYABbKZ43Trr1VCdpUkXMtz0AqpwPt0ENMTTjo,104
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
@@ -29,7 +29,7 @@ sourcecode/graph_analyzer.py,sha256=DHR8fY69oU_Pi4SYaWboX6EoEFrctQKB9dsjpqwGMzw,
29
29
  sourcecode/license.py,sha256=3JCV2OeTVttKrOGBguU5uZC0c02Stig-KLB0mP2lNiY,22742
30
30
  sourcecode/mcp_nudge.py,sha256=5ELU_ixzh6uA83NXLOZT8h00OhL53okfQdji3jyKOjg,2917
31
31
  sourcecode/metrics_analyzer.py,sha256=m0ENgtqKeBL17kUIK3fmGkgo7UfXBNHxCMj0H_Y5K7c,22750
32
- sourcecode/migrate_check.py,sha256=-jghKewJwMO0VCXML-ZY1KI_RQO5gd5-pyLCMg5u8jA,52971
32
+ sourcecode/migrate_check.py,sha256=GuYK36DDFkwf07jbAgcoc-Ovq8ttLQNMsRqhsUilMzY,54514
33
33
  sourcecode/output_budget.py,sha256=Js9yUlfQtPhqBl9R6wn_9UHVjjJc3GtLcqyfjf5t50Q,9869
34
34
  sourcecode/path_filters.py,sha256=ROFRQ8eSLBEMiixK9f45-RO7um4VEEcjoD5AA4I427I,3739
35
35
  sourcecode/pr_comment_renderer.py,sha256=smHslxiG14lrytCkq5nFrFu-qTHgA-t-LFYfdrfjz2o,14423
@@ -40,14 +40,14 @@ sourcecode/ranking_engine.py,sha256=ZAucq_YX2KkWUuAZf4P0lhtQ_38vEFnUhuGtSZd1S0E,
40
40
  sourcecode/redactor.py,sha256=SB4hwIvg8h-hvcqKcDWaZvA-aSyn-at-BIRwa0tUv5E,3227
41
41
  sourcecode/relevance_scorer.py,sha256=0AgEt4KrV73nioMqBgjhGjtY7L2C7L7cSyKtj3IKcrw,9408
42
42
  sourcecode/repo_classifier.py,sha256=FG1vaWKdWXsWdl-S8hjVMiTqcwgaRXkDyvK4rPcOGtQ,22681
43
- sourcecode/repository_ir.py,sha256=vTdWuFj0iRUavs4mOWl87JETR7Je9bKVeDycKBqOFp8,169640
43
+ sourcecode/repository_ir.py,sha256=hl7Vc7o5LD0xWZ6Er7-x2IDrLurJZIfBKGPD-3cfraU,171754
44
44
  sourcecode/ris.py,sha256=RcqLVwC-doFcKKViYDkCjZLBqf_wzLES7-F6vHEeWzE,20419
45
45
  sourcecode/runtime_classifier.py,sha256=uTAD6BDCiBLUZEDRfqk718kM4RTT_vAbfkcOI2_Xx58,18432
46
46
  sourcecode/scanner.py,sha256=WdOQ78mMzjR1NjmKTlbxdgwinnCTfAhxCVLBEFQiFHU,8899
47
47
  sourcecode/schema.py,sha256=aHNXDf8LGyUC8ZDE_VS9kiskC2-Oswhi_WnpdGy6HDw,24897
48
48
  sourcecode/semantic_analyzer.py,sha256=TDuC3wzZR2DPm1mgrAg1YSLk2QzJoueS3TZAmyGGpCU,89417
49
49
  sourcecode/serializer.py,sha256=7SBJIbpC_Lg0RGWq8jjNbF5TiuZwoP_fi0qhHnzQM8M,124386
50
- sourcecode/spring_event_topology.py,sha256=LvGv5RXtU_O-fVB_OO9eDD2UmZM72Jn2oUHgOo50Qm0,17157
50
+ sourcecode/spring_event_topology.py,sha256=5_ON_21Le5zbG-1GRc5GLIi5HJfy_QjcXLVPC5WeUGQ,18055
51
51
  sourcecode/spring_findings.py,sha256=8V91iHOg9hFgg6tLLl4FSsgrF-dBqOcO2s-K5sD_goA,5417
52
52
  sourcecode/spring_impact.py,sha256=Ohm2k3W4Wts8Kx8Z7DIM-J-cwGtTJBWKFBsX-WkupBQ,32943
53
53
  sourcecode/spring_model.py,sha256=IzMcM5ftw1_EHG3FGUDT7qdAMpo3eqbAE1LRuasfr_4,14739
@@ -94,8 +94,8 @@ sourcecode/telemetry/consent.py,sha256=wLMvGNJeSSyZoNkQXpoUioY6mMv4Qdvuw7S9jAEWn
94
94
  sourcecode/telemetry/events.py,sha256=oEvvulfsv5GIDWG2174gSS6tNB95w38AIYiYeifGKlE,2294
95
95
  sourcecode/telemetry/filters.py,sha256=Asa71oRl7q3Wt_FMwuufIZJFzSYdgRNKS8LHCIyFeYE,4805
96
96
  sourcecode/telemetry/transport.py,sha256=QSslxIwij8YkRWcVvxykODDrkiN_GAAEu3dUP7KIWeE,1651
97
- sourcecode-1.35.25.dist-info/METADATA,sha256=XdPhns9JN4tVct_5ARdKlbsb3w76e9rBFXZ_SI4-VC0,21297
98
- sourcecode-1.35.25.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
- sourcecode-1.35.25.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
- sourcecode-1.35.25.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
- sourcecode-1.35.25.dist-info/RECORD,,
97
+ sourcecode-1.35.26.dist-info/METADATA,sha256=M8Y5dAsVOUGs7ij_YJC26hy4SJtJ_4fFGZZ6NpuabKQ,21297
98
+ sourcecode-1.35.26.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
99
+ sourcecode-1.35.26.dist-info/entry_points.txt,sha256=ex3F9rmbXeyDIoFQHtkEqTsKSaJow8F0LrVu8XfIktQ,57
100
+ sourcecode-1.35.26.dist-info/licenses/LICENSE,sha256=7DdHrU9Z_3e7dSvq4ISijZNjnuHo5NIHNiHDouMQ9JU,10491
101
+ sourcecode-1.35.26.dist-info/RECORD,,