diffsense 2.2.12__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.
Files changed (58) hide show
  1. adapters/__init__.py +0 -0
  2. adapters/base.py +27 -0
  3. adapters/github_adapter.py +164 -0
  4. adapters/gitlab_adapter.py +207 -0
  5. adapters/local_adapter.py +136 -0
  6. banner.py +71 -0
  7. cli.py +606 -0
  8. config/__init__.py +1 -0
  9. config/rules.yaml +371 -0
  10. core/__init__.py +235 -0
  11. core/ast_detector.py +853 -0
  12. core/change.py +46 -0
  13. core/composer.py +93 -0
  14. core/evaluator.py +15 -0
  15. core/ignore_manager.py +71 -0
  16. core/knowledge.py +77 -0
  17. core/parser.py +181 -0
  18. core/parser_manager.py +104 -0
  19. core/quality_manager.py +117 -0
  20. core/renderer.py +197 -0
  21. core/rule_base.py +98 -0
  22. core/rule_runtime.py +103 -0
  23. core/rules.py +718 -0
  24. core/run_config.py +85 -0
  25. core/semantic_diff.py +359 -0
  26. core/signal_model.py +21 -0
  27. core/signals_registry.py +62 -0
  28. diffsense-2.2.12.dist-info/METADATA +18 -0
  29. diffsense-2.2.12.dist-info/RECORD +58 -0
  30. diffsense-2.2.12.dist-info/WHEEL +5 -0
  31. diffsense-2.2.12.dist-info/entry_points.txt +3 -0
  32. diffsense-2.2.12.dist-info/licenses/LICENSE +176 -0
  33. diffsense-2.2.12.dist-info/top_level.txt +11 -0
  34. diffsense_mcp/__init__.py +1 -0
  35. diffsense_mcp/launcher.py +28 -0
  36. diffsense_mcp/server.py +687 -0
  37. governance/lifecycle.py +54 -0
  38. main.py +318 -0
  39. rules/__init__.py +246 -0
  40. rules/api_compatibility.py +372 -0
  41. rules/collection_handling.py +349 -0
  42. rules/concurrency.py +194 -0
  43. rules/concurrency_adapter.py +250 -0
  44. rules/cross_language_adapter.py +444 -0
  45. rules/exception_handling.py +320 -0
  46. rules/go_rules.py +401 -0
  47. rules/null_safety.py +301 -0
  48. rules/resource_management.py +222 -0
  49. rules/yaml_adapter.py +195 -0
  50. run_audit.py +478 -0
  51. sdk/cpp_adapter.py +238 -0
  52. sdk/go_adapter.py +199 -0
  53. sdk/java_adapter.py +199 -0
  54. sdk/javascript_adapter.py +229 -0
  55. sdk/language_adapter.py +313 -0
  56. sdk/python_adapter.py +195 -0
  57. sdk/rule.py +63 -0
  58. sdk/signal.py +14 -0
