pybinaryguard 1.0.0__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 (91) hide show
  1. pybinaryguard/__init__.py +78 -0
  2. pybinaryguard/__main__.py +7 -0
  3. pybinaryguard/_compat/__init__.py +0 -0
  4. pybinaryguard/agent/__init__.py +63 -0
  5. pybinaryguard/agent/guard.py +232 -0
  6. pybinaryguard/agent/recommender.py +283 -0
  7. pybinaryguard/agent/schema.py +200 -0
  8. pybinaryguard/agent/simulator.py +430 -0
  9. pybinaryguard/agent/tool_interface.py +474 -0
  10. pybinaryguard/analyzers/__init__.py +209 -0
  11. pybinaryguard/analyzers/base.py +40 -0
  12. pybinaryguard/analyzers/dependency_analyzer.py +336 -0
  13. pybinaryguard/analyzers/elf_analyzer.py +754 -0
  14. pybinaryguard/analyzers/symbol_analyzer.py +280 -0
  15. pybinaryguard/analyzers/wheel_analyzer.py +308 -0
  16. pybinaryguard/cli/__init__.py +5 -0
  17. pybinaryguard/cli/commands.py +414 -0
  18. pybinaryguard/cli/formatters.py +720 -0
  19. pybinaryguard/cli/main.py +250 -0
  20. pybinaryguard/diagnostics/__init__.py +13 -0
  21. pybinaryguard/diagnostics/explainer.py +356 -0
  22. pybinaryguard/diagnostics/findings.py +146 -0
  23. pybinaryguard/diagnostics/suggestions.py +508 -0
  24. pybinaryguard/frameworks/__init__.py +20 -0
  25. pybinaryguard/frameworks/onnxruntime.py +214 -0
  26. pybinaryguard/frameworks/pytorch.py +223 -0
  27. pybinaryguard/frameworks/tensorflow.py +266 -0
  28. pybinaryguard/frameworks/tensorrt.py +189 -0
  29. pybinaryguard/models/__init__.py +19 -0
  30. pybinaryguard/models/enums.py +102 -0
  31. pybinaryguard/models/finding.py +109 -0
  32. pybinaryguard/models/package.py +121 -0
  33. pybinaryguard/models/system.py +118 -0
  34. pybinaryguard/plugins/__init__.py +19 -0
  35. pybinaryguard/plugins/contrib/__init__.py +7 -0
  36. pybinaryguard/plugins/contrib/gstreamer.py +109 -0
  37. pybinaryguard/plugins/contrib/jetson.py +385 -0
  38. pybinaryguard/plugins/contrib/opencv.py +306 -0
  39. pybinaryguard/plugins/contrib/tensorrt.py +426 -0
  40. pybinaryguard/plugins/hooks.py +273 -0
  41. pybinaryguard/plugins/loader.py +190 -0
  42. pybinaryguard/predictor/__init__.py +23 -0
  43. pybinaryguard/predictor/dependency_graph.py +226 -0
  44. pybinaryguard/predictor/linker_simulator.py +161 -0
  45. pybinaryguard/predictor/predictor.py +144 -0
  46. pybinaryguard/predictor/resolver.py +282 -0
  47. pybinaryguard/probes/__init__.py +73 -0
  48. pybinaryguard/probes/base.py +45 -0
  49. pybinaryguard/probes/board_probe.py +248 -0
  50. pybinaryguard/probes/cpu_probe.py +176 -0
  51. pybinaryguard/probes/glibc_probe.py +215 -0
  52. pybinaryguard/probes/gpu_probe.py +430 -0
  53. pybinaryguard/probes/library_probe.py +132 -0
  54. pybinaryguard/probes/os_probe.py +227 -0
  55. pybinaryguard/probes/python_probe.py +135 -0
  56. pybinaryguard/probes/toolchain_probe.py +89 -0
  57. pybinaryguard/probes/venv_probe.py +111 -0
  58. pybinaryguard/profiles/__init__.py +12 -0
  59. pybinaryguard/profiles/engine.py +343 -0
  60. pybinaryguard/rules/__init__.py +24 -0
  61. pybinaryguard/rules/base.py +57 -0
  62. pybinaryguard/rules/builtin/__init__.py +136 -0
  63. pybinaryguard/rules/builtin/arch_rules.py +86 -0
  64. pybinaryguard/rules/builtin/board_profile_rules.py +282 -0
  65. pybinaryguard/rules/builtin/container_rules.py +170 -0
  66. pybinaryguard/rules/builtin/cpu_rules.py +204 -0
  67. pybinaryguard/rules/builtin/cuda_rules.py +760 -0
  68. pybinaryguard/rules/builtin/dependency_rules.py +278 -0
  69. pybinaryguard/rules/builtin/framework_rules.py +252 -0
  70. pybinaryguard/rules/builtin/glibc_rules.py +320 -0
  71. pybinaryguard/rules/builtin/numpy_rules.py +110 -0
  72. pybinaryguard/rules/builtin/predictive_rules.py +165 -0
  73. pybinaryguard/rules/builtin/python_abi_rules.py +259 -0
  74. pybinaryguard/rules/builtin/source_build_rules.py +150 -0
  75. pybinaryguard/rules/builtin/venv_rules.py +159 -0
  76. pybinaryguard/rules/engine.py +123 -0
  77. pybinaryguard/scanner.py +904 -0
  78. pybinaryguard/scoring/__init__.py +19 -0
  79. pybinaryguard/scoring/engine.py +416 -0
  80. pybinaryguard/snapshot/__init__.py +20 -0
  81. pybinaryguard/snapshot/generator.py +168 -0
  82. pybinaryguard/snapshot/lockfile.py +152 -0
  83. pybinaryguard/snapshot/verifier.py +315 -0
  84. pybinaryguard/validators/__init__.py +7 -0
  85. pybinaryguard/validators/import_validator.py +231 -0
  86. pybinaryguard-1.0.0.dist-info/METADATA +876 -0
  87. pybinaryguard-1.0.0.dist-info/RECORD +91 -0
  88. pybinaryguard-1.0.0.dist-info/WHEEL +5 -0
  89. pybinaryguard-1.0.0.dist-info/entry_points.txt +8 -0
  90. pybinaryguard-1.0.0.dist-info/licenses/LICENSE +21 -0
  91. pybinaryguard-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,250 @@
