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,637 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Performance benchmarking system for integration tests.
4
+
5
+ Tracks:
6
+ - Response times (p50, p95, p99)
7
+ - Throughput (requests/second)
8
+ - Resource utilization (CPU, memory)
9
+ - Regression detection
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import logging
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ from solokit.core.command_runner import CommandRunner
20
+ from solokit.core.constants import (
21
+ GIT_QUICK_TIMEOUT,
22
+ HTTP_REQUEST_TIMEOUT,
23
+ PERFORMANCE_REGRESSION_THRESHOLD,
24
+ PERFORMANCE_TEST_TIMEOUT,
25
+ )
26
+ from solokit.core.error_handlers import convert_subprocess_errors, log_errors
27
+ from solokit.core.exceptions import (
28
+ BenchmarkFailedError,
29
+ LoadTestFailedError,
30
+ PerformanceRegressionError,
31
+ PerformanceTestError,
32
+ ValidationError,
33
+ WorkItemNotFoundError,
34
+ )
35
+ from solokit.core.file_ops import load_json, save_json
36
+ from solokit.core.output import get_output
37
+
38
+ logger = logging.getLogger(__name__)
39
+ output = get_output()
40
+
41
+
42
+ class PerformanceBenchmark:
43
+ """Performance benchmarking for integration tests."""
44
+
45
+ def __init__(self, work_item: dict):
46
+ """
47
+ Initialize performance benchmark.
48
+
49
+ Args:
50
+ work_item: Integration test work item with performance requirements
51
+
52
+ Raises:
53
+ ValidationError: If work_item is invalid or missing required fields
54
+ """
55
+ if not work_item:
56
+ raise ValidationError(
57
+ message="Work item cannot be None or empty",
58
+ context={"work_item": work_item},
59
+ remediation="Provide a valid work item dictionary",
60
+ )
61
+
62
+ self.work_item = work_item
63
+ self.benchmarks = work_item.get("performance_benchmarks", {})
64
+ self.baselines_file = Path(".session/tracking/performance_baselines.json")
65
+ self.results: dict[str, Any] = {}
66
+ self.runner = CommandRunner(default_timeout=300) # Long timeout for perf tests
67
+
68
+ @log_errors()
69
+ def run_benchmarks(self, test_endpoint: str | None = None) -> tuple[bool, dict[str, Any]]:
70
+ """
71
+ Run performance benchmarks.
72
+
73
+ Args:
74
+ test_endpoint: Endpoint to benchmark (if None, uses work item config)
75
+
76
+ Returns:
77
+ (passed: bool, results: dict)
78
+
79
+ Raises:
80
+ LoadTestFailedError: If load test fails
81
+ PerformanceTestError: If benchmark execution fails
82
+ BenchmarkFailedError: If benchmarks don't meet requirements
83
+ PerformanceRegressionError: If regression is detected
84
+ """
85
+ logger.info("Running performance benchmarks...")
86
+
87
+ if test_endpoint is None:
88
+ test_endpoint = self.benchmarks.get("endpoint", "http://localhost:8000/health")
89
+
90
+ # Run load test
91
+ load_test_results = self._run_load_test(test_endpoint)
92
+ self.results["load_test"] = load_test_results
93
+
94
+ # Measure resource utilization
95
+ resource_usage = self._measure_resource_usage()
96
+ self.results["resource_usage"] = resource_usage
97
+
98
+ # Compare against baselines
99
+ passed = self._check_against_requirements()
100
+ regression_detected = self._check_for_regression()
101
+
102
+ self.results["passed"] = passed
103
+ self.results["regression_detected"] = regression_detected
104
+
105
+ # Store as new baseline if passed
106
+ if passed and not regression_detected:
107
+ self._store_baseline()
108
+
109
+ return passed and not regression_detected, self.results
110
+
111
+ @log_errors()
112
+ @convert_subprocess_errors
113
+ def _run_load_test(self, endpoint: str) -> dict[str, Any]:
114
+ """
115
+ Run load test using wrk or similar tool.
116
+
117
+ Args:
118
+ endpoint: URL to test
119
+
120
+ Returns:
121
+ Load test results dict
122
+
123
+ Raises:
124
+ LoadTestFailedError: If load test fails
125
+ ValidationError: If endpoint is invalid
126
+ """
127
+ if not endpoint:
128
+ raise ValidationError(
129
+ message="Endpoint cannot be empty",
130
+ context={"endpoint": endpoint},
131
+ remediation="Provide a valid endpoint URL",
132
+ )
133
+
134
+ duration = self.benchmarks.get("load_test_duration", 60)
135
+ threads = self.benchmarks.get("threads", 4)
136
+ connections = self.benchmarks.get("connections", 100)
137
+
138
+ try:
139
+ # Using wrk for load testing
140
+ result = self.runner.run(
141
+ [
142
+ "wrk",
143
+ "-t",
144
+ str(threads),
145
+ "-c",
146
+ str(connections),
147
+ "-d",
148
+ f"{duration}s",
149
+ "--latency",
150
+ endpoint,
151
+ ],
152
+ timeout=duration + 30,
153
+ )
154
+
155
+ if result.success:
156
+ # Parse wrk output
157
+ return self._parse_wrk_output(result.stdout)
158
+ else:
159
+ # wrk not installed, try using Python requests as fallback
160
+ logger.info("wrk not available, using fallback load test")
161
+ return self._run_simple_load_test(endpoint, duration)
162
+
163
+ except Exception as e:
164
+ raise LoadTestFailedError(
165
+ endpoint=endpoint,
166
+ details=str(e),
167
+ context={"duration": duration, "threads": threads, "connections": connections},
168
+ ) from e
169
+
170
+ def _parse_wrk_output(self, output: str) -> dict[str, Any]:
171
+ """
172
+ Parse wrk output to extract metrics.
173
+
174
+ Args:
175
+ output: Raw wrk output
176
+
177
+ Returns:
178
+ Parsed metrics dictionary
179
+
180
+ Raises:
181
+ PerformanceTestError: If parsing fails
182
+ """
183
+ results: dict[str, Any] = {"latency": {}, "throughput": {}}
184
+
185
+ try:
186
+ lines = output.split("\n")
187
+
188
+ for line in lines:
189
+ # Match percentile lines more precisely - look for lines starting with whitespace + percentage
190
+ line_stripped = line.strip()
191
+ if line_stripped.startswith("50.000%") or line_stripped.startswith("50%"):
192
+ # p50 latency
193
+ parts = line.split()
194
+ results["latency"]["p50"] = self._parse_latency(parts[-1])
195
+ elif line_stripped.startswith("75.000%") or line_stripped.startswith("75%"):
196
+ results["latency"]["p75"] = self._parse_latency(parts[-1])
197
+ elif line_stripped.startswith("90.000%") or line_stripped.startswith("90%"):
198
+ results["latency"]["p90"] = self._parse_latency(parts[-1])
199
+ elif line_stripped.startswith("99.000%") or line_stripped.startswith("99%"):
200
+ results["latency"]["p99"] = self._parse_latency(parts[-1])
201
+ elif "Requests/sec:" in line:
202
+ parts = line.split()
203
+ results["throughput"]["requests_per_sec"] = float(parts[1])
204
+ elif "Transfer/sec:" in line:
205
+ parts = line.split()
206
+ results["throughput"]["transfer_per_sec"] = parts[1]
207
+
208
+ return results
209
+ except (IndexError, ValueError) as e:
210
+ raise PerformanceTestError(
211
+ message="Failed to parse wrk output",
212
+ context={"output": output[:500], "error": str(e)},
213
+ remediation="Verify wrk is producing expected output format",
214
+ ) from e
215
+
216
+ def _parse_latency(self, latency_str: str) -> float:
217
+ """
218
+ Convert latency string (e.g., '1.23ms') to milliseconds.
219
+
220
+ Args:
221
+ latency_str: Latency string from wrk
222
+
223
+ Returns:
224
+ Latency in milliseconds
225
+
226
+ Raises:
227
+ PerformanceTestError: If parsing fails
228
+ """
229
+ try:
230
+ latency_str = latency_str.strip()
231
+ if "ms" in latency_str: # milliseconds
232
+ return float(latency_str.rstrip("ms"))
233
+ elif "s" in latency_str: # seconds
234
+ return float(latency_str.rstrip("s")) * 1000
235
+ return 0.0
236
+ except (ValueError, AttributeError) as e:
237
+ raise PerformanceTestError(
238
+ message=f"Failed to parse latency value: {latency_str}",
239
+ context={"latency_str": latency_str},
240
+ remediation="Check wrk output format",
241
+ ) from e
242
+
243
+ def _run_simple_load_test(self, endpoint: str, duration: int) -> dict[str, Any]:
244
+ """
245
+ Fallback load test using Python requests.
246
+
247
+ Args:
248
+ endpoint: URL to test
249
+ duration: Test duration in seconds
250
+
251
+ Returns:
252
+ Load test results dictionary
253
+
254
+ Raises:
255
+ LoadTestFailedError: If load test fails
256
+ """
257
+ import time
258
+
259
+ import requests # type: ignore[import-untyped]
260
+
261
+ latencies = []
262
+ start_time = time.time()
263
+ request_count = 0
264
+
265
+ logger.info("Using simple load test (wrk not available)...")
266
+
267
+ try:
268
+ while time.time() - start_time < duration:
269
+ req_start = time.time()
270
+ try:
271
+ requests.get(endpoint, timeout=HTTP_REQUEST_TIMEOUT)
272
+ latency = (time.time() - req_start) * 1000 # Convert to ms
273
+ latencies.append(latency)
274
+ request_count += 1
275
+ except Exception:
276
+ # Individual request failures are logged but don't stop the test
277
+ logger.debug(f"Request to {endpoint} failed, continuing test")
278
+ pass
279
+
280
+ total_duration = time.time() - start_time
281
+
282
+ if not latencies:
283
+ raise LoadTestFailedError(
284
+ endpoint=endpoint,
285
+ details="No successful requests during load test",
286
+ context={"duration": duration, "request_count": request_count},
287
+ )
288
+
289
+ latencies.sort()
290
+
291
+ return {
292
+ "latency": {
293
+ "p50": latencies[int(len(latencies) * 0.50)],
294
+ "p75": latencies[int(len(latencies) * 0.75)],
295
+ "p90": latencies[int(len(latencies) * 0.90)],
296
+ "p95": latencies[int(len(latencies) * 0.95)],
297
+ "p99": latencies[int(len(latencies) * 0.99)],
298
+ },
299
+ "throughput": {"requests_per_sec": request_count / total_duration},
300
+ }
301
+ except LoadTestFailedError:
302
+ raise
303
+ except Exception as e:
304
+ raise LoadTestFailedError(
305
+ endpoint=endpoint,
306
+ details=f"Load test execution failed: {str(e)}",
307
+ context={"duration": duration},
308
+ ) from e
309
+
310
+ @log_errors()
311
+ @convert_subprocess_errors
312
+ def _measure_resource_usage(self) -> dict[str, Any]:
313
+ """
314
+ Measure CPU and memory usage of services.
315
+
316
+ Returns:
317
+ Resource usage dictionary
318
+
319
+ Raises:
320
+ PerformanceTestError: If resource measurement fails
321
+ """
322
+ services = self.work_item.get("environment_requirements", {}).get("services_required", [])
323
+
324
+ resource_usage = {}
325
+
326
+ for service in services:
327
+ try:
328
+ # Get container ID
329
+ result = self.runner.run(
330
+ ["docker-compose", "ps", "-q", service], timeout=GIT_QUICK_TIMEOUT
331
+ )
332
+
333
+ container_id = result.stdout.strip()
334
+ if not container_id:
335
+ logger.warning(f"No container found for service: {service}")
336
+ continue
337
+
338
+ # Get resource stats
339
+ stats_result = self.runner.run(
340
+ [
341
+ "docker",
342
+ "stats",
343
+ container_id,
344
+ "--no-stream",
345
+ "--format",
346
+ "{{.CPUPerc}},{{.MemUsage}}",
347
+ ],
348
+ timeout=PERFORMANCE_TEST_TIMEOUT,
349
+ )
350
+
351
+ if stats_result.success:
352
+ parts = stats_result.stdout.strip().split(",")
353
+ resource_usage[service] = {
354
+ "cpu_percent": parts[0].rstrip("%"),
355
+ "memory_usage": parts[1],
356
+ }
357
+ else:
358
+ logger.warning(
359
+ f"Failed to get stats for service {service}: {stats_result.stderr}"
360
+ )
361
+
362
+ except Exception as e:
363
+ logger.warning(f"Error measuring resource usage for {service}: {e}")
364
+ resource_usage[service] = {"error": str(e)}
365
+
366
+ return resource_usage
367
+
368
+ def _check_against_requirements(self) -> bool:
369
+ """
370
+ Check if benchmarks meet requirements.
371
+
372
+ Returns:
373
+ True if all requirements met, False otherwise
374
+
375
+ Raises:
376
+ BenchmarkFailedError: If benchmarks fail to meet requirements
377
+ """
378
+ requirements = self.benchmarks.get("response_time", {})
379
+ load_test = self.results.get("load_test", {})
380
+ latency = load_test.get("latency", {})
381
+
382
+ failed_benchmarks = []
383
+
384
+ # Check response time requirements
385
+ if "p50" in requirements:
386
+ actual = latency.get("p50", float("inf"))
387
+ expected = requirements["p50"]
388
+ if actual > expected:
389
+ logger.warning(f"p50 latency {actual}ms exceeds requirement {expected}ms")
390
+ failed_benchmarks.append(
391
+ BenchmarkFailedError(metric="p50_latency", actual=actual, expected=expected)
392
+ )
393
+
394
+ if "p95" in requirements:
395
+ actual = latency.get("p95", float("inf"))
396
+ expected = requirements["p95"]
397
+ if actual > expected:
398
+ logger.warning(f"p95 latency {actual}ms exceeds requirement {expected}ms")
399
+ failed_benchmarks.append(
400
+ BenchmarkFailedError(metric="p95_latency", actual=actual, expected=expected)
401
+ )
402
+
403
+ if "p99" in requirements:
404
+ actual = latency.get("p99", float("inf"))
405
+ expected = requirements["p99"]
406
+ if actual > expected:
407
+ logger.warning(f"p99 latency {actual}ms exceeds requirement {expected}ms")
408
+ failed_benchmarks.append(
409
+ BenchmarkFailedError(metric="p99_latency", actual=actual, expected=expected)
410
+ )
411
+
412
+ # Check throughput requirements
413
+ throughput_req = self.benchmarks.get("throughput", {})
414
+ throughput = load_test.get("throughput", {})
415
+
416
+ if "minimum" in throughput_req:
417
+ actual_rps = throughput.get("requests_per_sec", 0)
418
+ expected_rps = throughput_req["minimum"]
419
+ if actual_rps < expected_rps:
420
+ logger.warning(f"Throughput {actual_rps} req/s below minimum {expected_rps} req/s")
421
+ failed_benchmarks.append(
422
+ BenchmarkFailedError(
423
+ metric="throughput", actual=actual_rps, expected=expected_rps, unit="req/s"
424
+ )
425
+ )
426
+
427
+ # If any benchmarks failed, raise the first one
428
+ if failed_benchmarks:
429
+ raise failed_benchmarks[0]
430
+
431
+ return True
432
+
433
+ def _check_for_regression(self) -> bool:
434
+ """
435
+ Check for performance regression against baseline.
436
+
437
+ Returns:
438
+ True if regression detected, False otherwise
439
+
440
+ Raises:
441
+ PerformanceRegressionError: If regression is detected
442
+ """
443
+ if not self.baselines_file.exists():
444
+ logger.info("No baseline found, skipping regression check")
445
+ return False
446
+
447
+ try:
448
+ baselines = load_json(self.baselines_file)
449
+ except Exception as e:
450
+ logger.warning(f"Failed to load baselines: {e}")
451
+ return False
452
+
453
+ work_item_id = self.work_item.get("id")
454
+ if not work_item_id:
455
+ logger.info("Work item has no id, skipping regression check")
456
+ return False
457
+
458
+ baseline = baselines.get(work_item_id)
459
+
460
+ if not baseline:
461
+ logger.info(f"No baseline for work item {work_item_id}")
462
+ return False
463
+
464
+ load_test = self.results.get("load_test", {})
465
+ latency = load_test.get("latency", {})
466
+ baseline_latency = baseline.get("latency", {})
467
+
468
+ # Check for latency regression
469
+ for percentile in ["p50", "p95", "p99"]:
470
+ current = latency.get(percentile, 0)
471
+ baseline_val = baseline_latency.get(percentile, 0)
472
+
473
+ if baseline_val > 0 and current > baseline_val * PERFORMANCE_REGRESSION_THRESHOLD:
474
+ regression_percent = (current / baseline_val - 1) * 100
475
+ logger.warning(
476
+ f"Performance regression detected: {percentile} increased from "
477
+ f"{baseline_val}ms to {current}ms ({regression_percent:.1f}% slower)"
478
+ )
479
+ raise PerformanceRegressionError(
480
+ metric=percentile,
481
+ current=current,
482
+ baseline=baseline_val,
483
+ threshold_percent=(PERFORMANCE_REGRESSION_THRESHOLD - 1) * 100,
484
+ )
485
+
486
+ return False
487
+
488
+ @log_errors()
489
+ def _store_baseline(self) -> None:
490
+ """
491
+ Store current results as baseline.
492
+
493
+ Raises:
494
+ PerformanceTestError: If baseline storage fails
495
+ """
496
+ try:
497
+ if not self.baselines_file.exists():
498
+ baselines = {}
499
+ else:
500
+ baselines = load_json(self.baselines_file)
501
+
502
+ work_item_id = self.work_item.get("id")
503
+ if not work_item_id:
504
+ raise PerformanceTestError(
505
+ message="Work item has no id, cannot store baseline",
506
+ context={"work_item": self.work_item},
507
+ remediation="Ensure work item has an 'id' field",
508
+ )
509
+
510
+ baselines[work_item_id] = {
511
+ "latency": self.results.get("load_test", {}).get("latency", {}),
512
+ "throughput": self.results.get("load_test", {}).get("throughput", {}),
513
+ "resource_usage": self.results.get("resource_usage", {}),
514
+ "timestamp": datetime.now().isoformat(),
515
+ "session": self._get_current_session(),
516
+ }
517
+
518
+ save_json(self.baselines_file, baselines)
519
+ logger.info(f"Baseline stored for work item {work_item_id}")
520
+ except Exception as e:
521
+ raise PerformanceTestError(
522
+ message=f"Failed to store baseline for work item {work_item_id}",
523
+ context={"work_item_id": work_item_id, "error": str(e)},
524
+ remediation="Check file permissions and disk space",
525
+ ) from e
526
+
527
+ def _get_current_session(self) -> int:
528
+ """
529
+ Get current session number.
530
+
531
+ Returns:
532
+ Current session number or 0 if not found
533
+ """
534
+ status_file = Path(".session/tracking/status_update.json")
535
+ if status_file.exists():
536
+ try:
537
+ status = load_json(status_file)
538
+ session_num = status.get("session_number", 0)
539
+ return int(session_num) if session_num is not None else 0
540
+ except Exception as e:
541
+ logger.warning(f"Failed to load session status: {e}")
542
+ return 0
543
+ return 0
544
+
545
+ def generate_report(self) -> str:
546
+ """
547
+ Generate performance benchmark report.
548
+
549
+ Returns:
550
+ Formatted report string
551
+ """
552
+ load_test = self.results.get("load_test", {})
553
+ latency = load_test.get("latency", {})
554
+ throughput = load_test.get("throughput", {})
555
+
556
+ report = f"""
557
+ Performance Benchmark Report
558
+ {"=" * 80}
559
+
560
+ Latency:
561
+ p50: {latency.get("p50", "N/A")} ms
562
+ p75: {latency.get("p75", "N/A")} ms
563
+ p90: {latency.get("p90", "N/A")} ms
564
+ p95: {latency.get("p95", "N/A")} ms
565
+ p99: {latency.get("p99", "N/A")} ms
566
+
567
+ Throughput:
568
+ Requests/sec: {throughput.get("requests_per_sec", "N/A")}
569
+
570
+ Resource Usage:
571
+ """
572
+
573
+ for service, usage in self.results.get("resource_usage", {}).items():
574
+ report += f" {service}:\n"
575
+ report += f" CPU: {usage.get('cpu_percent', 'N/A')}\n"
576
+ report += f" Memory: {usage.get('memory_usage', 'N/A')}\n"
577
+
578
+ report += f"\nStatus: {'PASSED' if self.results.get('passed') else 'FAILED'}\n"
579
+
580
+ if self.results.get("regression_detected"):
581
+ report += "WARNING: Performance regression detected!\n"
582
+
583
+ return report
584
+
585
+
586
+ @log_errors()
587
+ def main() -> None:
588
+ """
589
+ CLI entry point.
590
+
591
+ Raises:
592
+ ValidationError: If command line arguments are invalid
593
+ WorkItemNotFoundError: If work item doesn't exist
594
+ PerformanceTestError: If benchmarks fail
595
+ """
596
+ import sys
597
+
598
+ if len(sys.argv) < 2:
599
+ raise ValidationError(
600
+ message="Missing required argument: work_item_id",
601
+ context={"usage": "python performance_benchmark.py <work_item_id>"},
602
+ remediation="Provide a work item ID as the first argument",
603
+ )
604
+
605
+ work_item_id = sys.argv[1]
606
+
607
+ # Load work item
608
+ work_items_file = Path(".session/tracking/work_items.json")
609
+ try:
610
+ data = load_json(work_items_file)
611
+ work_item = data["work_items"].get(work_item_id)
612
+
613
+ if not work_item:
614
+ raise WorkItemNotFoundError(work_item_id)
615
+
616
+ # Run benchmarks
617
+ benchmark = PerformanceBenchmark(work_item)
618
+ passed, results = benchmark.run_benchmarks()
619
+
620
+ output.info(benchmark.generate_report())
621
+
622
+ sys.exit(0 if passed else 1)
623
+
624
+ except (BenchmarkFailedError, PerformanceRegressionError, LoadTestFailedError) as e:
625
+ logger.error(f"Performance test failed: {e.message}")
626
+ output.info(f"\nERROR: {e.message}")
627
+ if e.remediation:
628
+ output.info(f"REMEDIATION: {e.remediation}")
629
+ sys.exit(e.exit_code)
630
+ except Exception as e:
631
+ logger.exception("Unexpected error during performance benchmarking")
632
+ output.info(f"\nERROR: {e}")
633
+ sys.exit(1)
634
+
635
+
636
+ if __name__ == "__main__":
637
+ main()
@@ -0,0 +1 @@
1
+ """Visualization tools including dependency graphs."""