@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,271 @@
1
+ """
2
+ Framework Detector — detect test frameworks from project configuration files.
3
+
4
+ Scans a project directory for known config files (package.json, pyproject.toml,
5
+ setup.cfg, Cargo.toml, go.mod, Gemfile) and returns a FrameworkInfo dataclass
6
+ describing the detected framework(s).
7
+
8
+ Feature flag: TEST_GENERATION (default False) — detection works regardless.
9
+ Stdlib only: json, pathlib, re, dataclasses.
10
+ """
11
+ import json
12
+ import re
13
+ from dataclasses import dataclass, field
14
+ from pathlib import Path
15
+
16
+
17
+ @dataclass
18
+ class FrameworkInfo:
19
+ """Describes a detected test framework and its conventions."""
20
+
21
+ framework: str = "unknown"
22
+ config_file: str = ""
23
+ test_dir: str = ""
24
+ assertion_style: str = ""
25
+ mock_library: str = ""
26
+ multi_framework: list[str] = field(default_factory=list)
27
+
28
+
29
+ # --- JS/TS framework metadata ---
30
+
31
+ _JS_FRAMEWORKS = {
32
+ "vitest": {
33
+ "framework": "vitest",
34
+ "assertion_style": "expect",
35
+ "mock_library": "vi.mock",
36
+ "test_dir": "__tests__",
37
+ },
38
+ "jest": {
39
+ "framework": "jest",
40
+ "assertion_style": "expect",
41
+ "mock_library": "jest.mock",
42
+ "test_dir": "__tests__",
43
+ },
44
+ "mocha": {
45
+ "framework": "mocha",
46
+ "assertion_style": "assert",
47
+ "mock_library": "sinon",
48
+ "test_dir": "test",
49
+ },
50
+ "@playwright/test": {
51
+ "framework": "playwright",
52
+ "assertion_style": "expect",
53
+ "mock_library": "",
54
+ "test_dir": "tests",
55
+ },
56
+ }
57
+
58
+ # Detection priority: vitest > jest > mocha (vitest first because projects
59
+ # migrating from jest often keep jest in devDeps alongside vitest)
60
+ _JS_PRIORITY = ["vitest", "jest", "mocha", "@playwright/test"]
61
+
62
+
63
+ def _detect_from_package_json(project_dir: str) -> FrameworkInfo | None:
64
+ """Detect JS/TS framework from package.json devDependencies and scripts."""
65
+ pkg_path = Path(project_dir) / "package.json"
66
+ if not pkg_path.is_file():
67
+ return None
68
+
69
+ try:
70
+ with open(pkg_path, "r", encoding="utf-8") as f:
71
+ pkg = json.load(f)
72
+ except (json.JSONDecodeError, OSError):
73
+ return None
74
+
75
+ dev_deps = pkg.get("devDependencies", {})
76
+ deps = pkg.get("dependencies", {})
77
+ scripts = pkg.get("scripts", {})
78
+ all_deps = {**deps, **dev_deps}
79
+
80
+ # Also check scripts for framework names
81
+ scripts_text = " ".join(str(v) for v in scripts.values())
82
+
83
+ detected = []
84
+ for fw_key in _JS_PRIORITY:
85
+ if fw_key in all_deps:
86
+ detected.append(fw_key)
87
+ elif fw_key.lstrip("@").split("/")[-1] in scripts_text:
88
+ detected.append(fw_key)
89
+
90
+ if not detected:
91
+ return None
92
+
93
+ primary_key = detected[0]
94
+ meta = _JS_FRAMEWORKS[primary_key]
95
+
96
+ multi = []
97
+ if len(detected) > 1:
98
+ multi = [_JS_FRAMEWORKS[k]["framework"] for k in detected]
99
+
100
+ return FrameworkInfo(
101
+ framework=meta["framework"],
102
+ config_file="package.json",
103
+ test_dir=meta["test_dir"],
104
+ assertion_style=meta["assertion_style"],
105
+ mock_library=meta["mock_library"],
106
+ multi_framework=multi,
107
+ )
108
+
109
+
110
+ def _detect_from_pyproject_toml(project_dir: str) -> FrameworkInfo | None:
111
+ """Detect pytest from pyproject.toml [tool.pytest.*] section."""
112
+ path = Path(project_dir) / "pyproject.toml"
113
+ if not path.is_file():
114
+ return None
115
+
116
+ try:
117
+ content = path.read_text(encoding="utf-8")
118
+ except OSError:
119
+ return None
120
+
121
+ if re.search(r"\[tool\.pytest", content):
122
+ # Try to extract testpaths
123
+ test_dir = "tests"
124
+ m = re.search(r'testpaths\s*=\s*\["?([^"\]\s]+)', content)
125
+ if m:
126
+ test_dir = m.group(1)
127
+
128
+ return FrameworkInfo(
129
+ framework="pytest",
130
+ config_file="pyproject.toml",
131
+ test_dir=test_dir,
132
+ assertion_style="assert",
133
+ mock_library="unittest.mock",
134
+ multi_framework=[],
135
+ )
136
+
137
+ # Check for pytest in dependencies
138
+ if re.search(r"pytest", content):
139
+ return FrameworkInfo(
140
+ framework="pytest",
141
+ config_file="pyproject.toml",
142
+ test_dir="tests",
143
+ assertion_style="assert",
144
+ mock_library="unittest.mock",
145
+ multi_framework=[],
146
+ )
147
+
148
+ return None
149
+
150
+
151
+ def _detect_from_setup_cfg(project_dir: str) -> FrameworkInfo | None:
152
+ """Detect pytest from setup.cfg [tool:pytest] section."""
153
+ path = Path(project_dir) / "setup.cfg"
154
+ if not path.is_file():
155
+ return None
156
+
157
+ try:
158
+ content = path.read_text(encoding="utf-8")
159
+ except OSError:
160
+ return None
161
+
162
+ if re.search(r"\[tool:pytest\]", content):
163
+ test_dir = "tests"
164
+ m = re.search(r"testpaths\s*=\s*(\S+)", content)
165
+ if m:
166
+ test_dir = m.group(1)
167
+
168
+ return FrameworkInfo(
169
+ framework="pytest",
170
+ config_file="setup.cfg",
171
+ test_dir=test_dir,
172
+ assertion_style="assert",
173
+ mock_library="unittest.mock",
174
+ multi_framework=[],
175
+ )
176
+
177
+ return None
178
+
179
+
180
+ def _detect_from_go_mod(project_dir: str) -> FrameworkInfo | None:
181
+ """Detect Go test from go.mod presence."""
182
+ path = Path(project_dir) / "go.mod"
183
+ if not path.is_file():
184
+ return None
185
+
186
+ return FrameworkInfo(
187
+ framework="go test",
188
+ config_file="go.mod",
189
+ test_dir=".",
190
+ assertion_style="testing.T",
191
+ mock_library="testify/mock",
192
+ multi_framework=[],
193
+ )
194
+
195
+
196
+ def _detect_from_cargo_toml(project_dir: str) -> FrameworkInfo | None:
197
+ """Detect Rust/cargo test from Cargo.toml presence."""
198
+ path = Path(project_dir) / "Cargo.toml"
199
+ if not path.is_file():
200
+ return None
201
+
202
+ return FrameworkInfo(
203
+ framework="cargo test",
204
+ config_file="Cargo.toml",
205
+ test_dir="tests",
206
+ assertion_style="assert!",
207
+ mock_library="mockall",
208
+ multi_framework=[],
209
+ )
210
+
211
+
212
+ def _detect_from_gemfile(project_dir: str) -> FrameworkInfo | None:
213
+ """Detect RSpec from Gemfile containing 'rspec'."""
214
+ path = Path(project_dir) / "Gemfile"
215
+ if not path.is_file():
216
+ return None
217
+
218
+ try:
219
+ content = path.read_text(encoding="utf-8")
220
+ except OSError:
221
+ return None
222
+
223
+ if re.search(r"['\"]rspec['\"]", content):
224
+ return FrameworkInfo(
225
+ framework="rspec",
226
+ config_file="Gemfile",
227
+ test_dir="spec",
228
+ assertion_style="expect",
229
+ mock_library="rspec-mocks",
230
+ multi_framework=[],
231
+ )
232
+
233
+ return None
234
+
235
+
236
+ # Ordered detector chain — first match wins (except multi-framework from JS)
237
+ _DETECTORS = [
238
+ _detect_from_package_json,
239
+ _detect_from_pyproject_toml,
240
+ _detect_from_setup_cfg,
241
+ _detect_from_go_mod,
242
+ _detect_from_cargo_toml,
243
+ _detect_from_gemfile,
244
+ ]
245
+
246
+
247
+ def detect_test_framework(project_dir: str) -> FrameworkInfo:
248
+ """Detect test framework(s) in a project directory.
249
+
250
+ Scans known config files in priority order. Returns FrameworkInfo with
251
+ framework='unknown' if nothing detected (never crashes).
252
+
253
+ Feature flag TEST_GENERATION gates downstream generation — detection
254
+ itself always works.
255
+
256
+ Args:
257
+ project_dir: Absolute or relative path to the project root.
258
+
259
+ Returns:
260
+ FrameworkInfo dataclass with detected framework metadata.
261
+ """
262
+ try:
263
+ for detector in _DETECTORS:
264
+ result = detector(project_dir)
265
+ if result is not None:
266
+ return result
267
+ except Exception:
268
+ # Crash isolation: return unknown on any unexpected error
269
+ pass
270
+
271
+ return FrameworkInfo()
@@ -0,0 +1,219 @@
1
+ """Generate lightweight test skeletons from source files.
2
+
3
+ Python symbol extraction uses ``ast`` for precise parsing of top-level public
4
+ functions, classes, and class methods.
5
+
6
+ JavaScript/TypeScript symbol extraction uses regular expressions for exported
7
+ declarations (``export function``, ``export const``, ``export default
8
+ function``, ``export class``). This regex approach is intentionally lightweight
9
+ and expected to be about 70-80% accurate for common code styles.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import ast
15
+ from pathlib import Path
16
+ import re
17
+
18
+
19
+ _RE_JS_EXPORT_FUNCTION = re.compile(r"(?:^|\s)export\s+function\s+([A-Za-z_$][\w$]*)\s*\(")
20
+ _RE_JS_EXPORT_CONST = re.compile(r"(?:^|\s)export\s+const\s+([A-Za-z_$][\w$]*)\s*=")
21
+ _RE_JS_EXPORT_DEFAULT_FUNCTION = re.compile(
22
+ r"(?:^|\s)export\s+default\s+function(?:\s+([A-Za-z_$][\w$]*))?\s*\("
23
+ )
24
+ _RE_JS_EXPORT_CLASS = re.compile(r"(?:^|\s)export\s+class\s+([A-Za-z_$][\w$]*)\b")
25
+
26
+
27
+ def generate_test_skeleton(source_file: str, framework_info: dict[str, object]) -> str:
28
+ path = Path(source_file)
29
+ if not path.exists():
30
+ return ""
31
+
32
+ source_text = path.read_text(encoding="utf-8")
33
+ if not source_text.strip():
34
+ return ""
35
+
36
+ framework = str(framework_info.get("framework", "")).lower()
37
+
38
+ functions: list[str]
39
+ classes: dict[str, list[str]]
40
+ if path.suffix == ".py":
41
+ functions, classes = _extract_python_symbols(source_text)
42
+ else:
43
+ functions, classes = _extract_js_ts_symbols(source_text)
44
+
45
+ if framework == "pytest":
46
+ return _render_pytest(functions, classes)
47
+ if framework in {"jest", "vitest"}:
48
+ return _render_jest_vitest(functions, classes)
49
+ if framework == "go":
50
+ return _render_go(functions, classes)
51
+ return _render_generic(functions, classes)
52
+
53
+
54
+ def _extract_python_symbols(source_text: str) -> tuple[list[str], dict[str, list[str]]]:
55
+ functions: list[str] = []
56
+ classes: dict[str, list[str]] = {}
57
+
58
+ tree = ast.parse(source_text)
59
+ for node in tree.body:
60
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and not node.name.startswith("_"):
61
+ functions.append(node.name)
62
+ elif isinstance(node, ast.ClassDef) and not node.name.startswith("_"):
63
+ public_methods: list[str] = []
64
+ for member in node.body:
65
+ if isinstance(member, (ast.FunctionDef, ast.AsyncFunctionDef)) and not member.name.startswith("_"):
66
+ public_methods.append(member.name)
67
+ classes[node.name] = public_methods
68
+
69
+ return functions, classes
70
+
71
+
72
+ def _extract_js_ts_symbols(source_text: str) -> tuple[list[str], dict[str, list[str]]]:
73
+ functions: list[str] = []
74
+ classes: dict[str, list[str]] = {}
75
+
76
+ functions.extend(_RE_JS_EXPORT_FUNCTION.findall(source_text))
77
+ functions.extend(_RE_JS_EXPORT_CONST.findall(source_text))
78
+
79
+ for match in _RE_JS_EXPORT_DEFAULT_FUNCTION.findall(source_text):
80
+ functions.append(match or "defaultExport")
81
+
82
+ for class_name in _RE_JS_EXPORT_CLASS.findall(source_text):
83
+ classes[class_name] = []
84
+
85
+ return _dedupe(functions), classes
86
+
87
+
88
+ def _render_pytest(functions: list[str], classes: dict[str, list[str]]) -> str:
89
+ lines: list[str] = []
90
+
91
+ for func_name in functions:
92
+ lines.extend(
93
+ [
94
+ f"def test_{func_name}_happy_path():",
95
+ " # TODO: implement test",
96
+ " # happy path",
97
+ " assert True",
98
+ "",
99
+ f"def test_{func_name}_error_case():",
100
+ " # TODO: implement test",
101
+ " # error case",
102
+ " assert True",
103
+ "",
104
+ ]
105
+ )
106
+
107
+ for class_name, methods in classes.items():
108
+ lines.append(f"class Test{class_name}:")
109
+ if not methods:
110
+ lines.extend([" # TODO: implement test", " pass", ""])
111
+ continue
112
+
113
+ for method_name in methods:
114
+ lines.extend(
115
+ [
116
+ f" def test_{method_name}_happy_path(self):",
117
+ " # TODO: implement test",
118
+ " # happy path",
119
+ " assert True",
120
+ "",
121
+ f" def test_{method_name}_error_case(self):",
122
+ " # TODO: implement test",
123
+ " # error case",
124
+ " assert True",
125
+ "",
126
+ ]
127
+ )
128
+
129
+ return "\n".join(lines).rstrip()
130
+
131
+
132
+ def _render_jest_vitest(functions: list[str], classes: dict[str, list[str]]) -> str:
133
+ names = functions + list(classes.keys())
134
+ blocks: list[str] = []
135
+
136
+ for name in names:
137
+ blocks.extend(
138
+ [
139
+ f"describe('{name}', () => {{",
140
+ " it('should happy path', () => {",
141
+ " // TODO: implement test",
142
+ " // happy path",
143
+ " expect(value).toBe(expected);",
144
+ " });",
145
+ "",
146
+ " it('should error case', () => {",
147
+ " // TODO: implement test",
148
+ " // error case",
149
+ " expect(value).toBe(expected);",
150
+ " });",
151
+ "});",
152
+ "",
153
+ ]
154
+ )
155
+
156
+ return "\n".join(blocks).rstrip()
157
+
158
+
159
+ def _render_go(functions: list[str], classes: dict[str, list[str]]) -> str:
160
+ names = functions + list(classes.keys())
161
+ if not names:
162
+ return ""
163
+
164
+ lines = [
165
+ "package main",
166
+ "",
167
+ 'import "testing"',
168
+ "",
169
+ ]
170
+
171
+ for name in names:
172
+ test_name = _to_pascal_case(name)
173
+ lines.extend(
174
+ [
175
+ f"func Test{test_name}(t *testing.T) {{",
176
+ " // TODO: implement test",
177
+ " // happy path",
178
+ " // error case",
179
+ "}",
180
+ "",
181
+ ]
182
+ )
183
+
184
+ return "\n".join(lines).rstrip()
185
+
186
+
187
+ def _render_generic(functions: list[str], classes: dict[str, list[str]]) -> str:
188
+ names = functions + list(classes.keys())
189
+ if not names:
190
+ return ""
191
+
192
+ lines = []
193
+ for name in names:
194
+ lines.extend(
195
+ [
196
+ f"# Test skeleton for {name}",
197
+ "# TODO: implement test",
198
+ "# happy path",
199
+ "# error case",
200
+ "",
201
+ ]
202
+ )
203
+
204
+ return "\n".join(lines).rstrip()
205
+
206
+
207
+ def _dedupe(values: list[str]) -> list[str]:
208
+ seen: set[str] = set()
209
+ ordered: list[str] = []
210
+ for value in values:
211
+ if value not in seen:
212
+ seen.add(value)
213
+ ordered.append(value)
214
+ return ordered
215
+
216
+
217
+ def _to_pascal_case(name: str) -> str:
218
+ parts = re.split(r"[^A-Za-z0-9]+", name)
219
+ return "".join(part[:1].upper() + part[1:] for part in parts if part) or "Generated"
File without changes
@@ -0,0 +1,139 @@
1
+ from __future__ import annotations
2
+
3
+ import ast
4
+ import json
5
+ from pathlib import Path
6
+ from typing import override
7
+
8
+
9
+ def parse_python_imports(file_path: str) -> list[str]:
10
+ source = Path(file_path).read_text(encoding="utf-8")
11
+ tree = ast.parse(source)
12
+ collector = _ImportCollector()
13
+ collector.visit(tree)
14
+ return collector.imports
15
+
16
+
17
+ def build_dependency_graph(project_dir: str) -> dict[str, object]:
18
+ root = Path(project_dir)
19
+ py_files = sorted(root.rglob("*.py"))
20
+
21
+ graph: dict[str, list[str]] = {}
22
+ for py_file in py_files:
23
+ module_name = _module_name_for_path(root, py_file)
24
+ imports = parse_python_imports(str(py_file))
25
+ graph[module_name] = _dedupe_preserve_order(imports)
26
+
27
+ cycles = _detect_cycles(graph)
28
+ stats = {
29
+ "module_count": len(graph),
30
+ "edge_count": sum(len(edges) for edges in graph.values()),
31
+ "max_depth": _compute_max_depth(graph),
32
+ "has_cycles": bool(cycles),
33
+ }
34
+
35
+ payload: dict[str, object] = {
36
+ "graph": graph,
37
+ "cycles": cycles,
38
+ "stats": stats,
39
+ }
40
+
41
+ output_path = root / ".omg" / "state" / "dependency-graph.json"
42
+ output_path.parent.mkdir(parents=True, exist_ok=True)
43
+ _ = output_path.write_text(json.dumps(payload, indent=2, sort_keys=True), encoding="utf-8")
44
+
45
+ return payload
46
+
47
+
48
+ class _ImportCollector(ast.NodeVisitor):
49
+ def __init__(self) -> None:
50
+ self.imports: list[str] = []
51
+
52
+ @override
53
+ def visit_Import(self, node: ast.Import) -> None: # noqa: N802
54
+ for alias in node.names:
55
+ self.imports.append(alias.name)
56
+
57
+ @override
58
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> None: # noqa: N802
59
+ if node.level > 0:
60
+ prefix = "." * node.level
61
+ self.imports.append(f"{prefix}{node.module or ''}")
62
+ return
63
+
64
+ if node.module:
65
+ self.imports.append(node.module)
66
+
67
+
68
+ def _module_name_for_path(root: Path, py_file: Path) -> str:
69
+ rel = py_file.relative_to(root).with_suffix("")
70
+ parts = list(rel.parts)
71
+ if parts and parts[-1] == "__init__":
72
+ parts = parts[:-1]
73
+ return ".".join(parts) if parts else "__init__"
74
+
75
+
76
+ def _dedupe_preserve_order(values: list[str]) -> list[str]:
77
+ seen: set[str] = set()
78
+ ordered: list[str] = []
79
+ for value in values:
80
+ if value not in seen:
81
+ seen.add(value)
82
+ ordered.append(value)
83
+ return ordered
84
+
85
+
86
+ def _detect_cycles(graph: dict[str, list[str]]) -> list[list[str]]:
87
+ visited: set[str] = set()
88
+ on_stack: set[str] = set()
89
+ stack: list[str] = []
90
+ cycle_set: set[tuple[str, ...]] = set()
91
+
92
+ def dfs(node: str) -> None:
93
+ visited.add(node)
94
+ on_stack.add(node)
95
+ stack.append(node)
96
+
97
+ for nxt in graph.get(node, []):
98
+ if nxt not in graph:
99
+ continue
100
+ if nxt not in visited:
101
+ dfs(nxt)
102
+ elif nxt in on_stack:
103
+ start_idx = stack.index(nxt)
104
+ cycle = stack[start_idx:] + [nxt]
105
+ cycle_set.add(_canonical_cycle(cycle))
106
+
107
+ _ = stack.pop()
108
+ on_stack.remove(node)
109
+
110
+ for module in graph:
111
+ if module not in visited:
112
+ dfs(module)
113
+
114
+ return [list(cycle) for cycle in sorted(cycle_set)]
115
+
116
+
117
+ def _canonical_cycle(cycle: list[str]) -> tuple[str, ...]:
118
+ core = cycle[:-1]
119
+ if not core:
120
+ return tuple(cycle)
121
+
122
+ rotations = [tuple(core[idx:] + core[:idx]) for idx in range(len(core))]
123
+ minimal = min(rotations)
124
+ return minimal + (minimal[0],)
125
+
126
+
127
+ def _compute_max_depth(graph: dict[str, list[str]]) -> int:
128
+ def depth(node: str, path: set[str]) -> int:
129
+ best = 0
130
+ for nxt in graph.get(node, []):
131
+ if nxt not in graph or nxt in path:
132
+ continue
133
+ best = max(best, 1 + depth(nxt, path | {nxt}))
134
+ return best
135
+
136
+ best_depth = 0
137
+ for module in graph:
138
+ best_depth = max(best_depth, depth(module, {module}))
139
+ return best_depth