solokit 0.1.1__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 (323) hide show
  1. solokit/__init__.py +10 -0
  2. solokit/__version__.py +3 -0
  3. solokit/cli.py +374 -0
  4. solokit/core/__init__.py +1 -0
  5. solokit/core/cache.py +102 -0
  6. solokit/core/command_runner.py +278 -0
  7. solokit/core/config.py +453 -0
  8. solokit/core/config_validator.py +204 -0
  9. solokit/core/constants.py +291 -0
  10. solokit/core/error_formatter.py +279 -0
  11. solokit/core/error_handlers.py +346 -0
  12. solokit/core/exceptions.py +1567 -0
  13. solokit/core/file_ops.py +309 -0
  14. solokit/core/logging_config.py +166 -0
  15. solokit/core/output.py +99 -0
  16. solokit/core/performance.py +57 -0
  17. solokit/core/protocols.py +141 -0
  18. solokit/core/types.py +312 -0
  19. solokit/deployment/__init__.py +1 -0
  20. solokit/deployment/executor.py +411 -0
  21. solokit/git/__init__.py +1 -0
  22. solokit/git/integration.py +619 -0
  23. solokit/init/__init__.py +41 -0
  24. solokit/init/claude_commands_installer.py +87 -0
  25. solokit/init/dependency_installer.py +313 -0
  26. solokit/init/docs_structure.py +90 -0
  27. solokit/init/env_generator.py +160 -0
  28. solokit/init/environment_validator.py +334 -0
  29. solokit/init/git_hooks_installer.py +71 -0
  30. solokit/init/git_setup.py +188 -0
  31. solokit/init/gitignore_updater.py +195 -0
  32. solokit/init/initial_commit.py +145 -0
  33. solokit/init/initial_scans.py +109 -0
  34. solokit/init/orchestrator.py +246 -0
  35. solokit/init/readme_generator.py +207 -0
  36. solokit/init/session_structure.py +239 -0
  37. solokit/init/template_installer.py +424 -0
  38. solokit/learning/__init__.py +1 -0
  39. solokit/learning/archiver.py +115 -0
  40. solokit/learning/categorizer.py +126 -0
  41. solokit/learning/curator.py +428 -0
  42. solokit/learning/extractor.py +352 -0
  43. solokit/learning/reporter.py +351 -0
  44. solokit/learning/repository.py +254 -0
  45. solokit/learning/similarity.py +342 -0
  46. solokit/learning/validator.py +144 -0
  47. solokit/project/__init__.py +1 -0
  48. solokit/project/init.py +1162 -0
  49. solokit/project/stack.py +436 -0
  50. solokit/project/sync_plugin.py +438 -0
  51. solokit/project/tree.py +375 -0
  52. solokit/quality/__init__.py +1 -0
  53. solokit/quality/api_validator.py +424 -0
  54. solokit/quality/checkers/__init__.py +25 -0
  55. solokit/quality/checkers/base.py +114 -0
  56. solokit/quality/checkers/context7.py +221 -0
  57. solokit/quality/checkers/custom.py +162 -0
  58. solokit/quality/checkers/deployment.py +323 -0
  59. solokit/quality/checkers/documentation.py +179 -0
  60. solokit/quality/checkers/formatting.py +161 -0
  61. solokit/quality/checkers/integration.py +394 -0
  62. solokit/quality/checkers/linting.py +159 -0
  63. solokit/quality/checkers/security.py +261 -0
  64. solokit/quality/checkers/spec_completeness.py +127 -0
  65. solokit/quality/checkers/tests.py +184 -0
  66. solokit/quality/env_validator.py +306 -0
  67. solokit/quality/gates.py +655 -0
  68. solokit/quality/reporters/__init__.py +10 -0
  69. solokit/quality/reporters/base.py +25 -0
  70. solokit/quality/reporters/console.py +98 -0
  71. solokit/quality/reporters/json_reporter.py +34 -0
  72. solokit/quality/results.py +98 -0
  73. solokit/session/__init__.py +1 -0
  74. solokit/session/briefing/__init__.py +245 -0
  75. solokit/session/briefing/documentation_loader.py +53 -0
  76. solokit/session/briefing/formatter.py +476 -0
  77. solokit/session/briefing/git_context.py +282 -0
  78. solokit/session/briefing/learning_loader.py +212 -0
  79. solokit/session/briefing/milestone_builder.py +78 -0
  80. solokit/session/briefing/orchestrator.py +137 -0
  81. solokit/session/briefing/stack_detector.py +51 -0
  82. solokit/session/briefing/tree_generator.py +52 -0
  83. solokit/session/briefing/work_item_loader.py +209 -0
  84. solokit/session/briefing.py +353 -0
  85. solokit/session/complete.py +1188 -0
  86. solokit/session/status.py +246 -0
  87. solokit/session/validate.py +452 -0
  88. solokit/templates/.claude/commands/end.md +109 -0
  89. solokit/templates/.claude/commands/init.md +159 -0
  90. solokit/templates/.claude/commands/learn-curate.md +88 -0
  91. solokit/templates/.claude/commands/learn-search.md +62 -0
  92. solokit/templates/.claude/commands/learn-show.md +69 -0
  93. solokit/templates/.claude/commands/learn.md +136 -0
  94. solokit/templates/.claude/commands/start.md +114 -0
  95. solokit/templates/.claude/commands/status.md +22 -0
  96. solokit/templates/.claude/commands/validate.md +27 -0
  97. solokit/templates/.claude/commands/work-delete.md +119 -0
  98. solokit/templates/.claude/commands/work-graph.md +139 -0
  99. solokit/templates/.claude/commands/work-list.md +26 -0
  100. solokit/templates/.claude/commands/work-new.md +114 -0
  101. solokit/templates/.claude/commands/work-next.md +25 -0
  102. solokit/templates/.claude/commands/work-show.md +24 -0
  103. solokit/templates/.claude/commands/work-update.md +141 -0
  104. solokit/templates/CHANGELOG.md +17 -0
  105. solokit/templates/WORK_ITEM_TYPES.md +141 -0
  106. solokit/templates/__init__.py +1 -0
  107. solokit/templates/bug_spec.md +217 -0
  108. solokit/templates/config.schema.json +150 -0
  109. solokit/templates/dashboard_refine/base/.gitignore +36 -0
  110. solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
  111. solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
  112. solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
  113. solokit/templates/dashboard_refine/base/app/globals.css +60 -0
  114. solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
  115. solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
  116. solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
  117. solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
  118. solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
  119. solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
  120. solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
  121. solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
  122. solokit/templates/dashboard_refine/base/components.json +16 -0
  123. solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
  124. solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
  125. solokit/templates/dashboard_refine/base/next.config.ts +10 -0
  126. solokit/templates/dashboard_refine/base/package.json.template +40 -0
  127. solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
  128. solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
  129. solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
  130. solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
  131. solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
  132. solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
  133. solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
  134. solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
  135. solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
  136. solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
  137. solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
  138. solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
  139. solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
  140. solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
  141. solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
  142. solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
  143. solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
  144. solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
  145. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
  146. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
  147. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
  148. solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
  149. solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
  150. solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
  151. solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
  152. solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
  153. solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
  154. solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
  155. solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
  156. solokit/templates/deployment_spec.md +500 -0
  157. solokit/templates/feature_spec.md +248 -0
  158. solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
  159. solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
  160. solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
  161. solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
  162. solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
  163. solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
  164. solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
  165. solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
  166. solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
  167. solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
  168. solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
  169. solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
  170. solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
  171. solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
  172. solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
  173. solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
  174. solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
  175. solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
  176. solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
  177. solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
  178. solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
  179. solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
  180. solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
  181. solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
  182. solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
  183. solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
  184. solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
  185. solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
  186. solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
  187. solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
  188. solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
  189. solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
  190. solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
  191. solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
  192. solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
  193. solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
  194. solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
  195. solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
  196. solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
  197. solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
  198. solokit/templates/git-hooks/prepare-commit-msg +24 -0
  199. solokit/templates/integration_test_spec.md +363 -0
  200. solokit/templates/learnings.json +15 -0
  201. solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
  202. solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
  203. solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
  204. solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
  205. solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
  206. solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
  207. solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
  208. solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
  209. solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
  210. solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
  211. solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
  212. solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
  213. solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
  214. solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
  215. solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
  216. solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
  217. solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
  218. solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
  219. solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
  220. solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
  221. solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
  222. solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
  223. solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
  224. solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
  225. solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
  226. solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
  227. solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
  228. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
  229. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
  230. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
  231. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
  232. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
  233. solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
  234. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
  235. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
  236. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
  237. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
  238. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
  239. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
  240. solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
  241. solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
  242. solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
  243. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
  244. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
  245. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
  246. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
  247. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
  248. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
  249. solokit/templates/refactor_spec.md +287 -0
  250. solokit/templates/saas_t3/base/.gitignore +36 -0
  251. solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
  252. solokit/templates/saas_t3/base/app/globals.css +27 -0
  253. solokit/templates/saas_t3/base/app/layout.tsx +23 -0
  254. solokit/templates/saas_t3/base/app/page.tsx +31 -0
  255. solokit/templates/saas_t3/base/lib/api.tsx +77 -0
  256. solokit/templates/saas_t3/base/lib/utils.ts +13 -0
  257. solokit/templates/saas_t3/base/next.config.ts +7 -0
  258. solokit/templates/saas_t3/base/package.json.template +38 -0
  259. solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
  260. solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
  261. solokit/templates/saas_t3/base/server/api/root.ts +19 -0
  262. solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
  263. solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
  264. solokit/templates/saas_t3/base/server/db.ts +17 -0
  265. solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
  266. solokit/templates/saas_t3/base/tsconfig.json +27 -0
  267. solokit/templates/saas_t3/docker/Dockerfile +60 -0
  268. solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
  269. solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
  270. solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
  271. solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
  272. solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
  273. solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
  274. solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
  275. solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
  276. solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
  277. solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
  278. solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
  279. solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
  280. solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
  281. solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
  282. solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
  283. solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
  284. solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
  285. solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
  286. solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
  287. solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
  288. solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
  289. solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
  290. solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
  291. solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
  292. solokit/templates/security_spec.md +287 -0
  293. solokit/templates/stack-versions.yaml +617 -0
  294. solokit/templates/status_update.json +6 -0
  295. solokit/templates/template-registry.json +257 -0
  296. solokit/templates/work_items.json +11 -0
  297. solokit/testing/__init__.py +1 -0
  298. solokit/testing/integration_runner.py +550 -0
  299. solokit/testing/performance.py +637 -0
  300. solokit/visualization/__init__.py +1 -0
  301. solokit/visualization/dependency_graph.py +788 -0
  302. solokit/work_items/__init__.py +1 -0
  303. solokit/work_items/creator.py +217 -0
  304. solokit/work_items/delete.py +264 -0
  305. solokit/work_items/get_dependencies.py +185 -0
  306. solokit/work_items/get_dependents.py +113 -0
  307. solokit/work_items/get_metadata.py +121 -0
  308. solokit/work_items/get_next_recommendations.py +133 -0
  309. solokit/work_items/manager.py +235 -0
  310. solokit/work_items/milestones.py +137 -0
  311. solokit/work_items/query.py +376 -0
  312. solokit/work_items/repository.py +267 -0
  313. solokit/work_items/scheduler.py +184 -0
  314. solokit/work_items/spec_parser.py +838 -0
  315. solokit/work_items/spec_validator.py +493 -0
  316. solokit/work_items/updater.py +157 -0
  317. solokit/work_items/validator.py +205 -0
  318. solokit-0.1.1.dist-info/METADATA +640 -0
  319. solokit-0.1.1.dist-info/RECORD +323 -0
  320. solokit-0.1.1.dist-info/WHEEL +5 -0
  321. solokit-0.1.1.dist-info/entry_points.txt +2 -0
  322. solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
  323. solokit-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,788 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Dependency graph visualization for work items