@@ -0,0 +1,117 @@
1
+ import os
2
+ import json
3
+ import time
4
+ from typing import Dict, Any, Tuple, List
5
+
6
+ class RuleQualityManager:
7
+ def __init__(self, path: str, auto_tune: bool, degrade_threshold: float, disable_threshold: float, min_samples: int):
8
+ self.path = path
9
+ self.auto_tune = auto_tune
10
+ self.degrade_threshold = degrade_threshold
11
+ self.disable_threshold = disable_threshold
12
+ self.min_samples = min_samples
13
+ self.data = self._load()
14
+
15
+ def _load(self) -> Dict[str, Any]:
16
+ if not os.path.exists(self.path):
17
+ return {"rules": {}}
18
+ try:
19
+ with open(self.path, "r", encoding="utf-8") as f:
20
+ data = json.load(f)
21
+ if isinstance(data, dict) and isinstance(data.get("rules"), dict):
22
+ return data
23
+ except Exception:
24
+ return {"rules": {}}
25
+ return {"rules": {}}
26
+
27
+ def persist(self) -> None:
28
+ self.data["updated_at"] = int(time.time())
29
+ try:
30
+ with open(self.path, "w", encoding="utf-8") as f:
31
+ json.dump(self.data, f, ensure_ascii=False, indent=2)
32
+ except Exception:
33
+ pass
34
+
35
+ def get_metrics(self) -> Dict[str, Any]:
36
+ return self.data.get("rules", {})
37
+
38
+ def _entry(self, rule_id: str) -> Dict[str, Any]:
39
+ rules = self.data.setdefault("rules", {})
40
+ entry = rules.get(rule_id)
41
+ if not isinstance(entry, dict):
42
+ entry = {"hits": 0, "confirmed": 0, "false_positive": 0, "precision": 1.0}
43
+ rules[rule_id] = entry
44
+ if "precision" not in entry:
45
+ entry["precision"] = self._precision(entry)
46
+ return entry
47
+
48
+ def _precision(self, entry: Dict[str, Any]) -> float:
49
+ hits = int(entry.get("hits", 0))
50
+ confirmed = int(entry.get("confirmed", 0))
51
+ return (confirmed / hits) if hits else 1.0
52
+
53
+ def record_hit(self, rule_id: str) -> Dict[str, Any]:
54
+ entry = self._entry(rule_id)
55
+ entry["hits"] = int(entry.get("hits", 0)) + 1
56
+ entry["confirmed"] = int(entry.get("confirmed", 0)) + 1
57
+ entry["precision"] = self._precision(entry)
58
+ return entry
59
+
60
+ def record_false_positive(self, rule_id: str) -> Dict[str, Any]:
61
+ entry = self._entry(rule_id)
62
+ entry["hits"] = int(entry.get("hits", 0)) + 1
63
+ entry["false_positive"] = int(entry.get("false_positive", 0)) + 1
64
+ entry["precision"] = self._precision(entry)
65
+ return entry
66
+
67
+ def status(self, rule_id: str) -> Tuple[str, float, int]:
68
+ entry = self._entry(rule_id)
69
+ hits = int(entry.get("hits", 0))
70
+ precision = float(entry.get("precision", 1.0))
71
+ if hits < self.min_samples:
72
+ return "insufficient", precision, hits
73
+ if precision < self.disable_threshold:
74
+ return "disabled", precision, hits
75
+ if precision < self.degrade_threshold:
76
+ return "degraded", precision, hits
77
+ return "normal", precision, hits
78
+
79
+ def should_skip(self, rule_id: str) -> bool:
80
+ """
81
+ [Architecture Principle Violation] NEVER automatically skip a rule based on quality.
82
+ Decision must be human-led.
83
+ """
84
+ return False
85
+
86
+ def adjust_severity(self, severity: str, rule_id: str) -> str:
87
+ """
88
+ [Architecture Principle Violation] NEVER automatically downgrade severity.
89
+ Severity defines risk semantics, not frequency.
90
+ """
91
+ return severity
92
+
93
+ def warnings(self) -> List[Dict[str, Any]]:
94
+ rows = []
95
+ for rule_id, entry in self.get_metrics().items():
96
+ if not isinstance(entry, dict):
97
+ continue
98
+ status, precision, hits = self.status(rule_id)
99
+ if status in ["degraded", "disabled"]:
100
+ rows.append({
101
+ "rule_id": rule_id,
102
+ "precision": precision,
103
+ "hits": hits,
104
+ "false_positive": entry.get("false_positive", 0),
105
+ "confirmed": entry.get("confirmed", 0),
106
+ "status": status
107
+ })
108
+ return sorted(rows, key=lambda r: (r["status"], r["precision"]))
109
+
110
+ def update_report(self, metrics: Dict[str, Dict[str, Any]], confidences: Dict[str, float]) -> None:
111
+ for rule_id, m in metrics.items():
112
+ entry = self._entry(rule_id)
113
+ calls = int(m.get("calls", 0))
114
+ time_ns = int(m.get("time_ns", 0))
115
+ avg_time_ms = (time_ns / 1_000_000 / calls) if calls else 0.0
116
+ entry["avg_time_ms"] = avg_time_ms
117
+ entry["confidence"] = float(confidences.get(rule_id, entry.get("confidence", 1.0)))
core/renderer.py ADDED
@@ -0,0 +1,197 @@
1
+ from typing import Dict, Any
2
+ import html as _html
3
+
4
+ class MarkdownRenderer:
5
+ def render(self, result: Dict[str, Any]) -> str:
6
+ """
7
+ Renders the audit result into a Markdown string.
8
+ """
9
+ review_level = result.get("review_level", "unknown").capitalize()
10
+ details = result.get("details", [])
11
+
12
+ lines = []
13
+
14
+ if review_level in ["Elevated", "Critical"]:
15
+ lines.append(f"# 🚨 DiffSense Risk Signal: {review_level}")
16
+ else:
17
+ lines.append(f"# ✅ DiffSense Audit: {review_level}")
18
+ lines.append("")
19
+
20
+ if not details:
21
+ lines.append("No warnings detected.")
22
+ rule_stats = (result.get("_metrics") or {}).get("rule_stats", {})
23
+ total_rules = rule_stats.get("total_rules", 0)
24
+ executed_count = rule_stats.get("executed_count", 0)
25
+ if total_rules or executed_count:
26
+ lines.append("")
27
+ lines.append(f"Rules executed: {executed_count} / {total_rules}")
28
+ return "\n".join(lines)
29
+
30
+ severity_rank = {
31
+ "critical": 0,
32
+ "high": 1,
33
+ "medium": 2,
34
+ "low": 3,
35
+ "unknown": 4
36
+ }
37
+
38
+ grouped = {}
39
+ for d in details:
40
+ file_path = d.get("file") or d.get("matched_file") or "unknown"
41
+ rule_id = d.get("rule_id") or d.get("id") or "unknown"
42
+ severity = (d.get("severity") or "unknown").lower()
43
+ impact = d.get("impact") or "unknown"
44
+ rationale = d.get("rationale") or ""
45
+ grouped.setdefault(file_path, []).append({
46
+ "rule_id": rule_id,
47
+ "severity": severity,
48
+ "impact": impact,
49
+ "rationale": rationale
50
+ })
51
+
52
+ # Print risky files to stderr for CI logs
53
+ import sys
54
+ sys.stderr.write("\n" + "="*40 + "\n")
55
+ sys.stderr.write("🔍 DiffSense Risk Files\n")
56
+ sys.stderr.write("="*40 + "\n")
57
+ for file_path in sorted(grouped.keys()):
58
+ if file_path != "unknown":
59
+ issues_count = len(grouped[file_path])
60
+ max_severity = min(grouped[file_path], key=lambda x: severity_rank.get(x["severity"], 4))["severity"]
61
+ sys.stderr.write(f" 📁 {file_path} ({issues_count} issue(s), severity: {max_severity.upper()})\n")
62
+ sys.stderr.write("="*40 + "\n\n")
63
+
64
+ lines.append("## ⚠️ Warnings by File")
65
+ for file_path in sorted(grouped.keys()):
66
+ lines.append("")
67
+ lines.append(f"### `{file_path}`")
68
+ for item in sorted(grouped[file_path], key=lambda x: severity_rank.get(x["severity"], 4)):
69
+ sev_label = item["severity"].upper() if item["severity"] else "UNKNOWN"
70
+ lines.append(f"- **{sev_label}** `{item['rule_id']}` ({item['impact']})")
71
+ if item["rationale"]:
72
+ lines.append(f" - {item['rationale']}")
73
+
74
+ if review_level in ["Elevated", "Critical"]:
75
+ lines.append("")
76
+ lines.append("---")
77
+ lines.append("**Required action:**")
78
+ lines.append("This is a risk signal, not a block.")
79
+ lines.append("")
80
+ lines.append("👉 **Approve this PR** OR **React with 👍** to this comment, then **Re-run this job** to pass.")
81
+
82
+ rule_stats = (result.get("_metrics") or {}).get("rule_stats", {})
83
+ total_rules = rule_stats.get("total_rules", 0)
84
+ executed_count = rule_stats.get("executed_count", 0)
85
+ if total_rules or executed_count:
86
+ lines.append("")
87
+ lines.append(f"Rules executed: {executed_count} / {total_rules}")
88
+ return "\n".join(lines)
89
+
90
+ class HtmlRenderer:
91
+ def render(self, result: Dict[str, Any]) -> str:
92
+ review_level = result.get("review_level", "unknown")
93
+ details = result.get("details", [])
94
+ metrics = result.get("_metrics", {})
95
+ rule_metrics = result.get("_metrics", {})
96
+ rule_quality = result.get("_rule_quality", {})
97
+
98
+ def esc(value: Any) -> str:
99
+ return _html.escape(str(value))
100
+
101
+ rows = []
102
+ for d in details:
103
+ rows.append(
104
+ "<tr>"
105
+ f"<td>{esc(d.get('rule_id', ''))}</td>"
106
+ f"<td>{esc(d.get('severity', ''))}</td>"
107
+ f"<td>{esc(d.get('file', ''))}</td>"
108
+ f"<td>{esc(d.get('impact', ''))}</td>"
109
+ f"<td>{esc(d.get('rationale', ''))}</td>"
110
+ f"<td>{esc(d.get('precision', ''))}</td>"
111
+ f"<td>{esc(d.get('quality_status', ''))}</td>"
112
+ "</tr>"
113
+ )
114
+ detail_table = "\n".join(rows) if rows else "<tr><td colspan='7'>No warnings detected.</td></tr>"
115
+
116
+ cache = metrics.get("cache", {})
117
+ diff_cache = cache.get("diff", {})
118
+ ast_cache = cache.get("ast", {})
119
+ d_total = diff_cache.get("hits", 0) + diff_cache.get("misses", 0)
120
+ a_total = ast_cache.get("hits", 0) + ast_cache.get("misses", 0)
121
+ d_rate = (diff_cache.get("hits", 0) / d_total * 100) if d_total else 0
122
+ a_rate = (ast_cache.get("hits", 0) / a_total * 100) if a_total else 0
123
+ rule_stats = metrics.get("rule_stats", {})
124
+ total_rules = rule_stats.get("total_rules", 0)
125
+ executed_count = rule_stats.get("executed_count", 0)
126
+ exec_pct = (executed_count / total_rules * 100) if total_rules else 0
127
+ rules_executed_line = f"<div>Rules executed: {executed_count} / {total_rules} ({exec_pct:.0f}%)</div>"
128
+
129
+ rule_rows = []
130
+ for rule_id, m in rule_metrics.items():
131
+ if rule_id in ("cache", "rule_stats"):
132
+ continue
133
+ time_ms = (m.get("time_ns", 0) / 1_000_000) if isinstance(m, dict) else 0
134
+ hits = m.get("hits", 0) if isinstance(m, dict) else 0
135
+ ignores = m.get("ignores", 0) if isinstance(m, dict) else 0
136
+ errors = m.get("errors", 0) if isinstance(m, dict) else 0
137
+ precision = ""
138
+ q = rule_quality.get(rule_id)
139
+ if isinstance(q, dict):
140
+ precision = q.get("precision", "")
141
+ rule_rows.append(
142
+ "<tr>"
143
+ f"<td>{esc(rule_id)}</td>"
144
+ f"<td>{esc(hits)}</td>"
145
+ f"<td>{esc(ignores)}</td>"
146
+ f"<td>{esc(errors)}</td>"
147
+ f"<td>{esc(time_ms)}</td>"
148
+ f"<td>{esc(precision)}</td>"
149
+ "</tr>"
150
+ )
151
+ rule_table = "\n".join(rule_rows) if rule_rows else "<tr><td colspan='6'>No rule metrics.</td></tr>"
152
+
153
+ return f"""<!DOCTYPE html>
154
+ <html>
155
+ <head>
156
+ <meta charset="utf-8">
157
+ <title>DiffSense Report</title>
158
+ <style>
159
+ body{{font-family:Arial,sans-serif;margin:20px}}
160
+ table{{border-collapse:collapse;width:100%}}
161
+ th,td{{border:1px solid #ddd;padding:8px;text-align:left}}
162
+ th{{background:#f4f4f4}}
163
+ .summary{{margin-bottom:16px}}
164
+ </style>
165
+ </head>
166
+ <body>
167
+ <h1>DiffSense Report</h1>
168
+ <div class="summary">
169
+ <div>Review Level: {esc(review_level)}</div>
170
+ <div>Diff Cache Hit: {d_rate:.1f}% ({diff_cache.get("hits", 0)}/{d_total})</div>
171
+ <div>AST Cache Hit: {a_rate:.1f}% ({ast_cache.get("hits", 0)}/{a_total})</div>
172
+ {rules_executed_line}
173
+ </div>
174
+ <h2>Findings</h2>
175
+ <table>
176
+ <thead>
177
+ <tr>
178
+ <th>Rule</th><th>Severity</th><th>File</th><th>Impact</th><th>Rationale</th><th>Precision</th><th>Quality</th>
179
+ </tr>
180
+ </thead>
181
+ <tbody>
182
+ {detail_table}
183
+ </tbody>
184
+ </table>
185
+ <h2>Rule Metrics</h2>
186
+ <table>
187
+ <thead>
188
+ <tr>
189
+ <th>Rule</th><th>Hits</th><th>Ignores</th><th>Errors</th><th>Time(ms)</th><th>Precision</th>
190
+ </tr>
191
+ </thead>
192
+ <tbody>
193
+ {rule_table}
194
+ </tbody>
195
+ </table>
196
+ </body>
197
+ </html>"""
core/rule_base.py ADDED
@@ -0,0 +1,98 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Any, Optional, List
3
+
4
+ class Rule(ABC):
5
+ """
6
+ Abstract Base Class for all DiffSense Rules.
7
+ This defines the Plugin Interface (SDK).
8
+ """
9
+
10
+ @property
11
+ @abstractmethod
12
+ def id(self) -> str:
13
+ """Unique Rule ID (e.g., 'runtime.concurrency.lock_removed')"""
14
+ pass
15
+
16
+ @property
17
+ @abstractmethod
18
+ def severity(self) -> str:
19
+ """Severity level: critical, high, medium, low"""
20
+ pass
21
+
22
+ @property
23
+ @abstractmethod
24
+ def impact(self) -> str:
25
+ """Impact dimension: security, runtime, data, maintenance"""
26
+ pass
27
+
28
+ @property
29
+ @abstractmethod
30
+ def rationale(self) -> str:
31
+ """Explanation of why this rule exists and what risk it prevents"""
32
+ pass
33
+
34
+ @property
35
+ def title(self) -> str:
36
+ """Human-readable title for the rule (optional, defaults to rule id)"""
37
+ return self.id
38
+
39
+ # Optional metadata (defaults for built-in rules; YamlRule overrides from YAML)
40
+ @property
41
+ def category(self) -> str:
42
+ """Rule category: concurrency, performance, reliability, security, general"""
43
+ return "general"
44
+
45
+ @property
46
+ def confidence(self) -> float:
47
+ """Confidence score 0.0-1.0"""
48
+ return 1.0
49
+
50
+ @property
51
+ def tags(self) -> List[str]:
52
+ """Optional tags for filtering"""
53
+ return []
54
+
55
+ @property
56
+ def enabled(self) -> bool:
57
+ """Whether this rule is enabled (engine skips if False)"""
58
+ return True
59
+
60
+ @property
61
+ def language(self) -> str:
62
+ """Language scope: * for all, or java, go, js, etc."""
63
+ return "*"
64
+
65
+ @property
66
+ def scope(self) -> str:
67
+ """File scope pattern (e.g. ** or **/core/**)"""
68
+ return "**"
69
+
70
+ @property
71
+ def rule_type(self) -> str:
72
+ """Rule type: regression (depends on diff history) or absolute (context-independent)"""
73
+ return "absolute"
74
+
75
+ @property
76
+ def is_blocking(self) -> bool:
77
+ """If True, any hit will force a 'critical' review level and suggested 'block_pr' action"""
78
+ return False
79
+
80
+ @property
81
+ def status(self) -> str:
82
+ """Lifecycle status: experimental, beta, stable, deprecated, disabled. Engine skips disabled."""
83
+ return "stable"
84
+
85
+ @abstractmethod
86
+ def evaluate(self, diff_data: Dict[str, Any], ast_signals: List[Any]) -> Optional[Dict[str, Any]]:
87
+ """
88
+ Execute the rule logic against the diff and signals.
89
+
90
+ Args:
91
+ diff_data: The raw diff parsing result
92
+ ast_signals: List of AST signals detected by ASTDetector
93
+
94
+ Returns:
95
+ Dict with match details (must contain 'file' key) if matched,
96
+ None if not matched.
97
+ """
98
+ pass
core/rule_runtime.py ADDED
@@ -0,0 +1,103 @@
1
+ """
2
+ Legacy rule runtime. Prefer core.rules.RuleEngine as the single entry point;
3
+ RuleEngine now integrates LifecycleManager, profile, directory/entry_point loading.
4
+ """
5
+ import yaml
6
+ import time
7
+ from typing import Dict, List, Any, Optional
8
+ from sdk.rule import BaseRule
9
+ from sdk.signal import Signal
10
+ from governance.lifecycle import LifecycleManager
11
+ from rules.concurrency import (
12
+ ThreadPoolSemanticChangeRule,
13
+ ConcurrencyRegressionRule,
14
+ ThreadSafetyRemovalRule,
15
+ LatchMisuseRule
16
+ )
17
+ from rules.yaml_adapter import YamlRule
18
+
19
+ class RuleRuntime:
20
+ """
21
+ The orchestrator for executing rules.
22
+ Handles Lifecycle, Metrics, Suppression, and Feedback.
23
+ """
24
+ def __init__(self, rules_path: str, config: Dict[str, Any] = None):
25
+ self.rules: List[BaseRule] = []
26
+ self.metrics: Dict[str, Dict[str, Any]] = {}
27
+ self.lifecycle = LifecycleManager(config)
28
+
29
+ # 1. Register Built-in Rules (Plugins)
30
+ self._register_builtins()
31
+
32
+ # 2. Load YAML Rules (Plugins)
33
+ self._load_yaml_rules(rules_path)
34
+
35
+ def _register_builtins(self):
36
+ """
37
+ Registers core rules. In a real OS, this would be a dynamic plugin loader.
38
+ """
39
+ self.rules.append(ThreadPoolSemanticChangeRule())
40
+ self.rules.append(ConcurrencyRegressionRule())
41
+ self.rules.append(ThreadSafetyRemovalRule())
42
+ self.rules.append(LatchMisuseRule())
43
+
44
+ def _load_yaml_rules(self, path: str):
45
+ try:
46
+ with open(path, 'r', encoding='utf-8') as f:
47
+ data = yaml.safe_load(f) or {}
48
+ raw_rules = data.get('rules', [])
49
+ for r in raw_rules:
50
+ self.rules.append(YamlRule(r))
51
+ except FileNotFoundError:
52
+ pass
53
+
54
+ def execute(self, diff_data: Dict[str, Any], signals: List[Signal]) -> List[Dict[str, Any]]:
55
+ """
56
+ Main execution pipeline.
57
+ """
58
+ findings = []
59
+
60
+ for rule in self.rules:
61
+ # 1. Lifecycle Check
62
+ if not self.lifecycle.should_run(rule):
63
+ continue
64
+
65
+ rule_id = rule.id
66
+ if rule_id not in self.metrics:
67
+ self.metrics[rule_id] = {"calls": 0, "hits": 0, "time_ns": 0, "errors": 0}
68
+
69
+ self.metrics[rule_id]["calls"] += 1
70
+
71
+ start_time = time.time_ns()
72
+ match_details = None
73
+
74
+ try:
75
+ # 2. Execute Rule
76
+ match_details = rule.evaluate(diff_data, signals)
77
+ except Exception:
78
+ self.metrics[rule_id]["errors"] += 1
79
+ finally:
80
+ duration = time.time_ns() - start_time
81
+ self.metrics[rule_id]["time_ns"] += duration
82
+
83
+ if match_details:
84
+ # 3. Suppress Check (TODO)
85
+ # if self.suppress.is_suppressed(rule_id, match_details): continue
86
+
87
+ self.metrics[rule_id]["hits"] += 1
88
+
89
+ # 4. Severity Adjustment
90
+ severity = self.lifecycle.adjust_severity(rule, rule.severity)
91
+
92
+ findings.append({
93
+ "id": rule.id,
94
+ "severity": severity,
95
+ "impact": rule.impact,
96
+ "rationale": rule.rationale,
97
+ "matched_file": match_details.get('file', 'unknown')
98
+ })
99
+
100
+ return findings
101
+
102
+ def get_metrics(self) -> Dict[str, Any]:
103
+ return self.metrics