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
core/run_config.py ADDED
@@ -0,0 +1,85 @@
1
+ """
2
+ Load run config from repo .diffsense.yaml or diffsense-ignore.yaml.
3
+ Used as defaults for profile, auto_tune, ci_fail_level, cache, scheduler, rule_quality.
4
+ CLI / main / run_audit override these with explicit args when provided.
5
+ Also resolves pro_rules_path (super rules) for formal DiffSense runs.
6
+ """
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, Any, Optional
10
+
11
+ def _load_yaml(path: str) -> Optional[Dict[str, Any]]:
12
+ if not os.path.isfile(path):
13
+ return None
14
+ try:
15
+ import yaml
16
+ with open(path, "r", encoding="utf-8") as f:
17
+ return yaml.safe_load(f) or {}
18
+ except Exception:
19
+ return None
20
+
21
+ def get_run_config(repo_root: str = ".") -> Dict[str, Any]:
22
+ """
23
+ Load run config from repo_root. Prefer .diffsense.yaml, then diffsense-ignore.yaml.
24
+ Returns dict with keys: profile, auto_tune, ci_fail_level, cache, scheduler, rule_quality.
25
+ Missing keys are absent (caller uses own defaults).
26
+ """
27
+ out: Dict[str, Any] = {}
28
+ for fname in (".diffsense.yaml", "diffsense-ignore.yaml"):
29
+ path = os.path.join(repo_root, fname)
30
+ data = _load_yaml(path)
31
+ if not data:
32
+ continue
33
+ if data.get("profile") is not None:
34
+ out["profile"] = str(data["profile"]).strip() or None
35
+ if data.get("auto_tune") is not None:
36
+ out["auto_tune"] = bool(data["auto_tune"])
37
+ if data.get("ci_fail_level") is not None:
38
+ out["ci_fail_level"] = str(data["ci_fail_level"]).strip() or None
39
+ if data.get("cache") is not None:
40
+ out["cache"] = bool(data["cache"])
41
+ if data.get("scheduler") is not None:
42
+ out["scheduler"] = bool(data["scheduler"])
43
+ if isinstance(data.get("rule_quality"), dict):
44
+ out["rule_quality"] = dict(data["rule_quality"])
45
+ if data.get("pro_rules_path") is not None:
46
+ out["pro_rules_path"] = str(data["pro_rules_path"]).strip() or None
47
+ if isinstance(data.get("dependency_versions"), dict):
48
+ # 用户配置的依赖版本,用于 CVE 规则精确匹配:ecosystem -> package_name -> version
49
+ # 例如: dependency_versions: { npm: { lodash: "4.17.21" }, maven: { "org.apache.tomcat:tomcat-catalina": "9.0.72" } }
50
+ out["dependency_versions"] = {k: dict(v) for k, v in data["dependency_versions"].items() if isinstance(v, dict)}
51
+ break
52
+ return out
53
+
54
+
55
+ def get_pro_rules_path(cwd: Optional[str] = None) -> Optional[str]:
56
+ """
57
+ 解析 PRO 规则(超级规则)目录路径,供正式 audit/main 加载。
58
+ 优先级: 环境变量 DIFFSENSE_PRO_RULES > 仓库 .diffsense.yaml 的 pro_rules_path >
59
+ 相对 diffsense 包上级的 pro-rules(开发/同仓部署)。
60
+ 仅当路径存在时返回,否则返回 None。
61
+ """
62
+ cwd = cwd or os.getcwd()
63
+ # 1. 环境变量
64
+ env_path = os.environ.get("DIFFSENSE_PRO_RULES", "").strip()
65
+ if env_path and os.path.isdir(env_path):
66
+ return os.path.normpath(env_path)
67
+ # 2. 仓库配置
68
+ run_cfg = get_run_config(cwd)
69
+ cfg_path = run_cfg.get("pro_rules_path")
70
+ if cfg_path and os.path.isdir(os.path.normpath(cfg_path)):
71
+ return os.path.normpath(cfg_path)
72
+ if cfg_path:
73
+ abs_cfg = os.path.normpath(os.path.join(cwd, cfg_path))
74
+ if os.path.isdir(abs_cfg):
75
+ return abs_cfg
76
+ # 3. 默认:与 diffsense 包同级的 pro-rules(源码/同仓)
77
+ try:
78
+ import diffsense
79
+ base = Path(diffsense.__file__).resolve().parent.parent
80
+ except Exception:
81
+ base = Path(__file__).resolve().parent.parent.parent
82
+ default = base / "pro-rules"
83
+ if default.is_dir():
84
+ return str(default)
85
+ return None
core/semantic_diff.py ADDED
@@ -0,0 +1,359 @@
1
+ """
2
+ Semantic Analysis Engine for DiffSense
3
+
4
+ Multi-language semantic analysis supporting Java, Go, Python, C++, and JavaScript.
5
+ Uses language-specific parsers and adapters to extract semantic signals.
6
+ """
7
+
8
+ from typing import List, Set, Dict, Any, Tuple, Optional
9
+ from sdk.signal import Signal
10
+
11
+ # Import parsers registry (lazy import to avoid circular dependency)
12
+ def _get_parser_for_file(filename: str):
13
+ try:
14
+ from diffsense.parsers.registry import get_parser_for_file
15
+ return get_parser_for_file(filename)
16
+ except ImportError:
17
+ return None
18
+
19
+
20
+ class SemanticDiff:
21
+ """
22
+ L2 Semantic Analysis Engine (Multi-Language Version).
23
+
24
+ Responsibility: Parse Diff -> Extract Semantic Signals.
25
+ Does NOT execute rules.
26
+
27
+ Supports: Java, Go, Python, C++, JavaScript
28
+ """
29
+
30
+ def __init__(self):
31
+ # Language-specific semantic analysis (currently only Java has deep support)
32
+ # Other languages use parser-level signals
33
+ self._language_extensions = {
34
+ '.java': 'java',
35
+ '.go': 'go',
36
+ '.py': 'python',
37
+ '.pyi': 'python',
38
+ '.cpp': 'cpp',
39
+ '.cc': 'cpp',
40
+ '.cxx': 'cpp',
41
+ '.h': 'cpp',
42
+ '.hpp': 'cpp',
43
+ '.js': 'javascript',
44
+ '.jsx': 'javascript',
45
+ '.ts': 'javascript',
46
+ '.tsx': 'javascript',
47
+ '.mjs': 'javascript',
48
+ '.cjs': 'javascript',
49
+ }
50
+
51
+ # Pagination variables (for Java)
52
+ self._pagination_vars = {"pageNo", "pageSize", "start", "limit", "offset"}
53
+
54
+ # Critical calls (for Java)
55
+ self._critical_calls = {"encode", "decode", "validate", "check", "normalize", "sanitize"}
56
+
57
+ def get_language(self, filename: str) -> str:
58
+ """Detect language from filename extension."""
59
+ ext = '.' + filename.split('.')[-1] if '.' in filename else ''
60
+ return self._language_extensions.get(ext, 'unknown')
61
+
62
+ def detect_signals(self, diff_data: Dict[str, Any]) -> List[Signal]:
63
+ """
64
+ Main Entry Point: Returns semantic signals for all languages.
65
+ """
66
+ signals = []
67
+ file_patches = diff_data.get('file_patches', [])
68
+
69
+ # Fallback if parser isn't upgraded
70
+ if not file_patches and 'raw_diff' in diff_data:
71
+ file_patches = [{'file': 'unknown', 'patch': diff_data['raw_diff']}]
72
+
73
+ # Group files by language
74
+ files_by_language: Dict[str, List[Dict]] = {}
75
+ for entry in file_patches:
76
+ filename = entry.get('file', 'unknown')
77
+ language = self.get_language(filename)
78
+ if language not in files_by_language:
79
+ files_by_language[language] = []
80
+ files_by_language[language].append(entry)
81
+
82
+ # Analyze each language's files
83
+ for language, files in files_by_language.items():
84
+ if language == 'java':
85
+ # Java: Use existing semantic analysis
86
+ java_signals = self._detect_java_signals(files)
87
+ signals.extend(java_signals)
88
+ elif language != 'unknown':
89
+ # Other languages: Use parser-level signals
90
+ parser_signals = self._detect_parser_signals(files, language, diff_data)
91
+ signals.extend(parser_signals)
92
+
93
+ return signals
94
+
95
+ def _detect_java_signals(self, file_patches: List[Dict]) -> List[Signal]:
96
+ """
97
+ Java-specific semantic analysis (deep analysis).
98
+ """
99
+ # Import Java-specific modules lazily to avoid issues
100
+ from javalang.tree import (
101
+ SynchronizedStatement, MethodInvocation, FieldDeclaration,
102
+ MethodDeclaration, LocalVariableDeclaration, VariableDeclarator,
103
+ ForStatement, WhileStatement, DoStatement
104
+ )
105
+ from .change import Change, ChangeKind
106
+ from .knowledge import is_thread_safe, is_lock_type
107
+
108
+ signals = []
109
+
110
+ # Determine Analysis Tier based on Java file count
111
+ if len(file_patches) > 30:
112
+ return [Signal(
113
+ id="meta.large_refactor",
114
+ file="meta",
115
+ action="detected",
116
+ line=None,
117
+ meta={"tier": 3, "language": "java"}
118
+ )]
119
+
120
+ analysis_mode = "deep"
121
+ if 10 < len(file_patches) <= 30:
122
+ analysis_mode = "light"
123
+
124
+ for entry in file_patches:
125
+ filename = entry.get('file', 'unknown')
126
+ patch_content = entry.get('patch', '')
127
+
128
+ if not filename.endswith('.java'):
129
+ continue
130
+
131
+ file_changes = self._detect_changes_in_patch(
132
+ filename, patch_content, mode=analysis_mode
133
+ )
134
+
135
+ for ch in file_changes:
136
+ sig = self._convert_change_to_signal(ch)
137
+ if sig:
138
+ signals.append(sig)
139
+
140
+ return signals
141
+
142
+ def _detect_parser_signals(self, file_patches: List[Dict], language: str, diff_data: Dict) -> List[Signal]:
143
+ """
144
+ Use language-specific parsers to extract signals for non-Java languages.
145
+ """
146
+ signals = []
147
+
148
+ # Get parser for this language
149
+ parser = None
150
+ for entry in file_patches:
151
+ filename = entry.get('file', 'unknown')
152
+ parser = _get_parser_for_file(filename)
153
+ if parser:
154
+ break
155
+
156
+ if not parser:
157
+ return signals
158
+
159
+ # Get diff context
160
+ changes_context = []
161
+ if 'changes' in diff_data:
162
+ changes_context = diff_data['changes']
163
+
164
+ diff_context = {'changes': changes_context}
165
+
166
+ # Parse each file and extract signals
167
+ for entry in file_patches:
168
+ filename = entry.get('file', 'unknown')
169
+ content = entry.get('content', '')
170
+
171
+ if content:
172
+ # Parse the file
173
+ ast_data = parser.parse_file(filename, content)
174
+
175
+ # Extract signals
176
+ if hasattr(parser, 'extract_signals'):
177
+ file_signals = parser.extract_signals(ast_data, diff_context)
178
+ for sig_dict in file_signals:
179
+ signal = Signal(
180
+ id=sig_dict.get('id', 'unknown'),
181
+ file=filename,
182
+ action=sig_dict.get('action', 'unknown'),
183
+ line=sig_dict.get('line'),
184
+ meta=sig_dict.get('meta', {})
185
+ )
186
+ signals.append(signal)
187
+
188
+ return signals
189
+
190
+ def _detect_changes_in_patch(self, filename: str, patch_content: str, mode: str = "deep") -> List[Any]:
191
+ """
192
+ Detect changes in a patch (Java-specific implementation).
193
+ This is the existing logic from the original semantic_diff.py
194
+ """
195
+ # Lazy import to avoid circular dependency
196
+ from javalang import parse as javalang_parse
197
+ from javalang.tree import (
198
+ SynchronizedStatement, MethodInvocation, FieldDeclaration,
199
+ MethodDeclaration, LocalVariableDeclaration, VariableDeclarator,
200
+ ForStatement, WhileStatement, DoStatement
201
+ )
202
+
203
+ changes = []
204
+ added_lines = []
205
+ removed_lines = []
206
+
207
+ for line in patch_content.splitlines():
208
+ if line.startswith('+') and not line.startswith('+++'):
209
+ added_lines.append(line[1:].strip())
210
+ elif line.startswith('-') and not line.startswith('---'):
211
+ removed_lines.append(line[1:].strip())
212
+
213
+ # Analyze removed lines
214
+ if removed_lines:
215
+ self._analyze_snippet_for_changes(
216
+ removed_lines, filename, is_added=False,
217
+ changes=changes, mode=mode
218
+ )
219
+
220
+ # Analyze added lines
221
+ if added_lines:
222
+ self._analyze_snippet_for_changes(
223
+ added_lines, filename, is_added=True,
224
+ changes=changes, mode=mode
225
+ )
226
+
227
+ return changes
228
+
229
+ def _analyze_snippet_for_changes(self, lines: List[str], filename: str, is_added: bool,
230
+ changes: List, mode: str = "deep"):
231
+ """Analyze a code snippet for semantic changes (Java)."""
232
+ from .change import Change, ChangeKind
233
+
234
+ try:
235
+ import javalang
236
+ from javalang.tree import (
237
+ SynchronizedStatement, MethodInvocation, FieldDeclaration,
238
+ MethodDeclaration, LocalVariableDeclaration
239
+ )
240
+ except ImportError:
241
+ return
242
+
243
+ code_snippet = "\n".join(lines)
244
+
245
+ try:
246
+ tokens = list(javalang.tokenizer.tokenize(code_snippet))
247
+ except:
248
+ return
249
+
250
+ token_values = [t.value for t in tokens]
251
+
252
+ # Quick token-based checks
253
+ if "synchronized" in token_values:
254
+ kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
255
+ changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
256
+
257
+ if "volatile" in token_values:
258
+ kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
259
+ changes.append(Change(kind=kind, file=filename, symbol="volatile"))
260
+
261
+ # Check for pattern matches
262
+ for i in range(len(tokens) - 2):
263
+ if (tokens[i].value == "." and
264
+ tokens[i+1].value == "lock" and
265
+ tokens[i+2].value == "("):
266
+ kind = ChangeKind.CALL_ADDED if is_added else ChangeKind.CALL_REMOVED
267
+ changes.append(Change(kind=kind, file=filename, symbol="lock"))
268
+
269
+ if (tokens[i].value == "Thread" and
270
+ tokens[i+1].value == "." and
271
+ tokens[i+2].value == "sleep"):
272
+ kind = ChangeKind.CALL_ADDED if is_added else ChangeKind.CALL_REMOVED
273
+ changes.append(Change(kind=kind, file=filename, symbol="sleep"))
274
+
275
+ # Skip deep analysis in light mode
276
+ if mode == "light":
277
+ return
278
+
279
+ # Deep AST analysis
280
+ wrapper_class = f"class Dummy {{ {code_snippet} }}"
281
+ try:
282
+ tree = javalang_parse.parse(wrapper_class)
283
+ self._analyze_tree_changes(tree, filename, is_added, {}, changes)
284
+ except:
285
+ pass
286
+
287
+ def _analyze_tree_changes(self, tree, filename: str, is_added: bool, var_map: Dict, changes: List):
288
+ """Analyze AST tree for changes (Java)."""
289
+ from .change import Change, ChangeKind
290
+
291
+ try:
292
+ from javalang.tree import (
293
+ SynchronizedStatement, MethodInvocation, FieldDeclaration,
294
+ MethodDeclaration, LocalVariableDeclaration, ForStatement, WhileStatement, DoStatement
295
+ )
296
+ except ImportError:
297
+ return
298
+
299
+ for path, node in tree:
300
+ if isinstance(node, SynchronizedStatement):
301
+ kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
302
+ changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
303
+
304
+ if isinstance(node, MethodDeclaration):
305
+ if 'synchronized' in node.modifiers:
306
+ kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
307
+ changes.append(Change(kind=kind, file=filename, symbol="synchronized"))
308
+
309
+ if isinstance(node, FieldDeclaration):
310
+ if 'volatile' in node.modifiers:
311
+ kind = ChangeKind.MODIFIER_ADDED if is_added else ChangeKind.MODIFIER_REMOVED
312
+ changes.append(Change(kind=kind, file=filename, symbol="volatile"))
313
+
314
+ if node.type:
315
+ for declarator in node.declarators:
316
+ var_map[declarator.name] = node.type.name
317
+
318
+ def _convert_change_to_signal(self, change) -> Optional[Signal]:
319
+ """Convert internal Change to Signal (Java)."""
320
+ from .change import ChangeKind
321
+
322
+ if not hasattr(change, 'kind'):
323
+ return None
324
+
325
+ # Map Change -> Signal ID
326
+ sig_id = None
327
+ if hasattr(change, 'symbol'):
328
+ if change.symbol == "lock" or change.symbol == "unlock":
329
+ sig_id = "runtime.concurrency.lock"
330
+ elif change.symbol == "synchronized":
331
+ sig_id = "runtime.concurrency.synchronized"
332
+ elif change.symbol == "volatile":
333
+ sig_id = "runtime.concurrency.volatile"
334
+ elif change.symbol == "sleep":
335
+ sig_id = "runtime.performance.sleep_added"
336
+
337
+ if not sig_id and change.kind == ChangeKind.TYPE_CHANGED:
338
+ if change.meta.get('downgrade'):
339
+ sig_id = "runtime.concurrency.thread_safety_downgrade"
340
+
341
+ if not sig_id:
342
+ return None
343
+
344
+ # Map ChangeKind -> Action
345
+ action = "changed"
346
+ if change.kind in [ChangeKind.CALL_ADDED, ChangeKind.FIELD_ADDED, ChangeKind.MODIFIER_ADDED]:
347
+ action = "added"
348
+ elif change.kind in [ChangeKind.CALL_REMOVED, ChangeKind.FIELD_REMOVED, ChangeKind.MODIFIER_REMOVED]:
349
+ action = "removed"
350
+ elif change.kind == ChangeKind.TYPE_CHANGED:
351
+ action = "downgrade"
352
+
353
+ return Signal(
354
+ id=sig_id,
355
+ file=change.file,
356
+ action=action,
357
+ line=getattr(change, 'line_no', None),
358
+ meta=getattr(change, 'meta', {})
359
+ )
core/signal_model.py ADDED
@@ -0,0 +1,21 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import Optional, Dict, Any
3
+
4
+ @dataclass
5
+ class Signal:
6
+ id: str
7
+ file: str
8
+ confidence: float = 1.0
9
+ meta: Dict[str, Any] = field(default_factory=dict)
10
+ action: str = "added" # "added" or "removed"
11
+ line: Optional[int] = None
12
+
13
+ def to_dict(self):
14
+ return {
15
+ "signal_id": self.id,
16
+ "file": self.file,
17
+ "confidence": self.confidence,
18
+ "meta": self.meta,
19
+ "action": self.action,
20
+ "line": self.line
21
+ }
@@ -0,0 +1,62 @@
1
+ """
2
+ DiffSense Signal Registry — single source of truth for all signals emitted by semantic analyzers.
3
+ Rules only consume these signals; they do not emit them.
4
+ Used by: docs/signals.md, CLI `diffsense signals`.
5
+ """
6
+ from typing import Dict, List
7
+
8
+ # Grouped by category for docs and CLI output. Order and grouping match docs/signals.md.
9
+ SIGNALS_BY_GROUP: Dict[str, List[str]] = {
10
+ "runtime.concurrency": [
11
+ "runtime.concurrency.synchronized",
12
+ "runtime.concurrency.lock",
13
+ "runtime.concurrency.lock_removed",
14
+ "runtime.concurrency.volatile",
15
+ "runtime.concurrency.volatile_removed",
16
+ "runtime.concurrency.final_removed",
17
+ "runtime.concurrency.concurrent_map",
18
+ "runtime.concurrency.thread_safety_downgrade",
19
+ "runtime.concurrency.static_unsafe_collection",
20
+ "runtime.concurrency.atomic_to_non_atomic_write",
21
+ "runtime.concurrency.threadpool_param_change",
22
+ "runtime.concurrency.threadpool_creation",
23
+ "runtime.concurrency.threadpool_unbounded_queue",
24
+ "runtime.concurrency.executors_factory_risk",
25
+ "runtime.concurrency.future_get_without_timeout",
26
+ "runtime.concurrency.busy_wait_added",
27
+ ],
28
+ "runtime.performance": [
29
+ "runtime.performance.sleep_added",
30
+ ],
31
+ "runtime.resource": [
32
+ "runtime.resource.try_with_resource_removed",
33
+ "runtime.resource.cache_eviction_removed",
34
+ ],
35
+ "runtime.network": [
36
+ "runtime.network.timeout_removed",
37
+ ],
38
+ "runtime.data": [
39
+ "runtime.data.null_check_removed",
40
+ "runtime.data.equals_to_reference_compare",
41
+ ],
42
+ "runtime": [
43
+ "runtime.input_normalization_removed",
44
+ "runtime.collection_mutation_inside_loop",
45
+ ],
46
+ "data": [
47
+ "data.pagination_semantic_change",
48
+ ],
49
+ }
50
+
51
+
52
+ def get_all_signals() -> List[str]:
53
+ """Flat list of all signal IDs, sorted."""
54
+ out = []
55
+ for group in SIGNALS_BY_GROUP.values():
56
+ out.extend(group)
57
+ return sorted(out)
58
+
59
+
60
+ def get_signals_by_group() -> Dict[str, List[str]]:
61
+ """Signals grouped by category (for CLI and docs)."""
62
+ return SIGNALS_BY_GROUP.copy()
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffsense
3
+ Version: 2.2.12
4
+ Summary: MR/PR risk audit: semantic diff + rule engine. Run in CI as image or pip.
5
+ License: Apache-2.0
6
+ Requires-Python: >=3.10
7
+ License-File: LICENSE
8
+ Requires-Dist: PyYAML
9
+ Requires-Dist: PyGithub
10
+ Requires-Dist: python-gitlab
11
+ Requires-Dist: requests
12
+ Requires-Dist: javalang
13
+ Requires-Dist: typer>=0.9.0
14
+ Provides-Extra: dev
15
+ Requires-Dist: pytest; extra == "dev"
16
+ Provides-Extra: mcp
17
+ Requires-Dist: mcp>=1.0.0; extra == "mcp"
18
+ Dynamic: license-file
@@ -0,0 +1,58 @@
1
+ banner.py,sha256=JHRCec0w4eGt68IBHJ0y5df4OsuzPiXmWf-FwqJJCA4,1907
2
+ cli.py,sha256=Lcc9bfOTeTgHBNvJtIMAO_3hBqS8M9luIDPAfRhv4vI,27358
3
+ main.py,sha256=XZsJh2TUzEEOi5PkgeYTomg_dYP2Yvoj1f630msGCdc,13768
4
+ run_audit.py,sha256=DpeeNj0B8zIQvM6gtjaZoLUKSrkcJKORsP76-61s7Wk,21909
5
+ adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ adapters/base.py,sha256=8WG6uLyvpexEqyynNr7X-WuRyDzE5jOxUV9qdIH7irM,673
7
+ adapters/github_adapter.py,sha256=MgM0i9oGtB746CuK0c4BjrtUux0S8jWVCfes0z7-Dpo,6534
8
+ adapters/gitlab_adapter.py,sha256=g5OWwVLocb2UNPRxVZMZytGsgTyYI7V-8P_0hV_ZEZ0,8636
9
+ adapters/local_adapter.py,sha256=IIYcAWfl7WJEzDIBCC9JGJ1fjGkAUiWcqkugGUeqSLY,5074
10
+ config/__init__.py,sha256=3udbRIyrQB2JnFoFWqL0IAyweo36J5SsJOrMEXMAsjI,50
11
+ config/rules.yaml,sha256=lUGEA-S6hD8XFdCTiS_rZxWb9_xlsZKBtO2h3uPkov8,13309
12
+ core/__init__.py,sha256=wM1ShcscRZ9jB9gwnHsYYPMG4Mp_41FXmmFgo1_eP84,7860
13
+ core/ast_detector.py,sha256=fH7aNgU3oESFgy2FWiaMyVXTVUXeA0LIrAv-2TzBCP4,39590
14
+ core/change.py,sha256=MMQ2M0XO3NmjAQ3aTR9Xm5g651AdjqlUo-LGAlY9PT0,1364
15
+ core/composer.py,sha256=1tN2OEIz501JzPsuGb3PDStgjX_I1ZBxXktDs5iHEk4,3395
16
+ core/evaluator.py,sha256=VqSzezO2CTzIWLfhdFEXtOsm-pZtw_kEQHE1nRUG9Aw,528
17
+ core/ignore_manager.py,sha256=KR9Ec475CcnwAjyXciHmWLl3KZA8OSnvspiVmrVFdXM,2643
18
+ core/knowledge.py,sha256=83KNJu0Pq_JFEVv_J-eepgBaGyYwkA53AefOCHnxhb0,1920
19
+ core/parser.py,sha256=FuU2oF3kMWLNm0hKf45wBDG-r5I4531WDO0CwcTFHqk,6570
20
+ core/parser_manager.py,sha256=QAgU84j_mpqkDXXnG95CbjIroeoAGPMRHsJLQqFMB_Q,3954
21
+ core/quality_manager.py,sha256=GCpzJc643CwpmFQXvfQKs-WIf2fD5hBkmlPEwBPXgBM,4650
22
+ core/renderer.py,sha256=ZMRvTyA08o1sp7yQdJ55-q7w85UdMF-ejgWPHQNa6X4,7661
23
+ core/rule_base.py,sha256=JXaRAjYNf2OVHT5g-5jp6pn49Is0qfq9VW2FiM5yq8g,2792
24
+ core/rule_runtime.py,sha256=pvjPvWwryAAuVVkzMTnIb9fmJ_bQlf1tGqiZDrC1ET4,3583
25
+ core/rules.py,sha256=qcYy0BeVxgdMROz4Y-P72UQ0WwuH36qe_vjJe_mwOSU,30502
26
+ core/run_config.py,sha256=GnVcpVKfJRl3dov3rz2VtxQj2WmeEaIwdBBxJB3Lo50,3730
27
+ core/semantic_diff.py,sha256=OjTc7PBasD8dHQLqpVQ85fV2Q6e0iD7FaoyBnA61YKI,13894
28
+ core/signal_model.py,sha256=4K1yP1i8shMUj0c8Old8bWsrJb2_e5AulqPk88wlbpY,555
29
+ core/signals_registry.py,sha256=BzQzGZ7OMNvZpt33YKiIpbZ1E319xwXB5uXgVpM6l1E,2152
30
+ diffsense-2.2.12.dist-info/licenses/LICENSE,sha256=psuoW8kuDP96RQsdhzwOqi6fyWv0ct8CR6Jr7He_P_k,10173
31
+ diffsense_mcp/__init__.py,sha256=Vf1bx7eiubdiOPap5ZGtDPLdgef6gSXHwyf8JAfRfOc,23
32
+ diffsense_mcp/launcher.py,sha256=bYBl04TdN_A1xSizSlMvsyMDEHqKg7WNt-_PeKmN_HA,721
33
+ diffsense_mcp/server.py,sha256=LvyluHKcVy8hkU5rrbdu6sLi7ZJeJObQP7VOreJB4Y4,20080
34
+ governance/lifecycle.py,sha256=tPTbtjlZ3-nAeN7ZBXy-NSMERh90I_foPitVHHS6gmE,1643
35
+ rules/__init__.py,sha256=Zl9rebPBEdEmqgDW4w9c27HH0VqdbECf3ljRtVIF9jI,6653
36
+ rules/api_compatibility.py,sha256=3XjPH31-pLwawia3eKNECi995KB9HDgMB1U6g5PVHSI,10812
37
+ rules/collection_handling.py,sha256=5QCbHaeeQgV3G05oYnPokD8v1w5XS30wayOFT-hcSqE,10815
38
+ rules/concurrency.py,sha256=eYJiZkwB-iFoUa6W_QO-0cdRGHvtqOH2gTY8tir2swQ,6965
39
+ rules/concurrency_adapter.py,sha256=H3ykRPRy-B9zPatR6Iq8V1sKvqXB3rG1F1NqJYginr0,8285
40
+ rules/cross_language_adapter.py,sha256=hJ7ASYaGfi0Xt8VNa31icn-riFG0Xm-5yUokEnQxL9o,16016
41
+ rules/exception_handling.py,sha256=ewRNqq5abkl7RvS4Upsr1DpOZ2AYuVgS-R-k-iVc8Fg,9851
42
+ rules/go_rules.py,sha256=2bSOwNncon9Os2V3CobPxDMlhdeoyZO1HJ9RFF6TWb0,13100
43
+ rules/null_safety.py,sha256=EozoR8ARWfBB_ygzj_fwjnZxf_CPv42B0YntJCg5eLQ,9500
44
+ rules/resource_management.py,sha256=PJ9KVfNvEcJ2AMJ2pWkYoBEGTSA0KuvZeAJqtyfi7sY,7140
45
+ rules/yaml_adapter.py,sha256=tdXc31Fg0ksBnGYfIMgaMHs8H9X-aObEY_QX9ZRKc0k,7022
46
+ sdk/cpp_adapter.py,sha256=28F0x1KQpsrwlK66mNf10s-HOdZFP-cOHs9GG2psIjE,8002
47
+ sdk/go_adapter.py,sha256=mpT8otbYYgJMQj-Uxfr_Vhl44GOW6Ylndrl3RoHtjG0,6752
48
+ sdk/java_adapter.py,sha256=ykomerEDfSXUrE9yyCnoUmfNGWKCBSdLEZaQv3JjF5I,7297
49
+ sdk/javascript_adapter.py,sha256=C3en-rdGdqrFYPlDPgSsR7EI-Jt_cdumrYWi2zV8vFo,7651
50
+ sdk/language_adapter.py,sha256=iXZl-V8bkEXN0_uDPYafYHMjZRA_ZulNgh3-mcW9bV8,10210
51
+ sdk/python_adapter.py,sha256=6CpceOA4fjli5Vlmq2IC7i0uNAEB8lk8SrD6aFLzedo,6889
52
+ sdk/rule.py,sha256=OpFDygxWJbzHP3OGUOxiqhwZphHc_hgqHZdM-pL2HiA,1840
53
+ sdk/signal.py,sha256=9Q22WR8rAgAqjfNps0GQT7wA5Sfh-Xzhlv2rgl-o0gQ,549
54
+ diffsense-2.2.12.dist-info/METADATA,sha256=7FU5H830pippzhvgU4zb94a11VxGo8Ylz-JKMIRUWAg,496
55
+ diffsense-2.2.12.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
56
+ diffsense-2.2.12.dist-info/entry_points.txt,sha256=1gDOb00JZWCulfon24mvvInZabeB2tb3aBz5j7DR4Xg,80
57
+ diffsense-2.2.12.dist-info/top_level.txt,sha256=QnVsGcF6thk9THgrcaD0SVJak5b1gw-fBe7aMvp47FE,82
58
+ diffsense-2.2.12.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ diffsense = cli:app
3
+ diffsense-mcp = diffsense_mcp.server:main