lucidscan 0.5.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 (91) hide show
  1. lucidscan/__init__.py +12 -0
  2. lucidscan/bootstrap/__init__.py +26 -0
  3. lucidscan/bootstrap/paths.py +160 -0
  4. lucidscan/bootstrap/platform.py +111 -0
  5. lucidscan/bootstrap/validation.py +76 -0
  6. lucidscan/bootstrap/versions.py +119 -0
  7. lucidscan/cli/__init__.py +50 -0
  8. lucidscan/cli/__main__.py +8 -0
  9. lucidscan/cli/arguments.py +405 -0
  10. lucidscan/cli/commands/__init__.py +64 -0
  11. lucidscan/cli/commands/autoconfigure.py +294 -0
  12. lucidscan/cli/commands/help.py +69 -0
  13. lucidscan/cli/commands/init.py +656 -0
  14. lucidscan/cli/commands/list_scanners.py +59 -0
  15. lucidscan/cli/commands/scan.py +307 -0
  16. lucidscan/cli/commands/serve.py +142 -0
  17. lucidscan/cli/commands/status.py +84 -0
  18. lucidscan/cli/commands/validate.py +105 -0
  19. lucidscan/cli/config_bridge.py +152 -0
  20. lucidscan/cli/exit_codes.py +17 -0
  21. lucidscan/cli/runner.py +284 -0
  22. lucidscan/config/__init__.py +29 -0
  23. lucidscan/config/ignore.py +178 -0
  24. lucidscan/config/loader.py +431 -0
  25. lucidscan/config/models.py +316 -0
  26. lucidscan/config/validation.py +645 -0
  27. lucidscan/core/__init__.py +3 -0
  28. lucidscan/core/domain_runner.py +463 -0
  29. lucidscan/core/git.py +174 -0
  30. lucidscan/core/logging.py +34 -0
  31. lucidscan/core/models.py +207 -0
  32. lucidscan/core/streaming.py +340 -0
  33. lucidscan/core/subprocess_runner.py +164 -0
  34. lucidscan/detection/__init__.py +21 -0
  35. lucidscan/detection/detector.py +154 -0
  36. lucidscan/detection/frameworks.py +270 -0
  37. lucidscan/detection/languages.py +328 -0
  38. lucidscan/detection/tools.py +229 -0
  39. lucidscan/generation/__init__.py +15 -0
  40. lucidscan/generation/config_generator.py +275 -0
  41. lucidscan/generation/package_installer.py +330 -0
  42. lucidscan/mcp/__init__.py +20 -0
  43. lucidscan/mcp/formatter.py +510 -0
  44. lucidscan/mcp/server.py +297 -0
  45. lucidscan/mcp/tools.py +1049 -0
  46. lucidscan/mcp/watcher.py +237 -0
  47. lucidscan/pipeline/__init__.py +17 -0
  48. lucidscan/pipeline/executor.py +187 -0
  49. lucidscan/pipeline/parallel.py +181 -0
  50. lucidscan/plugins/__init__.py +40 -0
  51. lucidscan/plugins/coverage/__init__.py +28 -0
  52. lucidscan/plugins/coverage/base.py +160 -0
  53. lucidscan/plugins/coverage/coverage_py.py +454 -0
  54. lucidscan/plugins/coverage/istanbul.py +411 -0
  55. lucidscan/plugins/discovery.py +107 -0
  56. lucidscan/plugins/enrichers/__init__.py +61 -0
  57. lucidscan/plugins/enrichers/base.py +63 -0
  58. lucidscan/plugins/linters/__init__.py +26 -0
  59. lucidscan/plugins/linters/base.py +125 -0
  60. lucidscan/plugins/linters/biome.py +448 -0
  61. lucidscan/plugins/linters/checkstyle.py +393 -0
  62. lucidscan/plugins/linters/eslint.py +368 -0
  63. lucidscan/plugins/linters/ruff.py +498 -0
  64. lucidscan/plugins/reporters/__init__.py +45 -0
  65. lucidscan/plugins/reporters/base.py +30 -0
  66. lucidscan/plugins/reporters/json_reporter.py +79 -0
  67. lucidscan/plugins/reporters/sarif_reporter.py +303 -0
  68. lucidscan/plugins/reporters/summary_reporter.py +61 -0
  69. lucidscan/plugins/reporters/table_reporter.py +81 -0
  70. lucidscan/plugins/scanners/__init__.py +57 -0
  71. lucidscan/plugins/scanners/base.py +60 -0
  72. lucidscan/plugins/scanners/checkov.py +484 -0
  73. lucidscan/plugins/scanners/opengrep.py +464 -0
  74. lucidscan/plugins/scanners/trivy.py +492 -0
  75. lucidscan/plugins/test_runners/__init__.py +27 -0
  76. lucidscan/plugins/test_runners/base.py +111 -0
  77. lucidscan/plugins/test_runners/jest.py +381 -0
  78. lucidscan/plugins/test_runners/karma.py +481 -0
  79. lucidscan/plugins/test_runners/playwright.py +434 -0
  80. lucidscan/plugins/test_runners/pytest.py +598 -0
  81. lucidscan/plugins/type_checkers/__init__.py +27 -0
  82. lucidscan/plugins/type_checkers/base.py +106 -0
  83. lucidscan/plugins/type_checkers/mypy.py +355 -0
  84. lucidscan/plugins/type_checkers/pyright.py +313 -0
  85. lucidscan/plugins/type_checkers/typescript.py +280 -0
  86. lucidscan-0.5.12.dist-info/METADATA +242 -0
  87. lucidscan-0.5.12.dist-info/RECORD +91 -0
  88. lucidscan-0.5.12.dist-info/WHEEL +5 -0
  89. lucidscan-0.5.12.dist-info/entry_points.txt +34 -0
  90. lucidscan-0.5.12.dist-info/licenses/LICENSE +201 -0
  91. lucidscan-0.5.12.dist-info/top_level.txt +1 -0