1
+ """CLI entry point for PyBinaryGuard.
2
+
3
+ Usage::
4
+
5
+ pybinaryguard scan
6
+ pybinaryguard check torch
7
+ pybinaryguard profile
8
+ pybinaryguard doctor --error "GLIBC_2.34 not found"
9
+ pybinaryguard inspect wheel.whl
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import sys
16
+ from typing import List, Optional
17
+
18
+
19
+ def _build_parser() -> argparse.ArgumentParser:
20
+ """Build the argument parser with all commands and options."""
21
+ # Parent parser for shared global options
22
+ parent = argparse.ArgumentParser(add_help=False)
23
+ parent.add_argument(
24
+ "--format",
25
+ choices=["table", "json", "minimal"],
26
+ default="table",
27
+ help="Output format (default: table)",
28
+ )
29
+ parent.add_argument(
30
+ "--severity",
31
+ choices=["critical", "warning", "info", "all"],
32
+ default="all",
33
+ help="Minimum severity to show (default: all)",
34
+ )
35
+ parent.add_argument(
36
+ "--no-color",
37
+ action="store_true",
38
+ help="Disable coloured output",
39
+ )
40
+ parent.add_argument(
41
+ "--verbose", "-v",
42
+ action="store_true",
43
+ help="Show technical details and all suggestions",
44
+ )
45
+ parent.add_argument(
46
+ "--quiet", "-q",
47
+ action="store_true",
48
+ help="Only show critical findings",
49
+ )
50
+ parent.add_argument(
51
+ "--ci",
52
+ action="store_true",
53
+ help="CI mode: minimal output + strict exit codes",
54
+ )
55
+ parent.add_argument(
56
+ "--timeout",
57
+ type=float,
58
+ default=30.0,
59
+ help="Max scan time in seconds (default: 30)",
60
+ )
61
+ parent.add_argument(
62
+ "--ignore",
63
+ nargs="*",
64
+ default=[],
65
+ metavar="RULE_ID",
66
+ help="Rule IDs to ignore (e.g. CUDA_MINOR_MISMATCH)",
67
+ )
68
+ mode_group = parent.add_mutually_exclusive_group()
69
+ mode_group.add_argument(
70
+ "--fast",
71
+ action="store_true",
72
+ help="Fast scan: metadata only, skip binary analysis (<1s)",
73
+ )
74
+ mode_group.add_argument(
75
+ "--deep",
76
+ action="store_true",
77
+ help="Deep scan: full symbol resolution + hash verification",
78
+ )
79
+
80
+ # Main parser
81
+ parser = argparse.ArgumentParser(
82
+ prog="pybinaryguard",
83
+ description="PyBinaryGuard -- Binary Compatibility Scanner for Python",
84
+ formatter_class=argparse.RawDescriptionHelpFormatter,
85
+ parents=[parent],
86
+ epilog=(
87
+ "Examples:\n"
88
+ " pybinaryguard scan Full environment scan\n"
89
+ " pybinaryguard check torch Check a specific package\n"
90
+ " pybinaryguard profile Show system profile\n"
91
+ " pybinaryguard doctor --error 'GLIBC_2.34 not found'\n"
92
+ " pybinaryguard inspect wheel.whl Analyse a wheel file\n"
93
+ ),
94
+ )
95
+ parser.add_argument(
96
+ "--version",
97
+ action="version",
98
+ version="%(prog)s 1.0.0",
99
+ )
100
+
101
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
102
+
103
+ # scan
104
+ subparsers.add_parser(
105
+ "scan", parents=[parent],
106
+ help="Full environment scan -- check everything",
107
+ )
108
+
109
+ # check
110
+ check_parser = subparsers.add_parser(
111
+ "check", parents=[parent],
112
+ help="Check a specific installed package",
113
+ )
114
+ check_parser.add_argument("package", help="Package name to check")
115
+
116
+ # profile
117
+ subparsers.add_parser(
118
+ "profile", parents=[parent],
119
+ help="Show system profile (no compatibility checks)",
120
+ )
121
+
122
+ # doctor
123
+ doctor_parser = subparsers.add_parser(
124
+ "doctor", parents=[parent],
125
+ help="Interactive troubleshooting",
126
+ )
127
+ doctor_parser.add_argument("--error", help="Error message to diagnose")
128
+ doctor_parser.add_argument("--package", help="Package that fails")
129
+
130
+ # inspect
131
+ inspect_parser = subparsers.add_parser(
132
+ "inspect", parents=[parent],
133
+ help="Analyse a .whl or .so file",
134
+ )
135
+ inspect_parser.add_argument("file", help="Path to .whl or .so file")
136
+
137
+ # snapshot
138
+ snapshot_parser = subparsers.add_parser(
139
+ "snapshot", parents=[parent],
140
+ help="Create environment snapshot (lockfile)",
141
+ )
142
+ snapshot_parser.add_argument(
143
+ "-o", "--output",
144
+ help="Output file (default: stdout)",
145
+ )
146
+ snapshot_parser.add_argument(
147
+ "--no-hashes",
148
+ action="store_true",
149
+ help="Skip computing binary hashes (faster)",
150
+ )
151
+
152
+ # verify
153
+ verify_parser = subparsers.add_parser(
154
+ "verify", parents=[parent],
155
+ help="Verify environment against snapshot",
156
+ )
157
+ verify_parser.add_argument(
158
+ "lockfile",
159
+ help="Path to lockfile to verify against",
160
+ )
161
+ verify_parser.add_argument(
162
+ "--no-hashes",
163
+ action="store_true",
164
+ help="Skip hash verification",
165
+ )
166
+ verify_parser.add_argument(
167
+ "--no-compat-checks",
168
+ action="store_true",
169
+ help="Skip compatibility checks",
170
+ )
171
+
172
+ # export-tool-schema (agent SDK)
173
+ schema_parser = subparsers.add_parser(
174
+ "export-tool-schema", parents=[parent],
175
+ help="Export tool schema for agent framework registration",
176
+ )
177
+ schema_parser.add_argument(
178
+ "--schema-format",
179
+ choices=["openai", "mcp", "json_schema"],
180
+ default="openai",
181
+ help="Schema format (default: openai)",
182
+ )
183
+
184
+ # simulate (agent SDK)
185
+ simulate_parser = subparsers.add_parser(
186
+ "simulate", parents=[parent],
187
+ help="Predict package compatibility before installing",
188
+ )
189
+ simulate_parser.add_argument(
190
+ "package_spec",
191
+ help="Package specifier or wheel filename to simulate",
192
+ )
193
+
194
+ # validate (import testing)
195
+ validate_parser = subparsers.add_parser(
196
+ "validate", parents=[parent],
197
+ help="Test actual imports in isolated subprocesses",
198
+ )
199
+ validate_parser.add_argument(
200
+ "--packages",
201
+ nargs="*",
202
+ metavar="PKG",
203
+ help="Specific packages to test (default: all with binaries)",
204
+ )
205
+ validate_parser.add_argument(
206
+ "--import-timeout",
207
+ type=float,
208
+ default=10.0,
209
+ help="Timeout per import test in seconds (default: 10)",
210
+ )
211
+
212
+ return parser
213
+
214
+
215
+ def main(argv: Optional[List[str]] = None) -> int:
216
+ """Main CLI entry point.
217
+
218
+ Returns
219
+ -------
220
+ int
221
+ Exit code: 0 = pass, 1 = warnings, 2 = critical, 3 = scanner error.
222
+ """
223
+ parser = _build_parser()
224
+ args = parser.parse_args(argv)
225
+
226
+ # Default to scan if no command given
227
+ if not args.command:
228
+ args.command = "scan"
229
+
230
+ # Apply CI mode overrides
231
+ if args.ci:
232
+ args.format = "minimal"
233
+ if args.severity == "all":
234
+ args.severity = "critical"
235
+ args.no_color = True
236
+
237
+ # Apply quiet mode
238
+ if args.quiet:
239
+ args.severity = "critical"
240
+
241
+ try:
242
+ from pybinaryguard.cli.commands import dispatch
243
+
244
+ return dispatch(args)
245
+ except KeyboardInterrupt:
246
+ print("\nInterrupted.", file=sys.stderr)
247
+ return 130
248
+ except Exception as exc:
249
+ print(f"Error: {exc}", file=sys.stderr)
250
+ return 3
@@ -0,0 +1,13 @@
1
+ """Diagnostics subsystem -- error diagnosis, explanation, and fix suggestions."""
2
+
3
+ from pybinaryguard.diagnostics.explainer import diagnose_error, explain_finding
4
+ from pybinaryguard.diagnostics.findings import filter_findings, sort_findings
5
+ from pybinaryguard.diagnostics.suggestions import suggest_fix
6
+
7
+ __all__ = [
8
+ "diagnose_error",
9
+ "explain_finding",
10
+ "filter_findings",
11
+ "sort_findings",
12
+ "suggest_fix",
13
+ ]
@@ -0,0 +1,356 @@
1
+ """Maps error messages and rule IDs to plain-English explanations.
2
+
3
+ This module provides the ``ErrorPatternDB`` class which matches raw error
4
+ messages against a curated database of regex patterns and returns
5
+ beginner-friendly diagnoses including root cause, explanation, and fix hints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import re
12
+ from pathlib import Path
13
+ from typing import Dict, List, Optional, Tuple
14
+
15
+ from pybinaryguard.models.finding import Finding
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Error-pattern database
20
+ # ---------------------------------------------------------------------------
21
+
22
+ ERROR_PATTERNS: List[Dict[str, str]] = [
23
+ {
24
+ "pattern": r"version .GLIBC_(\d+\.\d+). not found",
25
+ "root_cause": "GLIBC version too old",
26
+ "rule_id": "GLIBC_VERSION_MISMATCH",
27
+ "explanation": (
28
+ "Your system's C library (GLIBC) is older than what this package "
29
+ "needs. The package was compiled on a newer Linux system."
30
+ ),
31
+ "fix_hint": "Upgrade your OS or use an older package version.",
32
+ },
33
+ {
34
+ "pattern": r"Illegal instruction",
35
+ "root_cause": "CPU instruction set mismatch",
36
+ "rule_id": "ILLEGAL_INSTRUCTION_RISK",
37
+ "explanation": (
38
+ "The package uses CPU instructions (like AVX2) that your "
39
+ "processor doesn't support."
40
+ ),
41
+ "fix_hint": "Install a version built for your CPU, or use a different machine.",
42
+ },
43
+ {
44
+ "pattern": r"libcudart\.so\.(\d+): cannot open",
45
+ "root_cause": "CUDA runtime version mismatch",
46
+ "rule_id": "CUDA_RUNTIME_MISMATCH",
47
+ "explanation": (
48
+ "The package was built for a different CUDA version than "
49
+ "what's installed on your system."
50
+ ),
51
+ "fix_hint": "Install the package variant matching your CUDA version.",
52
+ },
53
+ {
54
+ "pattern": r"libcublas\.so\.(\d+): cannot open",
55
+ "root_cause": "CUDA library missing",
56
+ "rule_id": "CUDA_LIB_MISSING",
57
+ "explanation": (
58
+ "A CUDA math library required by this package is not installed."
59
+ ),
60
+ "fix_hint": (
61
+ "Install the matching CUDA toolkit or reinstall the package "
62
+ "for your CUDA version."
63
+ ),
64
+ },
65
+ {
66
+ "pattern": r"undefined symbol: _Py",
67
+ "root_cause": "Python ABI mismatch",
68
+ "rule_id": "PYTHON_ABI_MISMATCH",
69
+ "explanation": (
70
+ "The package was compiled for a different Python version's "
71
+ "internal API."
72
+ ),
73
+ "fix_hint": "Reinstall the package for your current Python version.",
74
+ },
75
+ {
76
+ "pattern": (
77
+ r"module compiled against API version (0x[0-9a-f]+) "
78
+ r"but this version of numpy is (0x[0-9a-f]+)"
79
+ ),
80
+ "root_cause": "NumPy C API version mismatch",
81
+ "rule_id": "NUMPY_ABI_MISMATCH",
82
+ "explanation": (
83
+ "The package was built against a different NumPy version's C API."
84
+ ),
85
+ "fix_hint": "Reinstall the package or upgrade/downgrade NumPy.",
86
+ },
87
+ {
88
+ "pattern": r"not a supported wheel on this platform",
89
+ "root_cause": "Platform/architecture mismatch",
90
+ "rule_id": "ARCH_MISMATCH",
91
+ "explanation": (
92
+ "This wheel was built for a different CPU architecture or OS."
93
+ ),
94
+ "fix_hint": "Download the correct wheel for your platform.",
95
+ },
96
+ {
97
+ "pattern": r"cudaErrorInsufficientDriver",
98
+ "root_cause": "GPU driver too old",
99
+ "rule_id": "CUDA_DRIVER_TOO_OLD",
100
+ "explanation": (
101
+ "Your GPU driver is too old to support the installed CUDA version."
102
+ ),
103
+ "fix_hint": "Update your NVIDIA GPU driver.",
104
+ },
105
+ {
106
+ "pattern": r"NVML: Driver/library version mismatch",
107
+ "root_cause": "Container driver mismatch",
108
+ "rule_id": "CONTAINER_DRIVER_MISMATCH",
109
+ "explanation": (
110
+ "The NVIDIA driver in your container doesn't match the host "
111
+ "driver. This often happens when the container image has a "
112
+ "newer CUDA than the host supports."
113
+ ),
114
+ "fix_hint": (
115
+ "Use nvidia-container-toolkit and ensure host driver supports "
116
+ "your CUDA version."
117
+ ),
118
+ },
119
+ {
120
+ "pattern": r"libcudnn\.so\.\d+: cannot open",
121
+ "root_cause": "cuDNN missing",
122
+ "rule_id": "CUDNN_VERSION_MISMATCH",
123
+ "explanation": (
124
+ "The cuDNN library required for GPU-accelerated deep learning "
125
+ "is not installed."
126
+ ),
127
+ "fix_hint": "Install the cuDNN version matching your CUDA installation.",
128
+ },
129
+ ]
130
+
131
+
132
+ # Pre-compile patterns once at import time for performance.
133
+ _COMPILED_PATTERNS: List[Tuple[re.Pattern[str], Dict[str, str]]] = [
134
+ (re.compile(entry["pattern"]), entry) for entry in ERROR_PATTERNS
135
+ ]
136
+
137
+
138
+ # ---------------------------------------------------------------------------
139
+ # ErrorPatternDB
140
+ # ---------------------------------------------------------------------------
141
+
142
+
143
+ class ErrorPatternDB:
144
+ """A database of error-message patterns with associated root causes.
145
+
146
+ The default set of patterns is embedded in the module (``ERROR_PATTERNS``)
147
+ *and* also available as ``error_patterns.json`` for external tooling. You
148
+ can extend the database at runtime by loading additional patterns from
149
+ JSON files or adding them programmatically.
150
+ """
151
+
152
+ def __init__(self) -> None:
153
+ self._patterns: List[Dict[str, str]] = list(ERROR_PATTERNS)
154
+ self._compiled: List[Tuple[re.Pattern[str], Dict[str, str]]] = list(
155
+ _COMPILED_PATTERNS
156
+ )
157
+
158
+ # -- mutation -----------------------------------------------------------
159
+
160
+ def add_pattern(self, entry: Dict[str, str]) -> None:
161
+ """Add a single pattern entry to the database.
162
+
163
+ Parameters
164
+ ----------
165
+ entry:
166
+ A dict with keys ``pattern``, ``root_cause``, ``rule_id``,
167
+ ``explanation``, and ``fix_hint``.
168
+
169
+ Raises
170
+ ------
171
+ ValueError
172
+ If any required key is missing.
173
+ """
174
+ required_keys = {"pattern", "root_cause", "rule_id", "explanation", "fix_hint"}
175
+ missing = required_keys - entry.keys()
176
+ if missing:
177
+ raise ValueError(f"Pattern entry is missing required keys: {missing}")
178
+
179
+ self._patterns.append(entry)
180
+ self._compiled.append((re.compile(entry["pattern"]), entry))
181
+
182
+ def load_json(self, path: str) -> int:
183
+ """Load additional patterns from a JSON file.
184
+
185
+ Parameters
186
+ ----------
187
+ path:
188
+ Absolute or relative path to a JSON file containing a list of
189
+ pattern entries.
190
+
191
+ Returns
192
+ -------
193
+ int
194
+ Number of patterns loaded.
195
+
196
+ Raises
197
+ ------
198
+ FileNotFoundError
199
+ If the file does not exist.
200
+ json.JSONDecodeError
201
+ If the file is not valid JSON.
202
+ ValueError
203
+ If any entry is missing required keys.
204
+ """
205
+ json_path = Path(path)
206
+ with json_path.open("r", encoding="utf-8") as fh:
207
+ entries: List[Dict[str, str]] = json.load(fh)
208
+
209
+ for entry in entries:
210
+ self.add_pattern(entry)
211
+ return len(entries)
212
+
213
+ # -- queries ------------------------------------------------------------
214
+
215
+ def get_error_patterns(self) -> List[Dict[str, str]]:
216
+ """Return all registered patterns as a list of dicts.
217
+
218
+ Returns
219
+ -------
220
+ List[Dict[str, str]]
221
+ A shallow copy of the internal pattern list.
222
+ """
223
+ return list(self._patterns)
224
+
225
+ def diagnose_error(self, error_message: str) -> Optional[Dict[str, str]]:
226
+ """Match *error_message* against all known patterns.
227
+
228
+ Parameters
229
+ ----------
230
+ error_message:
231
+ The raw error text (e.g. a traceback line or stderr fragment).
232
+
233
+ Returns
234
+ -------
235
+ Optional[Dict[str, str]]
236
+ A dict with keys ``root_cause``, ``rule_id``, ``explanation``,
237
+ ``fix_hint``, and ``matched_pattern`` if a match is found; otherwise
238
+ ``None``.
239
+ """
240
+ if not error_message:
241
+ return None
242
+
243
+ for compiled, entry in self._compiled:
244
+ match = compiled.search(error_message)
245
+ if match:
246
+ return {
247
+ "root_cause": entry["root_cause"],
248
+ "rule_id": entry["rule_id"],
249
+ "explanation": entry["explanation"],
250
+ "fix_hint": entry["fix_hint"],
251
+ "matched_pattern": entry["pattern"],
252
+ }
253
+ return None
254
+
255
+ def explain_finding(self, finding: Finding) -> str:
256
+ """Generate a full plain-English explanation paragraph for a Finding.
257
+
258
+ The explanation combines the finding's own explanation text with
259
+ any matching pattern data (if the finding carries a
260
+ ``related_error``), and appends a fix suggestion when available.
261
+
262
+ Parameters
263
+ ----------
264
+ finding:
265
+ A :class:`Finding` instance to explain.
266
+
267
+ Returns
268
+ -------
269
+ str
270
+ A multi-sentence explanation suitable for displaying to
271
+ end users.
272
+ """
273
+ parts: List[str] = []
274
+
275
+ # Start with the finding's built-in explanation.
276
+ if finding.explanation:
277
+ parts.append(finding.explanation)
278
+
279
+ # If there is a related error string, try to diagnose it for
280
+ # additional context.
281
+ if finding.related_error:
282
+ diagnosis = self.diagnose_error(finding.related_error)
283
+ if diagnosis and diagnosis["explanation"] not in (finding.explanation or ""):
284
+ parts.append(
285
+ f"Root cause: {diagnosis['root_cause']}. "
286
+ f"{diagnosis['explanation']}"
287
+ )
288
+ if diagnosis["fix_hint"]:
289
+ parts.append(f"Suggested fix: {diagnosis['fix_hint']}")
290
+
291
+ # Fall back to the finding's own suggestion if nothing else matched.
292
+ if finding.suggestion and not any("Suggested fix" in p for p in parts):
293
+ parts.append(f"Suggested fix: {finding.suggestion}")
294
+
295
+ if not parts:
296
+ return f"No detailed explanation available for rule {finding.rule_id}."
297
+
298
+ return " ".join(parts)
299
+
300
+
301
+ # ---------------------------------------------------------------------------
302
+ # Module-level convenience functions (thin wrappers around a default DB)
303
+ # ---------------------------------------------------------------------------
304
+
305
+ _DEFAULT_DB = ErrorPatternDB()
306
+
307
+
308
+ def diagnose_error(error_message: str) -> Optional[Dict[str, str]]:
309
+ """Match *error_message* against all known error patterns.
310
+
311
+ This is a convenience wrapper around
312
+ :meth:`ErrorPatternDB.diagnose_error` using the default built-in
313
+ pattern database.
314
+
315
+ Parameters
316
+ ----------
317
+ error_message:
318
+ The raw error text to diagnose.
319
+
320
+ Returns
321
+ -------
322
+ Optional[Dict[str, str]]
323
+ Diagnosis dict or ``None``.
324
+ """
325
+ return _DEFAULT_DB.diagnose_error(error_message)
326
+
327
+
328
+ def explain_finding(finding: Finding) -> str:
329
+ """Generate a plain-English explanation for a Finding.
330
+
331
+ This is a convenience wrapper around
332
+ :meth:`ErrorPatternDB.explain_finding` using the default built-in
333
+ pattern database.
334
+
335
+ Parameters
336
+ ----------
337
+ finding:
338
+ The finding to explain.
339
+
340
+ Returns
341
+ -------
342
+ str
343
+ A human-readable explanation paragraph.
344
+ """
345
+ return _DEFAULT_DB.explain_finding(finding)
346
+
347
+
348
+ def get_error_patterns() -> List[Dict[str, str]]:
349
+ """Return all built-in error patterns.
350
+
351
+ Returns
352
+ -------
353
+ List[Dict[str, str]]
354
+ A list of pattern dicts.
355
+ """
356
+ return _DEFAULT_DB.get_error_patterns()