@trac3er/oh-my-god 2.0.0 → 2.0.2

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 (243) hide show
  1. package/.claude-plugin/marketplace.json +8 -8
  2. package/.claude-plugin/plugin.json +5 -4
  3. package/.claude-plugin/scripts/uninstall.sh +74 -3
  4. package/.claude-plugin/scripts/update.sh +78 -3
  5. package/.coveragerc +26 -0
  6. package/.mcp.json +4 -4
  7. package/CHANGELOG.md +14 -0
  8. package/CODE_OF_CONDUCT.md +27 -0
  9. package/CONTRIBUTING.md +62 -0
  10. package/OMG-setup.sh +1201 -355
  11. package/README.md +77 -56
  12. package/SECURITY.md +25 -0
  13. package/agents/__init__.py +1 -0
  14. package/agents/model_roles.py +196 -0
  15. package/agents/omg-architect-mode.md +3 -5
  16. package/agents/omg-backend-engineer.md +3 -5
  17. package/agents/omg-database-engineer.md +3 -5
  18. package/agents/omg-frontend-designer.md +4 -5
  19. package/agents/omg-implement-mode.md +4 -5
  20. package/agents/omg-infra-engineer.md +3 -5
  21. package/agents/omg-research-mode.md +4 -6
  22. package/agents/omg-security-auditor.md +3 -5
  23. package/agents/omg-testing-engineer.md +3 -5
  24. package/build/lib/yaml.py +321 -0
  25. package/commands/OMG:ai-commit.md +101 -14
  26. package/commands/OMG:arch.md +302 -19
  27. package/commands/OMG:ccg.md +12 -7
  28. package/commands/OMG:compat.md +25 -17
  29. package/commands/OMG:cost.md +173 -13
  30. package/commands/OMG:crazy.md +1 -1
  31. package/commands/OMG:create-agent.md +170 -20
  32. package/commands/OMG:deps.md +235 -17
  33. package/commands/OMG:domain-init.md +1 -1
  34. package/commands/OMG:escalate.md +41 -12
  35. package/commands/OMG:health-check.md +37 -13
  36. package/commands/OMG:init.md +122 -14
  37. package/commands/OMG:project-init.md +1 -1
  38. package/commands/OMG:session-branch.md +76 -9
  39. package/commands/OMG:session-fork.md +42 -5
  40. package/commands/OMG:session-merge.md +124 -8
  41. package/commands/OMG:setup.md +69 -12
  42. package/commands/OMG:stats.md +215 -14
  43. package/commands/OMG:teams.md +19 -10
  44. package/config/lsp_languages.yaml +8 -0
  45. package/hooks/__init__.py +0 -0
  46. package/hooks/_agent_registry.py +423 -0
  47. package/hooks/_analytics.py +291 -0
  48. package/hooks/_budget.py +31 -0
  49. package/hooks/_common.py +569 -0
  50. package/hooks/_compression_optimizer.py +119 -0
  51. package/hooks/_cost_ledger.py +176 -0
  52. package/hooks/_learnings.py +126 -0
  53. package/hooks/_memory.py +103 -0
  54. package/hooks/_protected_context.py +150 -0
  55. package/hooks/_token_counter.py +221 -0
  56. package/hooks/branch_manager.py +236 -0
  57. package/hooks/budget_governor.py +232 -0
  58. package/hooks/circuit-breaker.py +270 -0
  59. package/hooks/compression_feedback.py +254 -0
  60. package/hooks/config-guard.py +216 -0
  61. package/hooks/context_pressure.py +53 -0
  62. package/hooks/credential_store.py +1020 -0
  63. package/hooks/fetch-rate-limits.py +212 -0
  64. package/hooks/firewall.py +48 -0
  65. package/hooks/hashline-formatter-bridge.py +224 -0
  66. package/hooks/hashline-injector.py +273 -0
  67. package/hooks/hashline-validator.py +216 -0
  68. package/hooks/idle-detector.py +95 -0
  69. package/hooks/intentgate-keyword-detector.py +188 -0
  70. package/hooks/magic-keyword-router.py +195 -0
  71. package/hooks/policy_engine.py +505 -0
  72. package/hooks/post-tool-failure.py +19 -0
  73. package/hooks/post-write.py +219 -0
  74. package/hooks/post_write.py +46 -0
  75. package/hooks/pre-compact.py +398 -0
  76. package/hooks/pre-tool-inject.py +98 -0
  77. package/hooks/prompt-enhancer.py +672 -0
  78. package/hooks/quality-runner.py +191 -0
  79. package/hooks/query.py +512 -0
  80. package/hooks/secret-guard.py +61 -0
  81. package/hooks/secret_audit.py +144 -0
  82. package/hooks/session-end-capture.py +137 -0
  83. package/hooks/session-start.py +277 -0
  84. package/hooks/setup_wizard.py +582 -0
  85. package/hooks/shadow_manager.py +297 -0
  86. package/hooks/state_migration.py +225 -0
  87. package/hooks/stop-gate.py +7 -0
  88. package/hooks/stop_dispatcher.py +945 -0
  89. package/hooks/test-validator.py +361 -0
  90. package/hooks/test_generator_hook.py +123 -0
  91. package/hooks/todo-state-tracker.py +114 -0
  92. package/hooks/tool-ledger.py +149 -0
  93. package/hooks/trust_review.py +585 -0
  94. package/hud/omg-hud.mjs +31 -1
  95. package/lab/__init__.py +1 -0
  96. package/lab/pipeline.py +75 -0
  97. package/lab/policies.py +52 -0
  98. package/package.json +7 -18
  99. package/plugins/README.md +33 -61
  100. package/plugins/advanced/commands/OMG:deep-plan.md +3 -3
  101. package/plugins/advanced/commands/OMG:learn.md +1 -1
  102. package/plugins/advanced/commands/OMG:security-review.md +3 -3
  103. package/plugins/advanced/commands/OMG:ship.md +1 -1
  104. package/plugins/advanced/plugin.json +1 -1
  105. package/plugins/core/plugin.json +8 -3
  106. package/plugins/dephealth/__init__.py +0 -0
  107. package/plugins/dephealth/cve_scanner.py +188 -0
  108. package/plugins/dephealth/license_checker.py +135 -0
  109. package/plugins/dephealth/manifest_detector.py +423 -0
  110. package/plugins/dephealth/vuln_analyzer.py +169 -0
  111. package/plugins/testgen/__init__.py +0 -0
  112. package/plugins/testgen/codamosa_engine.py +402 -0
  113. package/plugins/testgen/edge_case_synthesizer.py +184 -0
  114. package/plugins/testgen/framework_detector.py +271 -0
  115. package/plugins/testgen/skeleton_generator.py +219 -0
  116. package/plugins/viz/__init__.py +0 -0
  117. package/plugins/viz/ast_parser.py +139 -0
  118. package/plugins/viz/diagram_generator.py +192 -0
  119. package/plugins/viz/graph_builder.py +444 -0
  120. package/plugins/viz/native_parsers.py +259 -0
  121. package/plugins/viz/regex_parser.py +112 -0
  122. package/pyproject.toml +81 -0
  123. package/rules/contextual/write-verify.md +2 -2
  124. package/rules/core/00-truth.md +1 -1
  125. package/rules/core/01-surgical.md +1 -1
  126. package/rules/core/02-circuit-breaker.md +2 -2
  127. package/rules/core/03-ensemble.md +3 -3
  128. package/rules/core/04-testing.md +3 -3
  129. package/runtime/__init__.py +32 -0
  130. package/runtime/adapters/__init__.py +13 -0
  131. package/runtime/adapters/claude.py +60 -0
  132. package/runtime/adapters/gpt.py +53 -0
  133. package/runtime/adapters/local.py +53 -0
  134. package/runtime/adoption.py +212 -0
  135. package/runtime/business_workflow.py +220 -0
  136. package/runtime/cli_provider.py +85 -0
  137. package/runtime/compat.py +1299 -0
  138. package/runtime/custom_agent_loader.py +366 -0
  139. package/runtime/dispatcher.py +47 -0
  140. package/runtime/ecosystem.py +371 -0
  141. package/runtime/legacy_compat.py +7 -0
  142. package/runtime/mcp_config_writers.py +115 -0
  143. package/runtime/mcp_lifecycle.py +153 -0
  144. package/runtime/mcp_memory_server.py +135 -0
  145. package/runtime/memory_parsers/__init__.py +0 -0
  146. package/runtime/memory_parsers/chatgpt_parser.py +257 -0
  147. package/runtime/memory_parsers/claude_import.py +107 -0
  148. package/runtime/memory_parsers/export.py +97 -0
  149. package/runtime/memory_parsers/gemini_import.py +91 -0
  150. package/runtime/memory_parsers/kimi_import.py +91 -0
  151. package/runtime/memory_store.py +215 -0
  152. package/runtime/omc_compat.py +7 -0
  153. package/runtime/providers/__init__.py +0 -0
  154. package/runtime/providers/codex_provider.py +112 -0
  155. package/runtime/providers/gemini_provider.py +128 -0
  156. package/runtime/providers/kimi_provider.py +151 -0
  157. package/runtime/providers/opencode_provider.py +144 -0
  158. package/runtime/subagent_dispatcher.py +362 -0
  159. package/runtime/team_router.py +1167 -0
  160. package/runtime/tmux_session_manager.py +169 -0
  161. package/scripts/check-omg-compat-contract-snapshot.py +137 -0
  162. package/scripts/check-omg-contract-snapshot.py +12 -0
  163. package/scripts/check-omg-public-ready.py +193 -0
  164. package/scripts/check-omg-standalone-clean.py +103 -0
  165. package/scripts/legacy_to_omg_migrate.py +29 -0
  166. package/scripts/migrate-legacy.py +464 -0
  167. package/scripts/omc_to_omg_migrate.py +12 -0
  168. package/scripts/omg.py +492 -0
  169. package/scripts/settings-merge.py +283 -0
  170. package/scripts/verify-standalone.sh +8 -4
  171. package/settings.json +126 -29
  172. package/templates/profile.yaml +1 -1
  173. package/tools/__init__.py +2 -0
  174. package/tools/browser_consent.py +289 -0
  175. package/tools/browser_stealth.py +481 -0
  176. package/tools/browser_tool.py +448 -0
  177. package/tools/changelog_generator.py +347 -0
  178. package/tools/commit_splitter.py +746 -0
  179. package/tools/config_discovery.py +151 -0
  180. package/tools/config_merger.py +449 -0
  181. package/tools/dashboard_generator.py +300 -0
  182. package/tools/git_inspector.py +298 -0
  183. package/tools/lsp_client.py +275 -0
  184. package/tools/lsp_discovery.py +231 -0
  185. package/tools/lsp_operations.py +392 -0
  186. package/tools/pr_generator.py +404 -0
  187. package/tools/python_repl.py +656 -0
  188. package/tools/python_sandbox.py +609 -0
  189. package/tools/search_providers/__init__.py +77 -0
  190. package/tools/search_providers/brave.py +115 -0
  191. package/tools/search_providers/exa.py +116 -0
  192. package/tools/search_providers/jina.py +104 -0
  193. package/tools/search_providers/perplexity.py +139 -0
  194. package/tools/search_providers/synthetic.py +74 -0
  195. package/tools/session_snapshot.py +736 -0
  196. package/tools/ssh_manager.py +912 -0
  197. package/tools/theme_engine.py +294 -0
  198. package/tools/theme_selector.py +137 -0
  199. package/tools/web_search.py +622 -0
  200. package/yaml.py +321 -0
  201. package/.claude-plugin/scripts/install.sh +0 -9
  202. package/bun.lock +0 -23
  203. package/bunfig.toml +0 -3
  204. package/hooks/_budget.ts +0 -1
  205. package/hooks/_common.ts +0 -63
  206. package/hooks/circuit-breaker.ts +0 -101
  207. package/hooks/config-guard.ts +0 -4
  208. package/hooks/firewall.ts +0 -20
  209. package/hooks/policy_engine.ts +0 -156
  210. package/hooks/post-tool-failure.ts +0 -22
  211. package/hooks/post-write.ts +0 -4
  212. package/hooks/pre-tool-inject.ts +0 -4
  213. package/hooks/prompt-enhancer.ts +0 -46
  214. package/hooks/quality-runner.ts +0 -24
  215. package/hooks/secret-guard.ts +0 -4
  216. package/hooks/session-end-capture.ts +0 -19
  217. package/hooks/session-start.ts +0 -19
  218. package/hooks/shadow_manager.ts +0 -81
  219. package/hooks/stop-gate.ts +0 -22
  220. package/hooks/stop_dispatcher.ts +0 -147
  221. package/hooks/test-generator-hook.ts +0 -4
  222. package/hooks/tool-ledger.ts +0 -27
  223. package/hooks/trust_review.ts +0 -175
  224. package/lab/pipeline.ts +0 -75
  225. package/lab/policies.ts +0 -68
  226. package/runtime/common.ts +0 -111
  227. package/runtime/compat.ts +0 -174
  228. package/runtime/dispatcher.ts +0 -25
  229. package/runtime/ecosystem.ts +0 -186
  230. package/runtime/provider_bootstrap.ts +0 -99
  231. package/runtime/provider_smoke.ts +0 -34
  232. package/runtime/release_readiness.ts +0 -186
  233. package/runtime/team_router.ts +0 -144
  234. package/scripts/check-omg-compat-contract-snapshot.ts +0 -20
  235. package/scripts/check-omg-standalone-clean.ts +0 -12
  236. package/scripts/check-runtime-clean.ts +0 -94
  237. package/scripts/omg.ts +0 -352
  238. package/scripts/settings-merge.ts +0 -93
  239. package/tools/commit_splitter.ts +0 -23
  240. package/tools/git_inspector.ts +0 -18
  241. package/tools/session_snapshot.ts +0 -47
  242. package/trac3er-oh-my-god-2.0.0.tgz +0 -0
  243. package/tsconfig.json +0 -15
