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,838 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Spec Markdown Parsing Module
4
+
5
+ Parses work item specification files from .session/specs/ directory.
6
+ Extracts structured data from markdown for use by validators, runners, and quality gates.
7
+
8
+ Part of Phase 5.7.2: Spec File First Architecture
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ import sys
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from solokit.core.error_handlers import log_errors
20
+ from solokit.core.exceptions import (
21
+ ErrorCode,
22
+ ValidationError,
23
+ )
24
+ from solokit.core.exceptions import (
25
+ FileNotFoundError as SolokitFileNotFoundError,
26
+ )
27
+ from solokit.core.logging_config import get_logger
28
+ from solokit.core.output import get_output
29
+ from solokit.core.types import WorkItemType
30
+
31
+ logger = get_logger(__name__)
32
+ output = get_output()
33
+
34
+
35
+ def strip_html_comments(content: str) -> str:
36
+ """
37
+ Remove all HTML comments from content.
38
+
39
+ Args:
40
+ content: Markdown content with possible HTML comments
41
+
42
+ Returns:
43
+ Content with all <!-- ... --> comments removed
44
+ """
45
+ # Remove HTML comments (including multiline)
46
+ return re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
47
+
48
+
49
+ def parse_section(content: str, section_name: str) -> str | None:
50
+ """
51
+ Extract content between '## SectionName' and next '##' heading.
52
+
53
+ Args:
54
+ content: Full markdown content (should have HTML comments stripped first)
55
+ section_name: Name of section to extract (case-insensitive)
56
+
57
+ Returns:
58
+ Section content (excluding heading) or None if not found
59
+ """
60
+ lines = content.split("\n")
61
+ in_section = False
62
+ section_content = []
63
+
64
+ for line in lines:
65
+ # Check if this is an H2 heading
66
+ if line.startswith("## "):
67
+ heading = line[3:].strip()
68
+
69
+ # Found our target section
70
+ if heading.lower() == section_name.lower():
71
+ in_section = True
72
+ continue
73
+ # Found next section, stop collecting
74
+ elif in_section:
75
+ break
76
+
77
+ # Collect lines while in target section
78
+ if in_section:
79
+ section_content.append(line)
80
+
81
+ # Return None if section not found
82
+ if not section_content:
83
+ return None
84
+
85
+ # Return trimmed content
86
+ return "\n".join(section_content).strip()
87
+
88
+
89
+ def extract_subsection(section_content: str, subsection_name: str) -> str | None:
90
+ """
91
+ Extract content under '### SubsectionName' within a section.
92
+
93
+ Args:
94
+ section_content: Content of a section (from parse_section)
95
+ subsection_name: Name of subsection to extract (case-insensitive)
96
+
97
+ Returns:
98
+ Subsection content (excluding heading) or None if not found
99
+ """
100
+ if not section_content:
101
+ return None
102
+
103
+ lines = section_content.split("\n")
104
+ in_subsection = False
105
+ subsection_content = []
106
+
107
+ for line in lines:
108
+ # Check if this is an H3 heading
109
+ if line.startswith("### "):
110
+ heading = line[4:].strip()
111
+
112
+ # Found our target subsection
113
+ if heading.lower() == subsection_name.lower():
114
+ in_subsection = True
115
+ continue
116
+ # Found next subsection, stop collecting
117
+ elif in_subsection:
118
+ break
119
+ # H2 heading means we've left the parent section
120
+ elif line.startswith("## ") and in_subsection:
121
+ break
122
+
123
+ # Collect lines while in target subsection
124
+ if in_subsection:
125
+ subsection_content.append(line)
126
+
127
+ # Return None if subsection not found
128
+ if not subsection_content:
129
+ return None
130
+
131
+ # Return trimmed content
132
+ return "\n".join(subsection_content).strip()
133
+
134
+
135
+ def extract_checklist(content: str) -> list[dict[str, Any]]:
136
+ """
137
+ Extract checklist items from markdown.
138
+
139
+ Args:
140
+ content: Markdown content containing checklist items
141
+
142
+ Returns:
143
+ List of dicts with 'text' and 'checked' keys:
144
+ [
145
+ {"text": "Item text", "checked": False},
146
+ {"text": "Checked item", "checked": True},
147
+ ...
148
+ ]
149
+ """
150
+ if not content:
151
+ return []
152
+
153
+ checklist = []
154
+ for line in content.split("\n"):
155
+ # Match checklist pattern: - [ ] or - [x]
156
+ match = re.match(r"-\s+\[([ xX])\]\s+(.+)", line.strip())
157
+ if match:
158
+ checked = match.group(1).lower() == "x"
159
+ text = match.group(2).strip()
160
+ checklist.append({"text": text, "checked": checked})
161
+
162
+ return checklist
163
+
164
+
165
+ def extract_code_blocks(content: str) -> list[dict[str, str]]:
166
+ """
167
+ Extract all code blocks from content.
168
+
169
+ Args:
170
+ content: Markdown content with code blocks
171
+
172
+ Returns:
173
+ List of dicts with 'language' and 'code' keys:
174
+ [
175
+ {"language": "typescript", "code": "..."},
176
+ {"language": "bash", "code": "..."},
177
+ ...
178
+ ]
179
+ """
180
+ if not content:
181
+ return []
182
+
183
+ code_blocks = []
184
+
185
+ # Pattern to match ```language\n...\n```
186
+ pattern = r"```(\w+)?\n(.*?)```"
187
+ matches = re.finditer(pattern, content, flags=re.DOTALL)
188
+
189
+ for match in matches:
190
+ language = match.group(1) or "text" # Default to 'text' if no language specified
191
+ code = match.group(2).strip()
192
+ code_blocks.append({"language": language, "code": code})
193
+
194
+ return code_blocks
195
+
196
+
197
+ def extract_list_items(content: str) -> list[str]:
198
+ """
199
+ Extract bullet point or numbered list items from content.
200
+
201
+ Args:
202
+ content: Markdown content with list items
203
+
204
+ Returns:
205
+ List of item text (without bullets/numbers)
206
+ """
207
+ if not content:
208
+ return []
209
+
210
+ items = []
211
+ for line in content.split("\n"):
212
+ # Match bullet points (-, *, +) or numbered lists (1., 2., etc.)
213
+ match = re.match(r"^[\s]*(?:[-*+]|\d+\.)\s+(.+)", line)
214
+ if match:
215
+ items.append(match.group(1).strip())
216
+
217
+ return items
218
+
219
+
220
+ # ============================================================================
221
+ # Work Item Type-Specific Parsers
222
+ # ============================================================================
223
+
224
+
225
+ def parse_feature_spec(content: str) -> dict[str, Any]:
226
+ """
227
+ Parse feature specification.
228
+
229
+ Sections:
230
+ - Overview
231
+ - User Story
232
+ - Rationale
233
+ - Acceptance Criteria
234
+ - Implementation Details (with subsections: Approach, Components Affected, API Changes, Database Changes)
235
+ - Testing Strategy
236
+ - Documentation Updates
237
+ - Dependencies
238
+ - Estimated Effort
239
+ """
240
+ # Strip HTML comments first
241
+ content = strip_html_comments(content)
242
+
243
+ result: dict[str, Any] = {}
244
+
245
+ # Extract main sections
246
+ result["overview"] = parse_section(content, "Overview")
247
+ result["user_story"] = parse_section(content, "User Story")
248
+ result["rationale"] = parse_section(content, "Rationale")
249
+
250
+ # Acceptance Criteria - extract as checklist
251
+ ac_section = parse_section(content, "Acceptance Criteria")
252
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
253
+
254
+ # Implementation Details with subsections
255
+ impl_section = parse_section(content, "Implementation Details")
256
+ if impl_section:
257
+ result["implementation_details"] = {
258
+ "approach": extract_subsection(impl_section, "Approach"),
259
+ "llm_processing_config": extract_subsection(
260
+ impl_section, "LLM/Processing Configuration"
261
+ ),
262
+ "components_affected": extract_subsection(impl_section, "Components Affected"),
263
+ "api_changes": extract_subsection(impl_section, "API Changes"),
264
+ "database_changes": extract_subsection(impl_section, "Database Changes"),
265
+ "code_blocks": extract_code_blocks(impl_section),
266
+ }
267
+ else:
268
+ result["implementation_details"] = None
269
+
270
+ # Testing Strategy
271
+ result["testing_strategy"] = parse_section(content, "Testing Strategy")
272
+
273
+ # Documentation Updates - extract as checklist
274
+ doc_section = parse_section(content, "Documentation Updates")
275
+ result["documentation_updates"] = extract_checklist(doc_section) if doc_section else []
276
+
277
+ # Dependencies
278
+ result["dependencies"] = parse_section(content, "Dependencies")
279
+
280
+ # Estimated Effort
281
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
282
+
283
+ return result
284
+
285
+
286
+ def parse_bug_spec(content: str) -> dict[str, Any]:
287
+ """
288
+ Parse bug specification.
289
+
290
+ Sections:
291
+ - Description
292
+ - Steps to Reproduce
293
+ - Expected Behavior
294
+ - Actual Behavior
295
+ - Impact
296
+ - Root Cause Analysis (with subsections: Investigation, Root Cause, Why It Happened)
297
+ - Fix Approach
298
+ - Prevention
299
+ - Testing Strategy
300
+ - Dependencies
301
+ - Estimated Effort
302
+ """
303
+ # Strip HTML comments first
304
+ content = strip_html_comments(content)
305
+
306
+ result: dict[str, Any] = {}
307
+
308
+ # Extract main sections
309
+ result["description"] = parse_section(content, "Description")
310
+ result["steps_to_reproduce"] = parse_section(content, "Steps to Reproduce")
311
+ result["expected_behavior"] = parse_section(content, "Expected Behavior")
312
+ result["actual_behavior"] = parse_section(content, "Actual Behavior")
313
+ result["impact"] = parse_section(content, "Impact")
314
+
315
+ # Root Cause Analysis with subsections
316
+ rca_section = parse_section(content, "Root Cause Analysis")
317
+ if rca_section:
318
+ result["root_cause_analysis"] = {
319
+ "investigation": extract_subsection(rca_section, "Investigation"),
320
+ "root_cause": extract_subsection(rca_section, "Root Cause"),
321
+ "why_it_happened": extract_subsection(rca_section, "Why It Happened"),
322
+ "code_blocks": extract_code_blocks(rca_section),
323
+ }
324
+ else:
325
+ result["root_cause_analysis"] = None
326
+
327
+ # Fix Approach
328
+ result["fix_approach"] = parse_section(content, "Fix Approach")
329
+
330
+ # Prevention
331
+ result["prevention"] = parse_section(content, "Prevention")
332
+
333
+ # Testing Strategy
334
+ result["testing_strategy"] = parse_section(content, "Testing Strategy")
335
+
336
+ # Acceptance Criteria - extract as checklist
337
+ ac_section = parse_section(content, "Acceptance Criteria")
338
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
339
+
340
+ # Dependencies
341
+ result["dependencies"] = parse_section(content, "Dependencies")
342
+
343
+ # Estimated Effort
344
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
345
+
346
+ return result
347
+
348
+
349
+ def parse_refactor_spec(content: str) -> dict[str, Any]:
350
+ """
351
+ Parse refactor specification.
352
+
353
+ Sections:
354
+ - Overview
355
+ - Current State
356
+ - Problems with Current Approach
357
+ - Proposed Refactor (with subsections: New Approach, Benefits, Trade-offs)
358
+ - Implementation Plan
359
+ - Scope (with subsections: In Scope, Out of Scope)
360
+ - Risk Assessment
361
+ - Acceptance Criteria
362
+ - Testing Strategy
363
+ - Dependencies
364
+ - Estimated Effort
365
+ """
366
+ # Strip HTML comments first
367
+ content = strip_html_comments(content)
368
+
369
+ result: dict[str, Any] = {}
370
+
371
+ # Extract main sections
372
+ result["overview"] = parse_section(content, "Overview")
373
+ result["current_state"] = parse_section(content, "Current State")
374
+ result["problems"] = parse_section(content, "Problems with Current Approach")
375
+
376
+ # Proposed Refactor with subsections
377
+ refactor_section = parse_section(content, "Proposed Refactor")
378
+ if refactor_section:
379
+ result["proposed_refactor"] = {
380
+ "new_approach": extract_subsection(refactor_section, "New Approach"),
381
+ "benefits": extract_subsection(refactor_section, "Benefits"),
382
+ "trade_offs": extract_subsection(refactor_section, "Trade-offs"),
383
+ "code_blocks": extract_code_blocks(refactor_section),
384
+ }
385
+ else:
386
+ result["proposed_refactor"] = None
387
+
388
+ # Implementation Plan
389
+ result["implementation_plan"] = parse_section(content, "Implementation Plan")
390
+
391
+ # Scope with subsections
392
+ scope_section = parse_section(content, "Scope")
393
+ if scope_section:
394
+ result["scope"] = {
395
+ "in_scope": extract_subsection(scope_section, "In Scope"),
396
+ "out_of_scope": extract_subsection(scope_section, "Out of Scope"),
397
+ }
398
+ else:
399
+ result["scope"] = None
400
+
401
+ # Risk Assessment
402
+ result["risk_assessment"] = parse_section(content, "Risk Assessment")
403
+
404
+ # Acceptance Criteria - extract as checklist
405
+ ac_section = parse_section(content, "Acceptance Criteria")
406
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
407
+
408
+ # Testing Strategy
409
+ result["testing_strategy"] = parse_section(content, "Testing Strategy")
410
+
411
+ # Dependencies
412
+ result["dependencies"] = parse_section(content, "Dependencies")
413
+
414
+ # Estimated Effort
415
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
416
+
417
+ return result
418
+
419
+
420
+ def parse_security_spec(content: str) -> dict[str, Any]:
421
+ """
422
+ Parse security specification.
423
+
424
+ Sections:
425
+ - Security Issue
426
+ - Severity
427
+ - Affected Components
428
+ - Threat Model (with subsections: Assets at Risk, Threat Actors, Attack Scenarios)
429
+ - Attack Vector
430
+ - Mitigation Strategy
431
+ - Security Testing (with subsections: Automated Security Testing, Manual Security Testing, Test Cases)
432
+ - Compliance
433
+ - Acceptance Criteria
434
+ - Post-Deployment
435
+ - Dependencies
436
+ - Estimated Effort
437
+ """
438
+ # Strip HTML comments first
439
+ content = strip_html_comments(content)
440
+
441
+ result: dict[str, Any] = {}
442
+
443
+ # Extract main sections
444
+ result["security_issue"] = parse_section(content, "Security Issue")
445
+ result["severity"] = parse_section(content, "Severity")
446
+ result["affected_components"] = parse_section(content, "Affected Components")
447
+
448
+ # Threat Model with subsections
449
+ threat_section = parse_section(content, "Threat Model")
450
+ if threat_section:
451
+ result["threat_model"] = {
452
+ "assets_at_risk": extract_subsection(threat_section, "Assets at Risk"),
453
+ "threat_actors": extract_subsection(threat_section, "Threat Actors"),
454
+ "attack_scenarios": extract_subsection(threat_section, "Attack Scenarios"),
455
+ "code_blocks": extract_code_blocks(threat_section),
456
+ }
457
+ else:
458
+ result["threat_model"] = None
459
+
460
+ # Attack Vector
461
+ result["attack_vector"] = parse_section(content, "Attack Vector")
462
+
463
+ # Mitigation Strategy
464
+ result["mitigation_strategy"] = parse_section(content, "Mitigation Strategy")
465
+
466
+ # Security Testing with subsections
467
+ testing_section = parse_section(content, "Security Testing")
468
+ if testing_section:
469
+ result["security_testing"] = {
470
+ "automated": extract_subsection(testing_section, "Automated Security Testing"),
471
+ "manual": extract_subsection(testing_section, "Manual Security Testing"),
472
+ "test_cases": extract_subsection(testing_section, "Test Cases"),
473
+ "checklist": extract_checklist(testing_section),
474
+ }
475
+ else:
476
+ result["security_testing"] = None
477
+
478
+ # Compliance - extract as checklist
479
+ compliance_section = parse_section(content, "Compliance")
480
+ result["compliance"] = extract_checklist(compliance_section) if compliance_section else []
481
+
482
+ # Acceptance Criteria - extract as checklist
483
+ ac_section = parse_section(content, "Acceptance Criteria")
484
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
485
+
486
+ # Post-Deployment
487
+ post_section = parse_section(content, "Post-Deployment")
488
+ result["post_deployment"] = extract_checklist(post_section) if post_section else []
489
+
490
+ # Dependencies
491
+ result["dependencies"] = parse_section(content, "Dependencies")
492
+
493
+ # Estimated Effort
494
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
495
+
496
+ return result
497
+
498
+
499
+ def parse_integration_test_spec(content: str) -> dict[str, Any]:
500
+ """
501
+ Parse integration test specification.
502
+
503
+ Sections:
504
+ - Scope
505
+ - Test Scenarios (multiple subsections: Scenario 1, Scenario 2, etc.)
506
+ - Performance Benchmarks
507
+ - API Contracts
508
+ - Environment Requirements
509
+ - Acceptance Criteria
510
+ - Dependencies
511
+ - Estimated Effort
512
+ """
513
+ # Strip HTML comments first
514
+ content = strip_html_comments(content)
515
+
516
+ result: dict[str, Any] = {}
517
+
518
+ # Extract main sections
519
+ result["scope"] = parse_section(content, "Scope")
520
+
521
+ # Test Scenarios - extract all scenarios
522
+ scenarios_section = parse_section(content, "Test Scenarios")
523
+ if scenarios_section:
524
+ # Find all subsections that start with "Scenario"
525
+ scenarios = []
526
+ lines = scenarios_section.split("\n")
527
+ current_scenario: str | None = None
528
+ current_content: list[str] = []
529
+
530
+ for line in lines:
531
+ if line.startswith("### Scenario"):
532
+ # Save previous scenario if exists
533
+ if current_scenario is not None:
534
+ scenarios.append(
535
+ {
536
+ "name": current_scenario,
537
+ "content": "\n".join(current_content).strip(),
538
+ }
539
+ )
540
+ # Start new scenario
541
+ current_scenario = line[4:].strip() # Remove '### '
542
+ current_content = []
543
+ elif current_scenario:
544
+ current_content.append(line)
545
+
546
+ # Save last scenario
547
+ if current_scenario is not None:
548
+ scenarios.append(
549
+ {
550
+ "name": current_scenario,
551
+ "content": "\n".join(current_content).strip(),
552
+ }
553
+ )
554
+
555
+ result["test_scenarios"] = scenarios
556
+ else:
557
+ result["test_scenarios"] = []
558
+
559
+ # Performance Benchmarks
560
+ result["performance_benchmarks"] = parse_section(content, "Performance Benchmarks")
561
+
562
+ # API Contracts
563
+ result["api_contracts"] = parse_section(content, "API Contracts")
564
+
565
+ # Environment Requirements
566
+ result["environment_requirements"] = parse_section(content, "Environment Requirements")
567
+
568
+ # Acceptance Criteria - extract as checklist
569
+ ac_section = parse_section(content, "Acceptance Criteria")
570
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
571
+
572
+ # Dependencies
573
+ result["dependencies"] = parse_section(content, "Dependencies")
574
+
575
+ # Estimated Effort
576
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
577
+
578
+ return result
579
+
580
+
581
+ def parse_deployment_spec(content: str) -> dict[str, Any]:
582
+ """
583
+ Parse deployment specification.
584
+
585
+ Sections:
586
+ - Deployment Scope
587
+ - Deployment Procedure (with subsections: Pre-Deployment Checklist, Deployment Steps, Post-Deployment Steps)
588
+ - Environment Configuration
589
+ - Rollback Procedure (with subsections: Rollback Triggers, Rollback Steps)
590
+ - Smoke Tests (multiple subsections: Test 1, Test 2, etc.)
591
+ - Monitoring & Alerting
592
+ - Post-Deployment Monitoring Period
593
+ - Acceptance Criteria
594
+ - Dependencies
595
+ - Estimated Effort
596
+ """
597
+ # Strip HTML comments first
598
+ content = strip_html_comments(content)
599
+
600
+ result: dict[str, Any] = {}
601
+
602
+ # Extract main sections
603
+ result["deployment_scope"] = parse_section(content, "Deployment Scope")
604
+
605
+ # Deployment Procedure with subsections
606
+ procedure_section = parse_section(content, "Deployment Procedure")
607
+ if procedure_section:
608
+ result["deployment_procedure"] = {
609
+ "pre_deployment": extract_subsection(procedure_section, "Pre-Deployment Checklist"),
610
+ "deployment_steps": extract_subsection(procedure_section, "Deployment Steps"),
611
+ "post_deployment": extract_subsection(procedure_section, "Post-Deployment Steps"),
612
+ "code_blocks": extract_code_blocks(procedure_section),
613
+ "checklist": extract_checklist(procedure_section),
614
+ }
615
+ else:
616
+ result["deployment_procedure"] = None
617
+
618
+ # Environment Configuration
619
+ result["environment_configuration"] = parse_section(content, "Environment Configuration")
620
+
621
+ # Rollback Procedure with subsections
622
+ rollback_section = parse_section(content, "Rollback Procedure")
623
+ if rollback_section:
624
+ result["rollback_procedure"] = {
625
+ "triggers": extract_subsection(rollback_section, "Rollback Triggers"),
626
+ "steps": extract_subsection(rollback_section, "Rollback Steps"),
627
+ "code_blocks": extract_code_blocks(rollback_section),
628
+ }
629
+ else:
630
+ result["rollback_procedure"] = None
631
+
632
+ # Smoke Tests - extract all tests
633
+ smoke_section = parse_section(content, "Smoke Tests")
634
+ if smoke_section:
635
+ # Find all subsections that start with "Test"
636
+ tests = []
637
+ lines = smoke_section.split("\n")
638
+ current_test: str | None = None
639
+ current_content: list[str] = []
640
+
641
+ for line in lines:
642
+ if line.startswith("### Test"):
643
+ # Save previous test if exists
644
+ if current_test is not None:
645
+ tests.append(
646
+ {
647
+ "name": current_test,
648
+ "content": "\n".join(current_content).strip(),
649
+ }
650
+ )
651
+ # Start new test
652
+ current_test = line[4:].strip() # Remove '### '
653
+ current_content = []
654
+ elif current_test:
655
+ current_content.append(line)
656
+
657
+ # Save last test
658
+ if current_test is not None:
659
+ tests.append({"name": current_test, "content": "\n".join(current_content).strip()})
660
+
661
+ result["smoke_tests"] = tests
662
+ else:
663
+ result["smoke_tests"] = []
664
+
665
+ # Monitoring & Alerting
666
+ result["monitoring"] = parse_section(content, "Monitoring & Alerting")
667
+
668
+ # Post-Deployment Monitoring Period
669
+ result["monitoring_period"] = parse_section(content, "Post-Deployment Monitoring Period")
670
+
671
+ # Acceptance Criteria - extract as checklist
672
+ ac_section = parse_section(content, "Acceptance Criteria")
673
+ result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
674
+
675
+ # Dependencies
676
+ result["dependencies"] = parse_section(content, "Dependencies")
677
+
678
+ # Estimated Effort
679
+ result["estimated_effort"] = parse_section(content, "Estimated Effort")
680
+
681
+ return result
682
+
683
+
684
+ # ============================================================================
685
+ # Main Entry Point
686
+ # ============================================================================
687
+
688
+
689
+ @log_errors()
690
+ def parse_spec_file(work_item: str | dict[str, Any]) -> dict[str, Any]:
691
+ """
692
+ Parse a work item specification file.
693
+
694
+ Args:
695
+ work_item: Either a work item dict with 'spec_file' and 'id' fields,
696
+ or a string work_item_id (for backwards compatibility)
697
+
698
+ Returns:
699
+ Parsed specification as structured dict
700
+
701
+ Raises:
702
+ FileNotFoundError: If spec file doesn't exist
703
+ ValidationError: If work item type cannot be determined or spec format is invalid
704
+ SpecValidationError: If spec file structure is invalid
705
+ """
706
+ # Handle backwards compatibility: accept both dict and string
707
+ if isinstance(work_item, str):
708
+ # Legacy call with just work_item_id string
709
+ work_item_id: str | Any | None = work_item
710
+ spec_path = Path(f".session/specs/{work_item_id}.md")
711
+ logger.debug("Parsing spec file for work item (legacy): %s", work_item_id)
712
+ else:
713
+ # New call with work item dict
714
+ work_item_id = work_item.get("id")
715
+ # Use spec_file from work item if available, otherwise fallback to ID-based pattern
716
+ spec_file_path = work_item.get("spec_file")
717
+ if spec_file_path:
718
+ spec_path = Path(spec_file_path)
719
+ logger.debug("Parsing spec file from work_item.spec_file: %s", spec_path)
720
+ else:
721
+ # Fallback to legacy pattern for backwards compatibility
722
+ spec_path = Path(f".session/specs/{work_item_id}.md")
723
+ logger.debug("Parsing spec file (fallback to ID pattern): %s", spec_path)
724
+
725
+ if not spec_path.exists():
726
+ logger.error("Spec file not found: %s", spec_path)
727
+ raise SolokitFileNotFoundError(file_path=str(spec_path), file_type="spec")
728
+
729
+ try:
730
+ with open(spec_path, encoding="utf-8") as f:
731
+ content = f.read()
732
+ except OSError as e:
733
+ logger.error("Failed to read spec file: %s", spec_path)
734
+ raise ValidationError(
735
+ message=f"Failed to read spec file: {spec_path}",
736
+ code=ErrorCode.FILE_OPERATION_FAILED,
737
+ context={"file_path": str(spec_path)},
738
+ remediation="Check file permissions and try again",
739
+ cause=e,
740
+ )
741
+
742
+ # Determine work item type from first line (H1 heading)
743
+ first_line = content.split("\n")[0].strip()
744
+ if not first_line.startswith("# "):
745
+ logger.error("Invalid spec file: Missing H1 heading in %s", spec_path)
746
+ raise ValidationError(
747
+ message=f"Invalid spec file: Missing H1 heading in {spec_path}",
748
+ code=ErrorCode.SPEC_VALIDATION_FAILED,
749
+ context={"file_path": str(spec_path), "first_line": first_line},
750
+ remediation="Spec file must start with '# Type: Name' heading",
751
+ )
752
+
753
+ # Extract type from "# Type: Name" pattern
754
+ heading_match = re.match(r"#\s*(\w+):\s*(.+)", first_line)
755
+ if not heading_match:
756
+ logger.error("Invalid spec file: H1 heading doesn't match pattern in %s", spec_path)
757
+ raise ValidationError(
758
+ message=f"Invalid spec file: H1 heading doesn't match 'Type: Name' pattern in {spec_path}",
759
+ code=ErrorCode.SPEC_VALIDATION_FAILED,
760
+ context={"file_path": str(spec_path), "heading": first_line},
761
+ remediation="Use format: '# Type: Name' (e.g., '# Feature: My Feature')",
762
+ )
763
+
764
+ work_type = heading_match.group(1).lower()
765
+ work_name = heading_match.group(2).strip()
766
+ logger.debug("Detected work type: %s, name: %s", work_type, work_name)
767
+
768
+ # Parse based on work item type
769
+ parsers = {
770
+ WorkItemType.FEATURE.value: parse_feature_spec,
771
+ WorkItemType.BUG.value: parse_bug_spec,
772
+ WorkItemType.REFACTOR.value: parse_refactor_spec,
773
+ WorkItemType.SECURITY.value: parse_security_spec,
774
+ WorkItemType.INTEGRATION_TEST.value: parse_integration_test_spec,
775
+ WorkItemType.DEPLOYMENT.value: parse_deployment_spec,
776
+ }
777
+
778
+ parser = parsers.get(work_type)
779
+ if not parser:
780
+ valid_types = ", ".join(parsers.keys())
781
+ raise ValidationError(
782
+ message=f"Unknown work item type: {work_type}",
783
+ code=ErrorCode.INVALID_WORK_ITEM_TYPE,
784
+ context={
785
+ "work_type": work_type,
786
+ "valid_types": list(parsers.keys()),
787
+ "file_path": str(spec_path),
788
+ },
789
+ remediation=f"Use one of: {valid_types}",
790
+ )
791
+
792
+ # Parse the spec
793
+ try:
794
+ parsed = parser(content)
795
+ parsed["_meta"] = {
796
+ "work_item_id": work_item_id,
797
+ "work_type": work_type,
798
+ "name": work_name,
799
+ "spec_path": str(spec_path),
800
+ }
801
+ return parsed
802
+ except ValidationError:
803
+ # Re-raise ValidationError as-is
804
+ raise
805
+ except Exception as e:
806
+ # Wrap unexpected errors in ValidationError
807
+ logger.error("Error parsing spec file %s: %s", spec_path, str(e))
808
+ raise ValidationError(
809
+ message=f"Error parsing spec file {spec_path}",
810
+ code=ErrorCode.SPEC_VALIDATION_FAILED,
811
+ context={"file_path": str(spec_path), "work_type": work_type, "error": str(e)},
812
+ remediation="Check spec file format and content",
813
+ cause=e,
814
+ )
815
+
816
+
817
+ # ============================================================================
818
+ # CLI Interface for Testing
819
+ # ============================================================================
820
+
821
+ if __name__ == "__main__":
822
+ import sys
823
+
824
+ if len(sys.argv) < 2:
825
+ output.info("Usage: spec_parser.py <work_item_id>")
826
+ output.info("Example: spec_parser.py feature_001")
827
+ sys.exit(1)
828
+
829
+ work_item_id = sys.argv[1]
830
+
831
+ try:
832
+ logger.info("Parsing spec file for work item: %s", work_item_id)
833
+ result = parse_spec_file(work_item_id)
834
+ output.info(json.dumps(result, indent=2))
835
+ except Exception as e:
836
+ logger.error("Failed to parse spec file", exc_info=True)
837
+ output.error(f"Error: {e}")
838
+ sys.exit(1)