4
+
5
+ Generates visual dependency graphs with critical path analysis and work item timeline projection.
6
+ Supports DOT format, SVG, and ASCII art output.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ from pathlib import Path
14
+ from typing import Any
15
+
16
+ from solokit.core.command_runner import CommandRunner
17
+ from solokit.core.constants import DEPENDENCY_GRAPH_TIMEOUT
18
+ from solokit.core.error_handlers import convert_file_errors, log_errors
19
+ from solokit.core.exceptions import (
20
+ CircularDependencyError,
21
+ CommandExecutionError,
22
+ FileOperationError,
23
+ ValidationError,
24
+ )
25
+ from solokit.core.logging_config import get_logger
26
+ from solokit.core.output import get_output
27
+ from solokit.core.types import WorkItemStatus
28
+
29
+ logger = get_logger(__name__)
30
+ output = get_output()
31
+
32
+
33
+ class DependencyGraphVisualizer:
34
+ """Visualizes work item dependency graphs"""
35
+
36
+ def __init__(self, work_items_file: Path | None = None):
37
+ """
38
+ Initialize visualizer.
39
+
40
+ Args:
41
+ work_items_file: Path to work_items.json (default: .session/tracking/work_items.json)
42
+ """
43
+ if work_items_file is None:
44
+ work_items_file = Path(".session/tracking/work_items.json")
45
+ self.work_items_file = work_items_file
46
+ self.runner = CommandRunner(default_timeout=DEPENDENCY_GRAPH_TIMEOUT)
47
+
48
+ @convert_file_errors
49
+ @log_errors()
50
+ def load_work_items(
51
+ self,
52
+ status_filter: str | None = None,
53
+ milestone_filter: str | None = None,
54
+ type_filter: str | None = None,
55
+ include_completed: bool = False,
56
+ ) -> list[dict]:
57
+ """Load and filter work items from JSON file.
58
+
59
+ Args:
60
+ status_filter: Filter by status (not_started, in_progress, completed, blocked)
61
+ milestone_filter: Filter by milestone name
62
+ type_filter: Filter by work item type
63
+ include_completed: Include completed items (default: False)
64
+
65
+ Returns:
66
+ List of filtered work items
67
+
68
+ Raises:
69
+ FileNotFoundError: If work_items_file doesn't exist
70
+ FileOperationError: If JSON parsing fails
71
+ ValidationError: If work items data structure is invalid
72
+ """
73
+ if not self.work_items_file.exists():
74
+ return []
75
+
76
+ try:
77
+ with open(self.work_items_file) as f:
78
+ data = json.load(f)
79
+ except json.JSONDecodeError as e:
80
+ raise FileOperationError(
81
+ operation="parse",
82
+ file_path=str(self.work_items_file),
83
+ details=f"Invalid JSON: {e}",
84
+ cause=e,
85
+ ) from e
86
+
87
+ if not isinstance(data, dict):
88
+ raise ValidationError(
89
+ message="Work items file must contain a JSON object",
90
+ context={"file_path": str(self.work_items_file)},
91
+ remediation="Ensure the JSON file contains a top-level object with 'work_items' key",
92
+ )
93
+
94
+ work_items = list(data.get("work_items", {}).values())
95
+
96
+ # Apply filters
97
+ if not include_completed:
98
+ work_items = [
99
+ wi for wi in work_items if wi.get("status") != WorkItemStatus.COMPLETED.value
100
+ ]
101
+
102
+ if status_filter:
103
+ work_items = [wi for wi in work_items if wi.get("status") == status_filter]
104
+
105
+ if milestone_filter:
106
+ work_items = [wi for wi in work_items if wi.get("milestone") == milestone_filter]
107
+
108
+ if type_filter:
109
+ work_items = [wi for wi in work_items if wi.get("type") == type_filter]
110
+
111
+ return work_items
112
+
113
+ @log_errors()
114
+ def generate_dot(self, work_items: list[dict]) -> str:
115
+ """Generate DOT format graph
116
+
117
+ Args:
118
+ work_items: List of work items to include in graph
119
+
120
+ Returns:
121
+ DOT format string
122
+
123
+ Raises:
124
+ ValidationError: If work items have invalid structure
125
+ CircularDependencyError: If circular dependencies detected
126
+ """
127
+ # Validate work items structure
128
+ for item in work_items:
129
+ if not isinstance(item, dict):
130
+ raise ValidationError(
131
+ message="Work item must be a dictionary",
132
+ context={"item": str(item)},
133
+ )
134
+ if "id" not in item:
135
+ raise ValidationError(
136
+ message="Work item missing required 'id' field",
137
+ context={"item": str(item)},
138
+ )
139
+
140
+ # Calculate critical path
141
+ critical_items = self._calculate_critical_path(work_items)
142
+
143
+ # Start DOT graph
144
+ lines = [
145
+ "digraph WorkItems {",
146
+ " rankdir=TB;",
147
+ " node [shape=box, style=rounded];",
148
+ "",
149
+ ]
150
+
151
+ # Add nodes
152
+ for item in work_items:
153
+ # Determine node styling based on status and critical path
154
+ color = self._get_node_color(item, critical_items)
155
+ style = self._get_node_style(item)
156
+
157
+ label = self._format_node_label(item)
158
+
159
+ lines.append(f' "{item["id"]}" [label="{label}", color="{color}", style="{style}"];')
160
+
161
+ lines.append("")
162
+
163
+ # Add edges
164
+ for item in work_items:
165
+ for dep_id in item.get("dependencies", []):
166
+ # Check if dependency exists in filtered items
167
+ if any(wi["id"] == dep_id for wi in work_items):
168
+ edge_style = (
169
+ "bold, color=red"
170
+ if item["id"] in critical_items and dep_id in critical_items
171
+ else ""
172
+ )
173
+ if edge_style:
174
+ lines.append(f' "{dep_id}" -> "{item["id"]}" [{edge_style}];')
175
+ else:
176
+ lines.append(f' "{dep_id}" -> "{item["id"]}";')
177
+
178
+ lines.append("}")
179
+ return "\n".join(lines)
180
+
181
+ @log_errors()
182
+ def generate_ascii(self, work_items: list[dict]) -> str:
183
+ """Generate ASCII art graph
184
+
185
+ Args:
186
+ work_items: List of work items to include in graph
187
+
188
+ Returns:
189
+ ASCII art string
190
+
191
+ Raises:
192
+ ValidationError: If work items have invalid structure
193
+ CircularDependencyError: If circular dependencies detected
194
+ """
195
+ # Validate work items structure
196
+ for item in work_items:
197
+ if not isinstance(item, dict):
198
+ raise ValidationError(
199
+ message="Work item must be a dictionary",
200
+ context={"item": str(item)},
201
+ )
202
+ if "id" not in item:
203
+ raise ValidationError(
204
+ message="Work item missing required 'id' field",
205
+ context={"item": str(item)},
206
+ )
207
+
208
+ # Calculate critical path
209
+ critical_items = self._calculate_critical_path(work_items)
210
+
211
+ # Build dependency tree
212
+ lines = ["Work Item Dependency Graph", "=" * 50, ""]
213
+
214
+ # Group items by dependency level
215
+ levels = self._group_by_dependency_level(work_items)
216
+
217
+ for level_num, level_items in enumerate(levels):
218
+ lines.append(f"Level {level_num}:")
219
+ for item in level_items:
220
+ status_icon = self._get_status_icon(item)
221
+ critical_marker = " [CRITICAL PATH]" if item["id"] in critical_items else ""
222
+ lines.append(f" {status_icon} {item['id']}: {item['title']}{critical_marker}")
223
+
224
+ # Show dependencies
225
+ if item.get("dependencies"):
226
+ for dep_id in item["dependencies"]:
227
+ lines.append(f" └─ depends on: {dep_id}")
228
+
229
+ lines.append("")
230
+
231
+ # Add timeline projection
232
+ timeline = self._generate_timeline_projection(work_items)
233
+ if timeline:
234
+ lines.append("Timeline Projection:")
235
+ lines.append("-" * 50)
236
+ lines.extend(timeline)
237
+
238
+ return "\n".join(lines)
239
+
240
+ @log_errors()
241
+ def generate_svg(self, dot_content: str, output_file: Path) -> None:
242
+ """Generate SVG from DOT using Graphviz.
243
+
244
+ Args:
245
+ dot_content: DOT format string
246
+ output_file: Path to save SVG file
247
+
248
+ Raises:
249
+ CommandExecutionError: If Graphviz dot command fails
250
+ FileOperationError: If temporary file operations fail
251
+ ValidationError: If dot_content is empty or invalid
252
+ """
253
+ if not dot_content or not dot_content.strip():
254
+ raise ValidationError(
255
+ message="DOT content cannot be empty",
256
+ remediation="Provide valid DOT format graph data",
257
+ )
258
+
259
+ # Use stdin input via temporary file approach since CommandRunner doesn't support stdin input directly
260
+ import tempfile
261
+
262
+ temp_file = None
263
+ try:
264
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".dot", delete=False) as f:
265
+ f.write(dot_content)
266
+ temp_file = f.name
267
+ except OSError as e:
268
+ raise FileOperationError(
269
+ operation="write",
270
+ file_path=temp_file or "temporary file",
271
+ details=f"Failed to create temporary DOT file: {e}",
272
+ cause=e,
273
+ ) from e
274
+
275
+ try:
276
+ result = self.runner.run(["dot", "-Tsvg", temp_file, "-o", str(output_file)])
277
+
278
+ if not result.success:
279
+ raise CommandExecutionError(
280
+ command=f"dot -Tsvg {temp_file} -o {output_file}",
281
+ returncode=result.returncode,
282
+ stderr=result.stderr,
283
+ stdout=result.stdout,
284
+ )
285
+ finally:
286
+ # Clean up temp file
287
+ if temp_file:
288
+ try:
289
+ Path(temp_file).unlink()
290
+ except OSError:
291
+ pass # Ignore cleanup errors
292
+
293
+ @log_errors()
294
+ def get_bottlenecks(self, work_items: list[dict]) -> list[dict]:
295
+ """Identify bottleneck work items (items that block many others).
296
+
297
+ Args:
298
+ work_items: List of work items to analyze
299
+
300
+ Returns:
301
+ List of bottleneck info dicts with id, blocks count, and item details
302
+
303
+ Raises:
304
+ ValidationError: If work items have invalid structure
305
+ """
306
+ # Count how many items each work item blocks
307
+ blocking_count = {}
308
+ for wi in work_items:
309
+ blocking_count[wi["id"]] = 0
310
+
311
+ for wi in work_items:
312
+ for dep_id in wi.get("dependencies", []):
313
+ if dep_id in blocking_count:
314
+ blocking_count[dep_id] += 1
315
+
316
+ # Return items that block 2+ other items
317
+ bottlenecks = [
318
+ {
319
+ "id": wid,
320
+ "blocks": count,
321
+ "item": next(wi for wi in work_items if wi["id"] == wid),
322
+ }
323
+ for wid, count in blocking_count.items()
324
+ if count >= 2
325
+ ]
326
+
327
+ return sorted(bottlenecks, key=lambda x: x["blocks"], reverse=True)
328
+
329
+ @log_errors()
330
+ def get_neighborhood(self, work_items: list[dict], focus_id: str) -> list[dict]:
331
+ """Get work items in neighborhood of focus item (dependencies and dependents).
332
+
333
+ Args:
334
+ work_items: List of work items
335
+ focus_id: ID of focus work item
336
+
337
+ Returns:
338
+ List of work items in neighborhood (empty list if focus item not found)
339
+
340
+ Raises:
341
+ ValidationError: If focus_id is empty or work items have invalid structure
342
+ """
343
+ if not focus_id or not focus_id.strip():
344
+ raise ValidationError(
345
+ message="Focus item ID cannot be empty",
346
+ remediation="Provide a valid work item ID",
347
+ )
348
+
349
+ # Find focus item
350
+ focus_item = next((wi for wi in work_items if wi["id"] == focus_id), None)
351
+ if not focus_item:
352
+ return []
353
+
354
+ # Get all dependencies (recursive)
355
+ neighborhood_ids = {focus_id}
356
+ to_check = set(focus_item.get("dependencies", []))
357
+
358
+ while to_check:
359
+ dep_id = to_check.pop()
360
+ if dep_id not in neighborhood_ids:
361
+ neighborhood_ids.add(dep_id)
362
+ dep_item = next((wi for wi in work_items if wi["id"] == dep_id), None)
363
+ if dep_item:
364
+ to_check.update(dep_item.get("dependencies", []))
365
+
366
+ # Get all dependents (items that depend on any item in neighborhood)
367
+ for wi in work_items:
368
+ if any(dep_id in neighborhood_ids for dep_id in wi.get("dependencies", [])):
369
+ neighborhood_ids.add(wi["id"])
370
+
371
+ return [wi for wi in work_items if wi["id"] in neighborhood_ids]
372
+
373
+ @log_errors()
374
+ def generate_stats(self, work_items: list[dict], critical_path: set[str]) -> dict:
375
+ """Generate graph statistics.
376
+
377
+ Args:
378
+ work_items: List of work items
379
+ critical_path: Set of work item IDs on critical path
380
+
381
+ Returns:
382
+ Dictionary with statistics
383
+
384
+ Raises:
385
+ ValidationError: If work items have invalid structure
386
+ """
387
+ total = len(work_items)
388
+ completed = len(
389
+ [wi for wi in work_items if wi.get("status") == WorkItemStatus.COMPLETED.value]
390
+ )
391
+ in_progress = len(
392
+ [wi for wi in work_items if wi.get("status") == WorkItemStatus.IN_PROGRESS.value]
393
+ )
394
+ not_started = len(
395
+ [wi for wi in work_items if wi.get("status") == WorkItemStatus.NOT_STARTED.value]
396
+ )
397
+
398
+ return {
399
+ "total_items": total,
400
+ "completed": completed,
401
+ "in_progress": in_progress,
402
+ "not_started": not_started,
403
+ "completion_pct": round(completed / total * 100, 1) if total > 0 else 0,
404
+ "critical_path_length": len(critical_path),
405
+ "critical_items": list(critical_path),
406
+ }
407
+
408
+ @log_errors()
409
+ def _calculate_critical_path(self, work_items: list[dict]) -> set[str]:
410
+ """Calculate critical path through work items
411
+
412
+ The critical path is the longest chain of dependencies.
413
+
414
+ Args:
415
+ work_items: List of work items to analyze
416
+
417
+ Returns:
418
+ Set of work item IDs on the critical path
419
+
420
+ Raises:
421
+ CircularDependencyError: If circular dependencies detected (strict mode)
422
+ ValidationError: If work items have invalid structure
423
+ """
424
+ # Build dependency graph
425
+ item_dict = {item["id"]: item for item in work_items}
426
+
427
+ # Calculate depth for each item
428
+ depths: dict[str, int] = {}
429
+
430
+ def calculate_depth(item_id: str, visited: set[str], path: list[str]) -> int:
431
+ if item_id in depths:
432
+ return depths[item_id]
433
+
434
+ if item_id in visited:
435
+ # Circular dependency detected
436
+ # For now, return 0 to handle gracefully (existing behavior)
437
+ # In strict mode, we could:
438
+ # cycle = path[path.index(item_id):] + [item_id]
439
+ # raise CircularDependencyError(cycle)
440
+ return 0
441
+
442
+ if item_id not in item_dict:
443
+ return 0
444
+
445
+ item = item_dict[item_id]
446
+ if not item.get("dependencies"):
447
+ depths[item_id] = 0
448
+ return 0
449
+
450
+ visited.add(item_id)
451
+ path.append(item_id)
452
+ max_depth = 0
453
+
454
+ for dep_id in item.get("dependencies", []):
455
+ dep_depth = calculate_depth(dep_id, visited.copy(), path.copy())
456
+ max_depth = max(max_depth, dep_depth + 1)
457
+
458
+ depths[item_id] = max_depth
459
+ return max_depth
460
+
461
+ # Calculate depths for all items
462
+ for item in work_items:
463
+ calculate_depth(item["id"], set(), [])
464
+
465
+ # Find maximum depth
466
+ if not depths:
467
+ return set()
468
+
469
+ max_depth = max(depths.values())
470
+
471
+ # Trace critical path
472
+ critical_items = set()
473
+
474
+ def trace_critical_path(item_id: str, current_depth: int) -> None:
475
+ if current_depth == 0:
476
+ critical_items.add(item_id)
477
+ return
478
+
479
+ if item_id not in item_dict:
480
+ return
481
+
482
+ item = item_dict[item_id]
483
+ critical_items.add(item_id)
484
+
485
+ for dep_id in item.get("dependencies", []):
486
+ if dep_id in depths and depths[dep_id] == current_depth - 1:
487
+ trace_critical_path(dep_id, current_depth - 1)
488
+
489
+ # Find items at max depth
490
+ for item_id, depth in depths.items():
491
+ if depth == max_depth:
492
+ trace_critical_path(item_id, max_depth)
493
+
494
+ return critical_items
495
+
496
+ def _get_node_color(self, item: dict, critical_items: set[str]) -> str:
497
+ """Get node color based on status and critical path"""
498
+ if item["id"] in critical_items:
499
+ return "red"
500
+ elif item.get("status") == WorkItemStatus.COMPLETED.value:
501
+ return "green"
502
+ elif item.get("status") == WorkItemStatus.IN_PROGRESS.value:
503
+ return "blue"
504
+ elif item.get("status") == WorkItemStatus.BLOCKED.value:
505
+ return "orange"
506
+ else:
507
+ return "black"
508
+
509
+ def _get_node_style(self, item: dict) -> str:
510
+ """Get node style based on status"""
511
+ if item.get("status") == WorkItemStatus.COMPLETED.value:
512
+ return "rounded,filled"
513
+ elif item.get("status") == WorkItemStatus.IN_PROGRESS.value:
514
+ return "rounded,bold"
515
+ else:
516
+ return "rounded"
517
+
518
+ def _format_node_label(self, item: dict) -> str:
519
+ """Format node label with work item details"""
520
+ # Escape special characters for DOT
521
+ title = item["title"].replace('"', '\\"')
522
+
523
+ # Truncate long titles
524
+ if len(title) > 30:
525
+ title = title[:27] + "..."
526
+
527
+ status = item.get("status", WorkItemStatus.NOT_STARTED.value)
528
+ return f"{item['id']}\\n{title}\\n[{status}]"
529
+
530
+ def _get_status_icon(self, item: dict[str, Any]) -> str:
531
+ """Get ASCII icon for work item status"""
532
+ icons: dict[str, str] = {
533
+ WorkItemStatus.NOT_STARTED.value: "○",
534
+ WorkItemStatus.IN_PROGRESS.value: "◐",
535
+ WorkItemStatus.COMPLETED.value: "●",
536
+ WorkItemStatus.BLOCKED.value: "✗",
537
+ }
538
+ status = item.get("status")
539
+ return icons.get(str(status) if status is not None else "", "○")
540
+
541
+ def _group_by_dependency_level(self, work_items: list[dict]) -> list[list[dict]]:
542
+ """Group work items by dependency level
543
+
544
+ Level 0 = no dependencies
545
+ Level 1 = depends only on level 0
546
+ etc.
547
+ """
548
+ item_dict = {item["id"]: item for item in work_items}
549
+ levels: list[list[dict]] = []
550
+ assigned = set()
551
+
552
+ def get_item_level(item: dict) -> int:
553
+ if not item.get("dependencies"):
554
+ return 0
555
+
556
+ max_dep_level = -1
557
+ for dep_id in item.get("dependencies", []):
558
+ if dep_id in item_dict:
559
+ dep_item = item_dict[dep_id]
560
+ dep_level = get_item_level(dep_item)
561
+ max_dep_level = max(max_dep_level, dep_level)
562
+
563
+ return max_dep_level + 1
564
+
565
+ # Assign items to levels
566
+ for item in work_items:
567
+ level = get_item_level(item)
568
+
569
+ # Ensure we have enough levels
570
+ while len(levels) <= level:
571
+ levels.append([])
572
+
573
+ levels[level].append(item)
574
+ assigned.add(item["id"])
575
+
576
+ return levels
577
+
578
+ def _generate_timeline_projection(self, work_items: list[dict]) -> list[str]:
579
+ """Generate timeline projection based on work items
580
+
581
+ Note: This is a simplified projection assuming each item takes 1 time unit.
582
+ In practice, you'd use time_estimate from metadata.
583
+ """
584
+ lines = []
585
+
586
+ # Group by dependency level
587
+ levels = self._group_by_dependency_level(work_items)
588
+
589
+ total_time = 0
590
+ for level_num, level_items in enumerate(levels):
591
+ # Assume each level can be done in parallel
592
+ # Time for this level is the max time of any item in the level
593
+ # For now, assume each item takes 1 unit
594
+ level_time = 1 if level_items else 0
595
+
596
+ if level_items:
597
+ completed_count = sum(
598
+ 1
599
+ for item in level_items
600
+ if item.get("status") == WorkItemStatus.COMPLETED.value
601
+ )
602
+ in_progress_count = sum(
603
+ 1
604
+ for item in level_items
605
+ if item.get("status") == WorkItemStatus.IN_PROGRESS.value
606
+ )
607
+ not_started_count = sum(
608
+ 1
609
+ for item in level_items
610
+ if item.get("status") == WorkItemStatus.NOT_STARTED.value
611
+ )
612
+
613
+ lines.append(
614
+ f" Level {level_num}: {len(level_items)} items "
615
+ f"(✓{completed_count} ◐{in_progress_count} ○{not_started_count})"
616
+ )
617
+
618
+ total_time += level_time
619
+
620
+ lines.append("")
621
+ lines.append(f"Estimated remaining levels: {len(levels)}")
622
+ lines.append("Note: Timeline assumes items can be completed in parallel within each level")
623
+
624
+ return lines
625
+
626
+
627
+ def main() -> int:
628
+ """CLI entry point for graph generation."""
629
+ parser = argparse.ArgumentParser(description="Generate work item dependency graphs")
630
+
631
+ # Output format
632
+ parser.add_argument(
633
+ "--format",
634
+ choices=["ascii", "dot", "svg"],
635
+ default="ascii",
636
+ help="Output format (default: ascii)",
637
+ )
638
+ parser.add_argument("--output", help="Output file (for dot/svg formats)")
639
+
640
+ # Filters
641
+ parser.add_argument(
642
+ "--status",
643
+ choices=WorkItemStatus.values(),
644
+ help="Filter by status",
645
+ )
646
+ parser.add_argument("--milestone", help="Filter by milestone")
647
+ parser.add_argument("--type", help="Filter by work item type")
648
+ parser.add_argument(
649
+ "--include-completed",
650
+ action="store_true",
651
+ help="Include completed items (default: hide)",
652
+ )
653
+
654
+ # Special views
655
+ parser.add_argument("--critical-path", action="store_true", help="Show only critical path")
656
+ parser.add_argument("--bottlenecks", action="store_true", help="Show bottleneck analysis")
657
+ parser.add_argument("--stats", action="store_true", help="Show graph statistics")
658
+ parser.add_argument("--focus", help="Focus on neighborhood of specific work item")
659
+
660
+ # Work items file
661
+ parser.add_argument(
662
+ "--work-items-file",
663
+ type=Path,
664
+ help="Path to work_items.json (default: .session/tracking/work_items.json)",
665
+ )
666
+
667
+ args = parser.parse_args()
668
+
669
+ # Initialize visualizer
670
+ viz = DependencyGraphVisualizer(args.work_items_file)
671
+
672
+ # Load work items with filters
673
+ work_items = viz.load_work_items(
674
+ status_filter=args.status,
675
+ milestone_filter=args.milestone,
676
+ type_filter=args.type,
677
+ include_completed=args.include_completed,
678
+ )
679
+
680
+ if not work_items:
681
+ output.error("No work items found matching criteria.")
682
+ return 1
683
+
684
+ # Apply special filters
685
+ if args.focus:
686
+ try:
687
+ work_items = viz.get_neighborhood(work_items, args.focus)
688
+ if not work_items:
689
+ output.error(f"Work item '{args.focus}' not found.")
690
+ return 1
691
+ except ValidationError as e:
692
+ output.error(f"Error: {e.message}")
693
+ if e.remediation:
694
+ output.error(f"Remediation: {e.remediation}")
695
+ return e.exit_code
696
+
697
+ critical_path = viz._calculate_critical_path(work_items)
698
+
699
+ if args.critical_path:
700
+ work_items = [wi for wi in work_items if wi["id"] in critical_path]
701
+
702
+ # Handle special views
703
+ if args.stats:
704
+ stats = viz.generate_stats(work_items, critical_path)
705
+ output.info("Graph Statistics:")
706
+ output.info("=" * 50)
707
+ output.info(f"Total work items: {stats['total_items']}")
708
+ output.info(f"Completed: {stats['completed']} ({stats['completion_pct']}%)")
709
+ output.info(f"In progress: {stats['in_progress']}")
710
+ output.info(f"Not started: {stats['not_started']}")
711
+ output.info(f"Critical path length: {stats['critical_path_length']}")
712
+ if stats["critical_items"]:
713
+ output.info(f"Critical items: {', '.join(stats['critical_items'])}")
714
+ return 0
715
+
716
+ if args.bottlenecks:
717
+ bottlenecks = viz.get_bottlenecks(work_items)
718
+ output.info("Bottleneck Analysis:")
719
+ output.info("=" * 50)
720
+ if bottlenecks:
721
+ for bn in bottlenecks:
722
+ item = bn["item"]
723
+ output.info(
724
+ f"{bn['id']} - {item.get('title', 'N/A')} (blocks {bn['blocks']} items)"
725
+ )
726
+ else:
727
+ output.info("No bottlenecks found (no items block 2+ other items).")
728
+ return 0
729
+
730
+ # Generate graph
731
+ try:
732
+ if args.format == "ascii":
733
+ graph_output = viz.generate_ascii(work_items)
734
+ output.info(graph_output)
735
+
736
+ elif args.format == "dot":
737
+ graph_output = viz.generate_dot(work_items)
738
+ if args.output:
739
+ try:
740
+ Path(args.output).write_text(graph_output)
741
+ output.info(f"DOT graph saved to {args.output}")
742
+ except OSError as e:
743
+ from solokit.core.exceptions import FileOperationError
744
+
745
+ raise FileOperationError(
746
+ operation="write",
747
+ file_path=args.output,
748
+ details=str(e),
749
+ cause=e,
750
+ ) from e
751
+ else:
752
+ output.info(graph_output)
753
+
754
+ elif args.format == "svg":
755
+ dot_output = viz.generate_dot(work_items)
756
+ output_file = Path(args.output) if args.output else Path("dependency_graph.svg")
757
+ viz.generate_svg(dot_output, output_file)
758
+ output.info(f"SVG graph saved to {output_file}")
759
+
760
+ except ValidationError as e:
761
+ output.error(f"Validation Error: {e.message}")
762
+ if e.remediation:
763
+ output.error(f"Remediation: {e.remediation}")
764
+ return e.exit_code
765
+ except CommandExecutionError as e:
766
+ output.error(f"Command Error: {e.message}")
767
+ if e.context.get("stderr"):
768
+ output.error(f"Details: {e.context['stderr']}")
769
+ output.error(
770
+ "Hint: Ensure Graphviz is installed (apt-get install graphviz / brew install graphviz)"
771
+ )
772
+ return e.exit_code
773
+ except FileOperationError as e:
774
+ output.error(f"File Error: {e.message}")
775
+ if e.remediation:
776
+ output.error(f"Remediation: {e.remediation}")
777
+ return e.exit_code
778
+ except CircularDependencyError as e:
779
+ output.error(f"Dependency Error: {e.message}")
780
+ if e.remediation:
781
+ output.error(f"Remediation: {e.remediation}")
782
+ return e.exit_code
783
+
784
+ return 0
785
+
786
+
787
+ if __name__ == "__main__":
788
+ exit(main())