@@ -0,0 +1,313 @@
1
+ """pyright type checker plugin.
2
+
3
+ pyright is a fast static type checker for Python.
4
+ https://github.com/microsoft/pyright
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import json
11
+ import platform
12
+ import shutil
13
+ import subprocess
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from lucidscan.bootstrap.paths import LucidscanPaths
18
+ from lucidscan.bootstrap.versions import get_tool_version
19
+ from lucidscan.core.logging import get_logger
20
+ from lucidscan.core.models import (
21
+ ScanContext,
22
+ Severity,
23
+ ToolDomain,
24
+ UnifiedIssue,
25
+ )
26
+ from lucidscan.plugins.type_checkers.base import TypeCheckerPlugin
27
+
28
+ LOGGER = get_logger(__name__)
29
+
30
+ # Default version from pyproject.toml [tool.lucidscan.tools]
31
+ DEFAULT_VERSION = get_tool_version("pyright")
32
+
33
+ # pyright severity mapping
34
+ SEVERITY_MAP = {
35
+ "error": Severity.HIGH,
36
+ "warning": Severity.MEDIUM,
37
+ "information": Severity.LOW,
38
+ }
39
+
40
+
41
+ class PyrightChecker(TypeCheckerPlugin):
42
+ """pyright type checker plugin for Python code analysis."""
43
+
44
+ def __init__(
45
+ self,
46
+ version: str = DEFAULT_VERSION,
47
+ project_root: Optional[Path] = None,
48
+ ):
49
+ """Initialize PyrightChecker.
50
+
51
+ Args:
52
+ version: pyright version to use.
53
+ project_root: Optional project root for tool installation.
54
+ """
55
+ self._version = version
56
+ if project_root:
57
+ self._paths = LucidscanPaths.for_project(project_root)
58
+ self._project_root = project_root
59
+ else:
60
+ self._paths = LucidscanPaths.default()
61
+ self._project_root = None
62
+
63
+ @property
64
+ def name(self) -> str:
65
+ """Plugin identifier."""
66
+ return "pyright"
67
+
68
+ @property
69
+ def languages(self) -> List[str]:
70
+ """Supported languages."""
71
+ return ["python"]
72
+
73
+ @property
74
+ def supports_strict_mode(self) -> bool:
75
+ """pyright supports strict mode."""
76
+ return True
77
+
78
+ def get_version(self) -> str:
79
+ """Get pyright version."""
80
+ return self._version
81
+
82
+ def ensure_binary(self) -> Path:
83
+ """Ensure pyright is available.
84
+
85
+ Checks for pyright in:
86
+ 1. Project's .venv/bin/pyright (pip installed pyright)
87
+ 2. Project's node_modules/.bin/pyright
88
+ 3. System PATH (npm or pip installed)
89
+ 4. Downloads standalone binary if not found
90
+
91
+ Returns:
92
+ Path to pyright binary.
93
+ """
94
+ # Check project venv first (pip install pyright)
95
+ if self._project_root:
96
+ venv_pyright = self._project_root / ".venv" / "bin" / "pyright"
97
+ if venv_pyright.exists():
98
+ return venv_pyright
99
+
100
+ # Check project node_modules
101
+ if self._project_root:
102
+ node_pyright = self._project_root / "node_modules" / ".bin" / "pyright"
103
+ if node_pyright.exists():
104
+ return node_pyright
105
+
106
+ # Check system PATH (npm or pip installed)
107
+ pyright_path = shutil.which("pyright")
108
+ if pyright_path:
109
+ return Path(pyright_path)
110
+
111
+ # Download standalone binary
112
+ return self._download_binary()
113
+
114
+ def _download_binary(self) -> Path:
115
+ """Download pyright standalone binary.
116
+
117
+ Returns:
118
+ Path to downloaded binary.
119
+ """
120
+
121
+ binary_dir = self._paths.plugin_bin_dir(self.name, self._version)
122
+ binary_name = "pyright.exe" if platform.system() == "Windows" else "pyright"
123
+ binary_path = binary_dir / binary_name
124
+
125
+ if binary_path.exists():
126
+ return binary_path
127
+
128
+ LOGGER.info(f"Downloading pyright {self._version}...")
129
+ binary_dir.mkdir(parents=True, exist_ok=True)
130
+
131
+ # pyright is distributed as npm package, but we can use pyright-python
132
+ # which provides a standalone binary
133
+ # For now, we'll check if pyright is available via pip
134
+ pip_pyright = shutil.which("pyright")
135
+ if pip_pyright:
136
+ return Path(pip_pyright)
137
+
138
+ # If not available, try to use npm to install it locally
139
+ # or raise an error with installation instructions
140
+ raise FileNotFoundError(
141
+ "pyright is not installed. Install it with:\n"
142
+ " npm install -g pyright\n"
143
+ " OR\n"
144
+ " pip install pyright"
145
+ )
146
+
147
+ def check(self, context: ScanContext) -> List[UnifiedIssue]:
148
+ """Run pyright type checking.
149
+
150
+ Args:
151
+ context: Scan context with paths and configuration.
152
+
153
+ Returns:
154
+ List of type checking issues.
155
+ """
156
+ try:
157
+ binary = self.ensure_binary()
158
+ except FileNotFoundError as e:
159
+ LOGGER.warning(str(e))
160
+ return []
161
+
162
+ # Build command
163
+ cmd = [
164
+ str(binary),
165
+ "--outputjson",
166
+ ]
167
+
168
+ # Add paths to check
169
+ paths = [str(p) for p in context.paths] if context.paths else ["."]
170
+ cmd.extend(paths)
171
+
172
+ LOGGER.debug(f"Running: {' '.join(cmd)}")
173
+
174
+ try:
175
+ result = subprocess.run(
176
+ cmd,
177
+ capture_output=True,
178
+ text=True,
179
+ encoding="utf-8",
180
+ errors="replace",
181
+ cwd=str(context.project_root),
182
+ timeout=180, # 3 minute timeout
183
+ )
184
+ except subprocess.TimeoutExpired:
185
+ LOGGER.warning("pyright timed out after 180 seconds")
186
+ return []
187
+ except Exception as e:
188
+ LOGGER.error(f"Failed to run pyright: {e}")
189
+ return []
190
+
191
+ # Parse output
192
+ issues = self._parse_output(result.stdout, context.project_root)
193
+
194
+ LOGGER.info(f"pyright found {len(issues)} issues")
195
+ return issues
196
+
197
+ def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
198
+ """Parse pyright JSON output.
199
+
200
+ Args:
201
+ output: JSON output from pyright.
202
+ project_root: Project root directory.
203
+
204
+ Returns:
205
+ List of UnifiedIssue objects.
206
+ """
207
+ if not output.strip():
208
+ return []
209
+
210
+ try:
211
+ data = json.loads(output)
212
+ except json.JSONDecodeError:
213
+ LOGGER.warning("Failed to parse pyright output as JSON")
214
+ return []
215
+
216
+ issues = []
217
+ diagnostics = data.get("generalDiagnostics", [])
218
+
219
+ for diagnostic in diagnostics:
220
+ issue = self._diagnostic_to_issue(diagnostic, project_root)
221
+ if issue:
222
+ issues.append(issue)
223
+
224
+ return issues
225
+
226
+ def _diagnostic_to_issue(
227
+ self,
228
+ diagnostic: Dict[str, Any],
229
+ project_root: Path,
230
+ ) -> Optional[UnifiedIssue]:
231
+ """Convert pyright diagnostic to UnifiedIssue.
232
+
233
+ Args:
234
+ diagnostic: pyright diagnostic dict.
235
+ project_root: Project root directory.
236
+
237
+ Returns:
238
+ UnifiedIssue or None.
239
+ """
240
+ try:
241
+ severity_str = diagnostic.get("severity", "error")
242
+ message = diagnostic.get("message", "")
243
+ file = diagnostic.get("file", "")
244
+ rule = diagnostic.get("rule", "")
245
+
246
+ # Get range info
247
+ range_info = diagnostic.get("range", {})
248
+ start = range_info.get("start", {})
249
+ end = range_info.get("end", {})
250
+
251
+ line_start = start.get("line", 0) + 1 # pyright uses 0-based lines
252
+ line_end = end.get("line", 0) + 1
253
+ column = start.get("character", 0) + 1
254
+
255
+ # Get severity
256
+ severity = SEVERITY_MAP.get(severity_str, Severity.MEDIUM)
257
+
258
+ # Build file path
259
+ file_path = Path(file)
260
+ if not file_path.is_absolute():
261
+ file_path = project_root / file_path
262
+
263
+ # Generate deterministic ID
264
+ issue_id = self._generate_issue_id(rule, file, line_start, column, message)
265
+
266
+ # Build title
267
+ title = f"[{rule}] {message}" if rule else message
268
+
269
+ return UnifiedIssue(
270
+ id=issue_id,
271
+ domain=ToolDomain.TYPE_CHECKING,
272
+ source_tool="pyright",
273
+ severity=severity,
274
+ rule_id=rule or "unknown",
275
+ title=title,
276
+ description=message,
277
+ documentation_url="https://github.com/microsoft/pyright/blob/main/docs/configuration.md",
278
+ file_path=file_path,
279
+ line_start=line_start,
280
+ line_end=line_end,
281
+ column_start=column,
282
+ fixable=False,
283
+ metadata={
284
+ "severity_raw": severity_str,
285
+ },
286
+ )
287
+ except Exception as e:
288
+ LOGGER.warning(f"Failed to parse pyright diagnostic: {e}")
289
+ return None
290
+
291
+ def _generate_issue_id(
292
+ self,
293
+ rule: str,
294
+ file: str,
295
+ line: int,
296
+ column: int,
297
+ message: str,
298
+ ) -> str:
299
+ """Generate deterministic issue ID.
300
+
301
+ Args:
302
+ rule: Error rule/code.
303
+ file: File path.
304
+ line: Line number.
305
+ column: Column number.
306
+ message: Error message.
307
+
308
+ Returns:
309
+ Unique issue ID.
310
+ """
311
+ content = f"{rule}:{file}:{line}:{column}:{message}"
312
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
313
+ return f"pyright-{rule}-{hash_val}" if rule else f"pyright-{hash_val}"
@@ -0,0 +1,280 @@
1
+ """TypeScript type checker plugin.
2
+
3
+ TypeScript uses the tsc compiler for type checking.
4
+ https://www.typescriptlang.org/
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import hashlib
10
+ import re
11
+ import shutil
12
+ import subprocess
13
+ from pathlib import Path
14
+ from typing import List, Optional
15
+
16
+ from lucidscan.core.logging import get_logger
17
+ from lucidscan.core.models import (
18
+ ScanContext,
19
+ Severity,
20
+ ToolDomain,
21
+ UnifiedIssue,
22
+ )
23
+ from lucidscan.plugins.type_checkers.base import TypeCheckerPlugin
24
+
25
+ LOGGER = get_logger(__name__)
26
+
27
+ # TypeScript error pattern: file(line,col): error TS1234: message
28
+ TSC_ERROR_PATTERN = re.compile(
29
+ r"^(.+?)\((\d+),(\d+)\):\s+(error|warning)\s+(TS\d+):\s+(.+)$"
30
+ )
31
+
32
+
33
+ class TypeScriptChecker(TypeCheckerPlugin):
34
+ """TypeScript type checker plugin using tsc."""
35
+
36
+ def __init__(self, project_root: Optional[Path] = None):
37
+ """Initialize TypeScriptChecker.
38
+
39
+ Args:
40
+ project_root: Optional project root for finding tsc installation.
41
+ """
42
+ self._project_root = project_root
43
+
44
+ @property
45
+ def name(self) -> str:
46
+ """Plugin identifier."""
47
+ return "typescript"
48
+
49
+ @property
50
+ def languages(self) -> List[str]:
51
+ """Supported languages."""
52
+ return ["typescript"]
53
+
54
+ @property
55
+ def supports_strict_mode(self) -> bool:
56
+ """TypeScript supports strict mode via tsconfig.json."""
57
+ return True
58
+
59
+ def get_version(self) -> str:
60
+ """Get TypeScript version.
61
+
62
+ Returns:
63
+ Version string or 'unknown' if unable to determine.
64
+ """
65
+ try:
66
+ binary = self.ensure_binary()
67
+ result = subprocess.run(
68
+ [str(binary), "--version"],
69
+ capture_output=True,
70
+ text=True,
71
+ encoding="utf-8",
72
+ errors="replace",
73
+ timeout=30,
74
+ )
75
+ # Output is like "Version 5.3.3"
76
+ if result.returncode == 0:
77
+ parts = result.stdout.strip().split()
78
+ if len(parts) >= 2:
79
+ return parts[1]
80
+ except Exception:
81
+ pass
82
+ return "unknown"
83
+
84
+ def ensure_binary(self) -> Path:
85
+ """Ensure tsc is available.
86
+
87
+ Checks for tsc in:
88
+ 1. Project's node_modules/.bin/tsc
89
+ 2. System PATH (globally installed)
90
+
91
+ Returns:
92
+ Path to tsc binary.
93
+
94
+ Raises:
95
+ FileNotFoundError: If TypeScript is not installed.
96
+ """
97
+ # Check project node_modules first
98
+ if self._project_root:
99
+ node_tsc = self._project_root / "node_modules" / ".bin" / "tsc"
100
+ if node_tsc.exists():
101
+ return node_tsc
102
+
103
+ # Check system PATH
104
+ tsc_path = shutil.which("tsc")
105
+ if tsc_path:
106
+ return Path(tsc_path)
107
+
108
+ raise FileNotFoundError(
109
+ "TypeScript is not installed. Install it with:\n"
110
+ " npm install typescript --save-dev\n"
111
+ " OR\n"
112
+ " npm install -g typescript"
113
+ )
114
+
115
+ def check(self, context: ScanContext) -> List[UnifiedIssue]:
116
+ """Run TypeScript type checking.
117
+
118
+ Args:
119
+ context: Scan context with paths and configuration.
120
+
121
+ Returns:
122
+ List of type checking issues.
123
+ """
124
+ try:
125
+ binary = self.ensure_binary()
126
+ except FileNotFoundError as e:
127
+ LOGGER.warning(str(e))
128
+ return []
129
+
130
+ # Check for tsconfig.json
131
+ tsconfig = context.project_root / "tsconfig.json"
132
+ if not tsconfig.exists():
133
+ LOGGER.warning(
134
+ f"No tsconfig.json found in {context.project_root}, skipping TypeScript checking"
135
+ )
136
+ return []
137
+
138
+ # Build command
139
+ cmd = [
140
+ str(binary),
141
+ "--noEmit", # Don't emit compiled files
142
+ "--pretty", "false", # Plain output for parsing
143
+ ]
144
+
145
+ LOGGER.debug(f"Running: {' '.join(cmd)}")
146
+
147
+ try:
148
+ result = subprocess.run(
149
+ cmd,
150
+ capture_output=True,
151
+ text=True,
152
+ encoding="utf-8",
153
+ errors="replace",
154
+ cwd=str(context.project_root),
155
+ timeout=180, # 3 minute timeout
156
+ )
157
+ except subprocess.TimeoutExpired:
158
+ LOGGER.warning("tsc timed out after 180 seconds")
159
+ return []
160
+ except Exception as e:
161
+ LOGGER.error(f"Failed to run tsc: {e}")
162
+ return []
163
+
164
+ # Parse output (tsc outputs to stdout on success, stderr on error)
165
+ output = result.stdout or result.stderr
166
+ issues = self._parse_output(output, context.project_root)
167
+
168
+ LOGGER.info(f"TypeScript found {len(issues)} issues")
169
+ return issues
170
+
171
+ def _parse_output(self, output: str, project_root: Path) -> List[UnifiedIssue]:
172
+ """Parse tsc output.
173
+
174
+ tsc outputs errors in format:
175
+ file(line,col): error TS1234: message
176
+
177
+ Args:
178
+ output: Output from tsc command.
179
+ project_root: Project root directory.
180
+
181
+ Returns:
182
+ List of UnifiedIssue objects.
183
+ """
184
+ if not output.strip():
185
+ return []
186
+
187
+ issues = []
188
+ for line in output.strip().split("\n"):
189
+ line = line.strip()
190
+ if not line:
191
+ continue
192
+
193
+ match = TSC_ERROR_PATTERN.match(line)
194
+ if match:
195
+ issue = self._match_to_issue(match, project_root)
196
+ if issue:
197
+ issues.append(issue)
198
+ else:
199
+ LOGGER.debug(f"Skipping non-matching line: {line}")
200
+
201
+ return issues
202
+
203
+ def _match_to_issue(
204
+ self,
205
+ match: re.Match,
206
+ project_root: Path,
207
+ ) -> Optional[UnifiedIssue]:
208
+ """Convert regex match to UnifiedIssue.
209
+
210
+ Args:
211
+ match: Regex match from TSC_ERROR_PATTERN.
212
+ project_root: Project root directory.
213
+
214
+ Returns:
215
+ UnifiedIssue or None.
216
+ """
217
+ try:
218
+ file_path_str = match.group(1)
219
+ line = int(match.group(2))
220
+ column = int(match.group(3))
221
+ severity_str = match.group(4)
222
+ code = match.group(5)
223
+ message = match.group(6)
224
+
225
+ # TypeScript errors are always high severity
226
+ severity = Severity.HIGH if severity_str == "error" else Severity.MEDIUM
227
+
228
+ # Build file path
229
+ file_path = Path(file_path_str)
230
+ if not file_path.is_absolute():
231
+ file_path = project_root / file_path
232
+
233
+ # Generate deterministic ID
234
+ issue_id = self._generate_issue_id(code, file_path_str, line, column, message)
235
+
236
+ return UnifiedIssue(
237
+ id=issue_id,
238
+ domain=ToolDomain.TYPE_CHECKING,
239
+ source_tool="typescript",
240
+ severity=severity,
241
+ rule_id=code,
242
+ title=f"[{code}] {message}",
243
+ description=message,
244
+ documentation_url=f"https://typescript.tv/errors/#{code}",
245
+ file_path=file_path,
246
+ line_start=line,
247
+ line_end=line,
248
+ column_start=column,
249
+ fixable=False,
250
+ metadata={
251
+ "severity_raw": severity_str,
252
+ },
253
+ )
254
+ except Exception as e:
255
+ LOGGER.warning(f"Failed to parse tsc error: {e}")
256
+ return None
257
+
258
+ def _generate_issue_id(
259
+ self,
260
+ code: str,
261
+ file: str,
262
+ line: int,
263
+ column: int,
264
+ message: str,
265
+ ) -> str:
266
+ """Generate deterministic issue ID.
267
+
268
+ Args:
269
+ code: TypeScript error code (e.g., TS1234).
270
+ file: File path.
271
+ line: Line number.
272
+ column: Column number.
273
+ message: Error message.
274
+
275
+ Returns:
276
+ Unique issue ID.
277
+ """
278
+ content = f"{code}:{file}:{line}:{column}:{message}"
279
+ hash_val = hashlib.sha256(content.encode()).hexdigest()[:12]
280
+ return f"ts-{code}-{hash_val}"