@@ -0,0 +1,423 @@
1
+ """Package manifest detector — scans project directories for dependency manifests.
2
+
3
+ Supports: package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, pyproject.toml.
4
+ Returns a unified DependencyList with all discovered packages.
5
+
6
+ stdlib only — no external dependencies.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import re
14
+ import sys
15
+ from dataclasses import dataclass, field
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+
20
+ # ─── Data Classes ─────────────────────────────────────────────────────────────
21
+
22
+
23
+ @dataclass
24
+ class ManifestFile:
25
+ """Represents a discovered manifest file."""
26
+
27
+ path: str
28
+ format: str
29
+
30
+
31
+ @dataclass
32
+ class Package:
33
+ """Represents a single dependency package."""
34
+
35
+ name: str
36
+ version: str
37
+ dev: bool
38
+ source_manifest: str
39
+
40
+
41
+ @dataclass
42
+ class DependencyList:
43
+ """Unified result of manifest detection."""
44
+
45
+ manifests: list[ManifestFile] = field(default_factory=list)
46
+ packages: list[Package] = field(default_factory=list)
47
+
48
+
49
+ # ─── Known manifest filenames ────────────────────────────────────────────────
50
+
51
+ _MANIFEST_FILES: list[tuple[str, str]] = [
52
+ ("package.json", "package.json"),
53
+ ("requirements.txt", "requirements.txt"),
54
+ ("Cargo.toml", "Cargo.toml"),
55
+ ("go.mod", "go.mod"),
56
+ ("Gemfile", "Gemfile"),
57
+ ("pyproject.toml", "pyproject.toml"),
58
+ ]
59
+
60
+
61
+ # ─── Parsers ─────────────────────────────────────────────────────────────────
62
+
63
+
64
+ def _parse_package_json(file_path: str) -> list[Package]:
65
+ """Parse package.json for dependencies and devDependencies."""
66
+ packages: list[Package] = []
67
+ try:
68
+ with open(file_path, "r") as f:
69
+ data = json.load(f)
70
+ except (json.JSONDecodeError, OSError):
71
+ return packages
72
+
73
+ deps = data.get("dependencies", {})
74
+ if isinstance(deps, dict):
75
+ for name, version in deps.items():
76
+ packages.append(Package(
77
+ name=name,
78
+ version=str(version),
79
+ dev=False,
80
+ source_manifest=file_path,
81
+ ))
82
+
83
+ dev_deps = data.get("devDependencies", {})
84
+ if isinstance(dev_deps, dict):
85
+ for name, version in dev_deps.items():
86
+ packages.append(Package(
87
+ name=name,
88
+ version=str(version),
89
+ dev=True,
90
+ source_manifest=file_path,
91
+ ))
92
+
93
+ return packages
94
+
95
+
96
+ def _parse_requirements_txt(file_path: str) -> list[Package]:
97
+ """Parse requirements.txt (name==version, name>=version, or bare name)."""
98
+ packages: list[Package] = []
99
+ try:
100
+ with open(file_path, "r") as f:
101
+ lines = f.readlines()
102
+ except OSError:
103
+ return packages
104
+
105
+ # Matches: name==version, name>=version, name<=version, name~=version, name!=version
106
+ req_re = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(?:[><=!~]+\s*(.+))?$")
107
+
108
+ for line in lines:
109
+ line = line.strip()
110
+ if not line or line.startswith("#") or line.startswith("-"):
111
+ continue
112
+ m = req_re.match(line)
113
+ if m:
114
+ name = m.group(1)
115
+ version = m.group(2) or ""
116
+ packages.append(Package(
117
+ name=name,
118
+ version=version.strip(),
119
+ dev=False,
120
+ source_manifest=file_path,
121
+ ))
122
+
123
+ return packages
124
+
125
+
126
+ def _parse_cargo_toml(file_path: str) -> list[Package]:
127
+ """Parse Cargo.toml [dependencies] and [dev-dependencies] via regex."""
128
+ packages: list[Package] = []
129
+ try:
130
+ with open(file_path, "r") as f:
131
+ content = f.read()
132
+ except OSError:
133
+ return packages
134
+
135
+ # Split into sections by [header]
136
+ section_re = re.compile(r"^\[([^\]]+)\]\s*$", re.MULTILINE)
137
+ sections: dict[str, str] = {}
138
+ positions = [(m.group(1).strip(), m.end()) for m in section_re.finditer(content)]
139
+
140
+ for i, (name, start) in enumerate(positions):
141
+ end = positions[i + 1][1] if i + 1 < len(positions) else len(content)
142
+ # Adjust end to be the start of the next section header line
143
+ if i + 1 < len(positions):
144
+ # Find the start of the next header line
145
+ next_header_start = content.rfind("[", start, end)
146
+ if next_header_start >= 0:
147
+ end = next_header_start
148
+ sections[name] = content[start:end]
149
+
150
+ # Parse dependency lines: name = "version" or name = { version = "ver", ... }
151
+ dep_line_re = re.compile(
152
+ r'^([A-Za-z0-9_][A-Za-z0-9_-]*)\s*=\s*(?:"([^"]+)"|'
153
+ r'\{[^}]*version\s*=\s*"([^"]+)"[^}]*\})',
154
+ re.MULTILINE,
155
+ )
156
+
157
+ for section_name, section_body in sections.items():
158
+ is_dev = "dev-dependencies" in section_name.lower()
159
+ is_dep = "dependencies" in section_name.lower()
160
+ if not is_dep:
161
+ continue
162
+
163
+ for m in dep_line_re.finditer(section_body):
164
+ name = m.group(1)
165
+ version = m.group(2) or m.group(3) or ""
166
+ packages.append(Package(
167
+ name=name,
168
+ version=version,
169
+ dev=is_dev,
170
+ source_manifest=file_path,
171
+ ))
172
+
173
+ return packages
174
+
175
+
176
+ def _parse_go_mod(file_path: str) -> list[Package]:
177
+ """Parse go.mod require block."""
178
+ packages: list[Package] = []
179
+ try:
180
+ with open(file_path, "r") as f:
181
+ content = f.read()
182
+ except OSError:
183
+ return packages
184
+
185
+ # Match require ( ... ) block
186
+ require_block_re = re.compile(r"require\s*\(\s*(.*?)\s*\)", re.DOTALL)
187
+ for block_m in require_block_re.finditer(content):
188
+ block = block_m.group(1)
189
+ for line in block.strip().splitlines():
190
+ line = line.strip()
191
+ if not line or line.startswith("//"):
192
+ continue
193
+ parts = line.split()
194
+ if len(parts) >= 2:
195
+ packages.append(Package(
196
+ name=parts[0],
197
+ version=parts[1],
198
+ dev=False,
199
+ source_manifest=file_path,
200
+ ))
201
+
202
+ # Also match single-line require: require github.com/foo/bar v1.0.0
203
+ single_re = re.compile(r"^require\s+(\S+)\s+(\S+)", re.MULTILINE)
204
+ for m in single_re.finditer(content):
205
+ packages.append(Package(
206
+ name=m.group(1),
207
+ version=m.group(2),
208
+ dev=False,
209
+ source_manifest=file_path,
210
+ ))
211
+
212
+ return packages
213
+
214
+
215
+ def _parse_gemfile(file_path: str) -> list[Package]:
216
+ """Parse Gemfile gem declarations."""
217
+ packages: list[Package] = []
218
+ try:
219
+ with open(file_path, "r") as f:
220
+ lines = f.readlines()
221
+ except OSError:
222
+ return packages
223
+
224
+ # gem "name", "version" or gem 'name', 'version' or gem "name"
225
+ gem_re = re.compile(
226
+ r"""gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?"""
227
+ )
228
+
229
+ for line in lines:
230
+ line = line.strip()
231
+ if not line or line.startswith("#"):
232
+ continue
233
+ m = gem_re.search(line)
234
+ if m:
235
+ name = m.group(1)
236
+ version = m.group(2) or ""
237
+ packages.append(Package(
238
+ name=name,
239
+ version=version,
240
+ dev=False,
241
+ source_manifest=file_path,
242
+ ))
243
+
244
+ return packages
245
+
246
+
247
+ def _parse_pyproject_toml(file_path: str) -> list[Package]:
248
+ """Parse pyproject.toml for [project.dependencies] and optional-dependencies."""
249
+ packages: list[Package] = []
250
+ try:
251
+ with open(file_path, "r") as f:
252
+ content = f.read()
253
+ except OSError:
254
+ return packages
255
+
256
+ # Extract PEP 508 name and version from dependency string like "fastapi>=0.100.0"
257
+ pep508_re = re.compile(r"^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*(.*)$")
258
+
259
+ def _extract_deps_from_array(text: str, is_dev: bool) -> list[Package]:
260
+ """Extract packages from a TOML array literal."""
261
+ result: list[Package] = []
262
+ # Match quoted strings inside brackets
263
+ str_re = re.compile(r'["\']([^"\']+)["\']')
264
+ for m in str_re.finditer(text):
265
+ dep_str = m.group(1).strip()
266
+ pm = pep508_re.match(dep_str)
267
+ if pm:
268
+ name = pm.group(1)
269
+ version = pm.group(2).strip()
270
+ result.append(Package(
271
+ name=name,
272
+ version=version,
273
+ dev=is_dev,
274
+ source_manifest=file_path,
275
+ ))
276
+ return result
277
+
278
+ # Find dependencies = [...] under [project]
279
+ # Simple approach: find "dependencies = [" after [project] section
280
+ project_deps_re = re.compile(
281
+ r"\[project\].*?^dependencies\s*=\s*\[(.*?)\]",
282
+ re.MULTILINE | re.DOTALL,
283
+ )
284
+ pm = project_deps_re.search(content)
285
+ if pm:
286
+ packages.extend(_extract_deps_from_array(pm.group(1), is_dev=False))
287
+
288
+ # Find [project.optional-dependencies] sections
289
+ opt_deps_re = re.compile(
290
+ r"\[project\.optional-dependencies\].*?$",
291
+ re.MULTILINE,
292
+ )
293
+ om = opt_deps_re.search(content)
294
+ if om:
295
+ # Extract the section body until next [section] or end of file
296
+ start = om.end()
297
+ next_section = re.search(r"^\[", content[start:], re.MULTILINE)
298
+ end = start + next_section.start() if next_section else len(content)
299
+ section_body = content[start:end]
300
+
301
+ # Find key = [...] arrays (e.g., dev = ["pytest>=7.4.0"])
302
+ array_re = re.compile(r"^\w+\s*=\s*\[(.*?)\]", re.MULTILINE | re.DOTALL)
303
+ for am in array_re.finditer(section_body):
304
+ packages.extend(_extract_deps_from_array(am.group(1), is_dev=True))
305
+
306
+ # Also support [tool.poetry.dependencies] pattern
307
+ poetry_deps_re = re.compile(
308
+ r"\[tool\.poetry\.dependencies\](.*?)(?=\[|$)",
309
+ re.DOTALL,
310
+ )
311
+ pm = poetry_deps_re.search(content)
312
+ if pm:
313
+ section_body = pm.group(1)
314
+ # Poetry deps: name = "version" or name = {version = "ver"}
315
+ dep_line_re = re.compile(
316
+ r'^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*(?:"([^"]+)"|'
317
+ r"\{[^}]*version\s*=\s*\"([^\"]+)\"[^}]*\})",
318
+ re.MULTILINE,
319
+ )
320
+ for dm in dep_line_re.finditer(section_body):
321
+ name = dm.group(1)
322
+ if name.lower() == "python":
323
+ continue # Skip python version constraint
324
+ version = dm.group(2) or dm.group(3) or ""
325
+ packages.append(Package(
326
+ name=name,
327
+ version=version,
328
+ dev=False,
329
+ source_manifest=file_path,
330
+ ))
331
+
332
+ # [tool.poetry.dev-dependencies]
333
+ poetry_dev_re = re.compile(
334
+ r"\[tool\.poetry\.dev-dependencies\](.*?)(?=\[|$)",
335
+ re.DOTALL,
336
+ )
337
+ pm = poetry_dev_re.search(content)
338
+ if pm:
339
+ section_body = pm.group(1)
340
+ dep_line_re = re.compile(
341
+ r'^([A-Za-z0-9_][A-Za-z0-9._-]*)\s*=\s*(?:"([^"]+)"|'
342
+ r"\{[^}]*version\s*=\s*\"([^\"]+)\"[^}]*\})",
343
+ re.MULTILINE,
344
+ )
345
+ for dm in dep_line_re.finditer(section_body):
346
+ name = dm.group(1)
347
+ version = dm.group(2) or dm.group(3) or ""
348
+ packages.append(Package(
349
+ name=name,
350
+ version=version,
351
+ dev=True,
352
+ source_manifest=file_path,
353
+ ))
354
+
355
+ return packages
356
+
357
+
358
+ # ─── Parser dispatch ─────────────────────────────────────────────────────────
359
+
360
+ _PARSERS: dict[str, Any] = {
361
+ "package.json": _parse_package_json,
362
+ "requirements.txt": _parse_requirements_txt,
363
+ "Cargo.toml": _parse_cargo_toml,
364
+ "go.mod": _parse_go_mod,
365
+ "Gemfile": _parse_gemfile,
366
+ "pyproject.toml": _parse_pyproject_toml,
367
+ }
368
+
369
+
370
+ # ─── Public API ──────────────────────────────────────────────────────────────
371
+
372
+
373
+ def _dep_health_enabled() -> bool:
374
+ env_val = os.environ.get("OMG_DEP_HEALTH_ENABLED", "").lower()
375
+ if env_val in ("1", "true", "yes"):
376
+ return True
377
+ if env_val in ("0", "false", "no"):
378
+ return False
379
+ try:
380
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
381
+ from hooks._common import get_feature_flag
382
+ return get_feature_flag("DEP_HEALTH", default=False)
383
+ except Exception:
384
+ return False
385
+
386
+
387
+ def detect_manifests(project_dir: str) -> DependencyList:
388
+ """Scan project_dir for manifest files and return a unified DependencyList.
389
+
390
+ Supports: package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, pyproject.toml.
391
+ Gracefully handles missing/malformed files (skips, no crash).
392
+ """
393
+ if not _dep_health_enabled():
394
+ return DependencyList()
395
+
396
+ result = DependencyList()
397
+ project_path = Path(project_dir)
398
+
399
+ if not project_path.is_dir():
400
+ return result
401
+
402
+ for filename, fmt in _MANIFEST_FILES:
403
+ file_path = project_path / filename
404
+ if not file_path.is_file():
405
+ continue
406
+
407
+ parser = _PARSERS.get(fmt)
408
+ if not parser:
409
+ continue
410
+
411
+ try:
412
+ packages = parser(str(file_path))
413
+ if packages:
414
+ result.manifests.append(ManifestFile(
415
+ path=str(file_path),
416
+ format=fmt,
417
+ ))
418
+ result.packages.extend(packages)
419
+ except Exception:
420
+ # Graceful degradation: skip malformed files
421
+ continue
422
+
423
+ return result
@@ -0,0 +1,169 @@
1
+ import ast
2
+ import re
3
+ from pathlib import Path
4
+ from typing import Any
5
+
6
+
7
+ _CRITICAL_PATTERN = re.compile(r"\bcritical\b", re.IGNORECASE)
8
+ _HIGH_PATTERN = re.compile(r"\bhigh\b", re.IGNORECASE)
9
+
10
+
11
+ def analyze_reachability(cve_result: dict[str, Any], project_dir: str) -> dict[str, Any]:
12
+ package = str(cve_result.get("package", "")).strip()
13
+ cve_id = str(cve_result.get("id", "")).strip()
14
+ summary = str(cve_result.get("summary", "")).strip()
15
+ fixed_version = str(cve_result.get("fixed_version", "")).strip()
16
+
17
+ imported_files: list[str] = []
18
+ usage_found = False
19
+
20
+ for py_file in Path(project_dir).rglob("*.py"):
21
+ try:
22
+ source = py_file.read_text(encoding="utf-8")
23
+ except Exception:
24
+ continue
25
+
26
+ imported, used = _inspect_python_file(source, package)
27
+ if imported:
28
+ imported_files.append(py_file.relative_to(project_dir).as_posix())
29
+ if used:
30
+ usage_found = True
31
+
32
+ reachability = _classify_reachability(imported_files, usage_found)
33
+ risk_level = _classify_risk(reachability, summary)
34
+ recommendation = _build_recommendation(package, fixed_version, reachability)
35
+
36
+ return {
37
+ "package": package,
38
+ "cve_id": cve_id,
39
+ "reachability": reachability,
40
+ "import_locations": sorted(imported_files),
41
+ "risk_level": risk_level,
42
+ "recommendation": recommendation,
43
+ }
44
+
45
+
46
+ def _inspect_python_file(source: str, package: str) -> tuple[bool, bool]:
47
+ imported, module_aliases, imported_symbols = _grep_like_import_scan(source, package)
48
+
49
+ try:
50
+ tree = ast.parse(source)
51
+ except SyntaxError:
52
+ return imported, False
53
+
54
+ ast_imported, ast_module_aliases, ast_imported_symbols = _ast_import_scan(tree, package)
55
+ imported = imported or ast_imported
56
+ module_aliases.update(ast_module_aliases)
57
+ imported_symbols.update(ast_imported_symbols)
58
+
59
+ used = _ast_usage_scan(tree, module_aliases, imported_symbols)
60
+ return imported, used
61
+
62
+
63
+ def _grep_like_import_scan(source: str, package: str) -> tuple[bool, set[str], set[str]]:
64
+ imported = False
65
+ module_aliases: set[str] = set()
66
+ imported_symbols: set[str] = set()
67
+
68
+ if not package:
69
+ return imported, module_aliases, imported_symbols
70
+
71
+ pkg = re.escape(package)
72
+ import_re = re.compile(rf"^\s*import\s+{pkg}(?:\.|\s|,|$)", re.MULTILINE)
73
+ from_re = re.compile(rf"^\s*from\s+{pkg}(?:\.|\s)", re.MULTILINE)
74
+ alias_re = re.compile(rf"^\s*import\s+{pkg}\s+as\s+([A-Za-z_][A-Za-z0-9_]*)", re.MULTILINE)
75
+ from_names_re = re.compile(rf"^\s*from\s+{pkg}(?:\.[A-Za-z0-9_\.]+)?\s+import\s+(.+)$", re.MULTILINE)
76
+
77
+ if import_re.search(source) or from_re.search(source):
78
+ imported = True
79
+
80
+ module_aliases.add(package.split(".")[0])
81
+ for match in alias_re.findall(source):
82
+ module_aliases.add(match)
83
+
84
+ for line in from_names_re.findall(source):
85
+ for item in line.split(","):
86
+ part = item.strip()
87
+ if not part:
88
+ continue
89
+ if " as " in part:
90
+ imported_symbols.add(part.rsplit(" as ", 1)[1].strip())
91
+ else:
92
+ imported_symbols.add(part)
93
+
94
+ return imported, module_aliases, imported_symbols
95
+
96
+
97
+ def _ast_import_scan(tree: ast.AST, package: str) -> tuple[bool, set[str], set[str]]:
98
+ imported = False
99
+ module_aliases: set[str] = set()
100
+ imported_symbols: set[str] = set()
101
+
102
+ if not package:
103
+ return imported, module_aliases, imported_symbols
104
+
105
+ root = package.split(".")[0]
106
+
107
+ for node in ast.walk(tree):
108
+ if isinstance(node, ast.Import):
109
+ for alias in node.names:
110
+ if alias.name == package or alias.name.startswith(f"{package}."):
111
+ imported = True
112
+ module_aliases.add(alias.asname or alias.name.split(".")[0])
113
+ elif alias.name.split(".")[0] == root:
114
+ imported = True
115
+ module_aliases.add(alias.asname or alias.name.split(".")[0])
116
+ elif isinstance(node, ast.ImportFrom) and node.module:
117
+ if node.module == package or node.module.startswith(f"{package}."):
118
+ imported = True
119
+ for alias in node.names:
120
+ imported_symbols.add(alias.asname or alias.name)
121
+ elif node.module.split(".")[0] == root:
122
+ imported = True
123
+ for alias in node.names:
124
+ imported_symbols.add(alias.asname or alias.name)
125
+
126
+ return imported, module_aliases, imported_symbols
127
+
128
+
129
+ def _ast_usage_scan(tree: ast.AST, module_aliases: set[str], imported_symbols: set[str]) -> bool:
130
+ for node in ast.walk(tree):
131
+ if not isinstance(node, ast.Call):
132
+ continue
133
+
134
+ func = node.func
135
+ if isinstance(func, ast.Attribute) and isinstance(func.value, ast.Name):
136
+ if func.value.id in module_aliases or func.value.id in imported_symbols:
137
+ return True
138
+ elif isinstance(func, ast.Name) and func.id in imported_symbols:
139
+ return True
140
+
141
+ return False
142
+
143
+
144
+ def _classify_reachability(imported_files: list[str], usage_found: bool) -> str:
145
+ if usage_found:
146
+ return "REACHABLE"
147
+ if imported_files:
148
+ return "POTENTIALLY_REACHABLE"
149
+ return "UNREACHABLE"
150
+
151
+
152
+ def _classify_risk(reachability: str, summary: str) -> str:
153
+ if reachability == "UNREACHABLE":
154
+ return "LOW"
155
+ if reachability == "POTENTIALLY_REACHABLE":
156
+ return "MEDIUM"
157
+ if _CRITICAL_PATTERN.search(summary):
158
+ return "CRITICAL"
159
+ if _HIGH_PATTERN.search(summary):
160
+ return "HIGH"
161
+ return "HIGH"
162
+
163
+
164
+ def _build_recommendation(package: str, fixed_version: str, reachability: str) -> str:
165
+ if reachability == "UNREACHABLE":
166
+ return "No action needed (unreachable)"
167
+ if fixed_version:
168
+ return f"Upgrade {package} to {fixed_version}"
169
+ return f"Upgrade {package} to a fixed version"
File without changes