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,254 @@
1
+ """Learning repository for CRUD operations and data persistence"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import uuid
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from solokit.core.config import get_config_manager
11
+ from solokit.core.error_handlers import log_errors
12
+ from solokit.core.file_ops import load_json, save_json
13
+ from solokit.core.logging_config import get_logger
14
+ from solokit.core.output import get_output
15
+
16
+ logger = get_logger(__name__)
17
+ output = get_output()
18
+
19
+
20
+ class LearningRepository:
21
+ """Manages CRUD operations and data persistence for learnings"""
22
+
23
+ def __init__(self, session_dir: Path):
24
+ """
25
+ Initialize repository
26
+
27
+ Args:
28
+ session_dir: Path to .session directory
29
+ """
30
+ self.session_dir = session_dir
31
+ self.learnings_path = session_dir / "tracking" / "learnings.json"
32
+
33
+ # Load curation config
34
+ config_path = session_dir / "config.json"
35
+ config_manager = get_config_manager()
36
+ config_manager.load_config(config_path)
37
+ self.config = config_manager.curation
38
+
39
+ def load_learnings(self) -> dict[str, Any]:
40
+ """
41
+ Load learnings from file
42
+
43
+ Returns:
44
+ Learnings dictionary with metadata and categories
45
+ """
46
+ if self.learnings_path.exists():
47
+ data = load_json(self.learnings_path)
48
+ # Ensure metadata exists
49
+ if "metadata" not in data:
50
+ data["metadata"] = {
51
+ "total_learnings": self.count_all_learnings(data),
52
+ "last_curated": data.get("last_curated"),
53
+ }
54
+ return data
55
+ else:
56
+ # Create default structure
57
+ return {
58
+ "metadata": {
59
+ "total_learnings": 0,
60
+ "last_curated": None,
61
+ },
62
+ "last_curated": None,
63
+ "curator": "session_curator",
64
+ "categories": {
65
+ "architecture_patterns": [],
66
+ "gotchas": [],
67
+ "best_practices": [],
68
+ "technical_debt": [],
69
+ "performance_insights": [],
70
+ },
71
+ "archived": [],
72
+ }
73
+
74
+ def save_learnings(self, learnings: dict[str, Any]) -> None:
75
+ """
76
+ Save learnings to file
77
+
78
+ Args:
79
+ learnings: Learnings dictionary to save
80
+ """
81
+ save_json(self.learnings_path, learnings)
82
+ logger.debug(f"Saved learnings to {self.learnings_path}")
83
+
84
+ def count_all_learnings(self, learnings: dict[str, Any]) -> int:
85
+ """
86
+ Count all learnings across all categories
87
+
88
+ Args:
89
+ learnings: Learnings dictionary
90
+
91
+ Returns:
92
+ Total count of learnings
93
+ """
94
+ count = 0
95
+ categories = learnings.get("categories", {})
96
+
97
+ for category in categories.values():
98
+ count += len(category)
99
+
100
+ count += len(learnings.get("archived", []))
101
+
102
+ return count
103
+
104
+ def update_total_learnings(self, learnings: dict[str, Any]) -> None:
105
+ """
106
+ Update total_learnings metadata counter
107
+
108
+ Args:
109
+ learnings: Learnings dictionary to update
110
+ """
111
+ if "metadata" not in learnings:
112
+ learnings["metadata"] = {}
113
+ learnings["metadata"]["total_learnings"] = self.count_all_learnings(learnings)
114
+
115
+ @log_errors()
116
+ def add_learning(
117
+ self,
118
+ content: str,
119
+ category: str,
120
+ session: int | None = None,
121
+ tags: list[str] | None = None,
122
+ context: str | None = None,
123
+ ) -> str:
124
+ """
125
+ Add a new learning to the repository
126
+
127
+ Args:
128
+ content: Learning content text
129
+ category: Category to add learning to
130
+ session: Optional session number
131
+ tags: Optional list of tags
132
+ context: Optional context string
133
+
134
+ Returns:
135
+ Learning ID of the created learning
136
+ """
137
+ learnings = self.load_learnings()
138
+
139
+ # Generate unique ID
140
+ learning_id = str(uuid.uuid4())[:8]
141
+
142
+ # Create learning object
143
+ learning: dict[str, Any] = {
144
+ "id": learning_id,
145
+ "content": content,
146
+ "timestamp": datetime.now().isoformat(),
147
+ "learned_in": f"session_{session:03d}" if session else "unknown",
148
+ }
149
+
150
+ if tags:
151
+ learning["tags"] = tags
152
+
153
+ if context:
154
+ learning["context"] = context
155
+
156
+ # Add to category
157
+ categories = learnings.setdefault("categories", {})
158
+ if category not in categories:
159
+ categories[category] = []
160
+
161
+ categories[category].append(learning)
162
+
163
+ # Update total_learnings counter
164
+ self.update_total_learnings(learnings)
165
+
166
+ # Save
167
+ self.save_learnings(learnings)
168
+
169
+ output.info("\n✓ Learning captured!")
170
+ output.info(f" ID: {learning_id}")
171
+ output.info(f" Category: {category}")
172
+ if tags:
173
+ output.info(f" Tags: {', '.join(learning['tags'])}")
174
+ output.info("\nIt will be auto-categorized and curated.\n")
175
+
176
+ return learning_id
177
+
178
+ def add_learning_if_new(
179
+ self, learning_dict: dict[str, Any], similarity_checker: Any = None
180
+ ) -> bool:
181
+ """
182
+ Add learning if it doesn't already exist (based on similarity)
183
+
184
+ Args:
185
+ learning_dict: Learning dictionary to add
186
+ similarity_checker: Optional similarity checker with are_similar method
187
+
188
+ Returns:
189
+ True if learning was added, False if it already exists
190
+ """
191
+ learnings = self.load_learnings()
192
+ categories = learnings.get("categories", {})
193
+
194
+ # Check against all existing learnings if similarity checker provided
195
+ if similarity_checker:
196
+ for category_learnings in categories.values():
197
+ for existing in category_learnings:
198
+ if similarity_checker.are_similar(existing, learning_dict):
199
+ return False # Skip, already exists
200
+
201
+ # Auto-categorize if needed
202
+ category = learning_dict.get("category")
203
+ if not category:
204
+ # Default to best_practices if no category specified
205
+ category = "best_practices"
206
+
207
+ # Add to category
208
+ if category not in categories:
209
+ categories[category] = []
210
+
211
+ # Generate ID if missing
212
+ if "id" not in learning_dict:
213
+ learning_dict["id"] = str(uuid.uuid4())[:8]
214
+
215
+ categories[category].append(learning_dict)
216
+
217
+ # Update total_learnings counter
218
+ self.update_total_learnings(learnings)
219
+
220
+ # Save
221
+ self.save_learnings(learnings)
222
+
223
+ return True # Successfully added
224
+
225
+ def get_curation_config(self) -> Any:
226
+ """
227
+ Get curation configuration
228
+
229
+ Returns:
230
+ Curation config object
231
+ """
232
+ return self.config
233
+
234
+ def learning_exists(
235
+ self,
236
+ category_learnings: list[dict[str, Any]],
237
+ new_learning: dict[str, Any],
238
+ similarity_checker: Any,
239
+ ) -> bool:
240
+ """
241
+ Check if a similar learning already exists in category
242
+
243
+ Args:
244
+ category_learnings: List of learnings in category
245
+ new_learning: New learning to check
246
+ similarity_checker: Similarity checker with are_similar method
247
+
248
+ Returns:
249
+ True if similar learning exists
250
+ """
251
+ for existing in category_learnings:
252
+ if similarity_checker.are_similar(existing, new_learning):
253
+ return True
254
+ return False
@@ -0,0 +1,342 @@
1
+ """Learning similarity detection engine with pluggable algorithms"""
2
+
3
+ from typing import Any, Optional, Protocol
4
+
5
+ from solokit.core.logging_config import get_logger
6
+ from solokit.core.performance import measure_time
7
+
8
+ logger = get_logger(__name__)
9
+
10
+
11
+ # English stopwords for similarity comparison
12
+ ENGLISH_STOPWORDS: set[str] = {
13
+ "the",
14
+ "a",
15
+ "an",
16
+ "and",
17
+ "or",
18
+ "but",
19
+ "in",
20
+ "on",
21
+ "at",
22
+ "to",
23
+ "for",
24
+ "of",
25
+ "with",
26
+ "is",
27
+ "are",
28
+ "was",
29
+ "were",
30
+ "be",
31
+ "been",
32
+ "being",
33
+ "have",
34
+ "has",
35
+ "had",
36
+ "do",
37
+ "does",
38
+ "did",
39
+ "will",
40
+ "would",
41
+ "should",
42
+ "could",
43
+ "may",
44
+ "might",
45
+ "can",
46
+ "shall",
47
+ }
48
+
49
+
50
+ class SimilarityAlgorithm(Protocol):
51
+ """Protocol for similarity algorithms"""
52
+
53
+ def compute_similarity(self, text_a: str, text_b: str) -> float:
54
+ """Compute similarity score between two texts (0.0 to 1.0)"""
55
+ ...
56
+
57
+
58
+ class JaccardContainmentSimilarity:
59
+ """Jaccard + Containment similarity with stopword filtering"""
60
+
61
+ def __init__(
62
+ self,
63
+ stopwords: Optional[set[str]] = None,
64
+ jaccard_threshold: float = 0.6,
65
+ containment_threshold: float = 0.8,
66
+ ) -> None:
67
+ """
68
+ Initialize similarity algorithm
69
+
70
+ Args:
71
+ stopwords: Set of words to ignore (default: ENGLISH_STOPWORDS)
72
+ jaccard_threshold: Threshold for Jaccard similarity (default: 0.6)
73
+ containment_threshold: Threshold for containment similarity (default: 0.8)
74
+ """
75
+ self.stopwords = stopwords or ENGLISH_STOPWORDS
76
+ self.jaccard_threshold = jaccard_threshold
77
+ self.containment_threshold = containment_threshold
78
+
79
+ def compute_similarity(self, text_a: str, text_b: str) -> float:
80
+ """
81
+ Compute combined Jaccard + containment similarity
82
+
83
+ Returns:
84
+ Similarity score 0.0-1.0
85
+ """
86
+ # Normalize text
87
+ text_a = text_a.lower()
88
+ text_b = text_b.lower()
89
+
90
+ # Exact match
91
+ if text_a == text_b:
92
+ return 1.0
93
+
94
+ # Extract meaningful words (remove stopwords)
95
+ words_a = self._extract_words(text_a)
96
+ words_b = self._extract_words(text_b)
97
+
98
+ if len(words_a) == 0 or len(words_b) == 0:
99
+ return 0.0
100
+
101
+ # Calculate both metrics
102
+ jaccard = self._jaccard_similarity(words_a, words_b)
103
+ containment = self._containment_similarity(words_a, words_b)
104
+
105
+ # Return max of the two metrics (high score from either indicates similarity)
106
+ return max(jaccard, containment)
107
+
108
+ def are_similar(self, text_a: str, text_b: str) -> bool:
109
+ """Check if two texts are similar based on thresholds"""
110
+ # Extract words for threshold checking
111
+ text_a = text_a.lower()
112
+ text_b = text_b.lower()
113
+
114
+ # Exact match
115
+ if text_a == text_b:
116
+ return True
117
+
118
+ words_a = self._extract_words(text_a)
119
+ words_b = self._extract_words(text_b)
120
+
121
+ if len(words_a) == 0 or len(words_b) == 0:
122
+ return False
123
+
124
+ jaccard = self._jaccard_similarity(words_a, words_b)
125
+ containment = self._containment_similarity(words_a, words_b)
126
+
127
+ # Similar if either threshold is met
128
+ return jaccard > self.jaccard_threshold or containment > self.containment_threshold
129
+
130
+ def _extract_words(self, text: str) -> set[str]:
131
+ """Extract meaningful words by removing stopwords"""
132
+ return set(w for w in text.split() if w not in self.stopwords)
133
+
134
+ def _jaccard_similarity(self, words_a: set[str], words_b: set[str]) -> float:
135
+ """Calculate Jaccard similarity (intersection over union)"""
136
+ overlap = len(words_a & words_b)
137
+ total = len(words_a | words_b)
138
+ return overlap / total if total > 0 else 0.0
139
+
140
+ def _containment_similarity(self, words_a: set[str], words_b: set[str]) -> float:
141
+ """Calculate containment similarity (one contains the other)"""
142
+ overlap = len(words_a & words_b)
143
+ min_size = min(len(words_a), len(words_b))
144
+ return overlap / min_size if min_size > 0 else 0.0
145
+
146
+
147
+ class LearningSimilarityEngine:
148
+ """
149
+ Main similarity engine with caching and pluggable algorithms
150
+
151
+ Supports multiple similarity algorithms and caches results for performance.
152
+ """
153
+
154
+ def __init__(self, algorithm: Optional[SimilarityAlgorithm] = None) -> None:
155
+ """
156
+ Initialize similarity engine
157
+
158
+ Args:
159
+ algorithm: Similarity algorithm to use (default: JaccardContainmentSimilarity)
160
+ """
161
+ self.algorithm = algorithm or JaccardContainmentSimilarity()
162
+ self._cache: dict[tuple[str, str], float] = {}
163
+ self._word_cache: dict[int, set[str]] = {} # Cache word sets for merge operations
164
+
165
+ def are_similar(self, learning_a: dict, learning_b: dict) -> bool:
166
+ """
167
+ Check if two learnings are similar
168
+
169
+ Args:
170
+ learning_a: First learning dict with 'content' key
171
+ learning_b: Second learning dict with 'content' key
172
+
173
+ Returns:
174
+ True if learnings are similar, False otherwise
175
+ """
176
+ content_a = learning_a.get("content", "")
177
+ content_b = learning_b.get("content", "")
178
+
179
+ # Use cached result if available
180
+ if isinstance(self.algorithm, JaccardContainmentSimilarity):
181
+ return self.algorithm.are_similar(content_a, content_b)
182
+ else:
183
+ # For other algorithms, use threshold of 0.7
184
+ score = self.get_similarity_score(learning_a, learning_b)
185
+ return score > 0.7
186
+
187
+ def get_similarity_score(self, learning_a: dict, learning_b: dict) -> float:
188
+ """
189
+ Get similarity score between two learnings
190
+
191
+ Args:
192
+ learning_a: First learning dict
193
+ learning_b: Second learning dict
194
+
195
+ Returns:
196
+ Similarity score 0.0-1.0
197
+ """
198
+ content_a = learning_a.get("content", "")
199
+ content_b = learning_b.get("content", "")
200
+
201
+ # Check cache
202
+ cache_key = self._make_cache_key(content_a, content_b)
203
+ if cache_key in self._cache:
204
+ return self._cache[cache_key]
205
+
206
+ # Compute similarity
207
+ score = self.algorithm.compute_similarity(content_a, content_b)
208
+
209
+ # Cache result
210
+ self._cache[cache_key] = score
211
+
212
+ return score
213
+
214
+ @measure_time("similarity_merge")
215
+ def merge_similar_learnings(self, learnings: dict) -> int:
216
+ """
217
+ Find and merge similar learnings within each category
218
+
219
+ Optimized with word set caching to avoid redundant word extraction
220
+ during similarity comparisons.
221
+
222
+ Args:
223
+ learnings: Learnings dict with 'categories' key
224
+
225
+ Returns:
226
+ Number of learnings merged
227
+ """
228
+ merged_count = 0
229
+ categories = learnings.get("categories", {})
230
+
231
+ for category_name, category_learnings in categories.items():
232
+ # Clear word cache for new category
233
+ self._word_cache.clear()
234
+
235
+ # Pre-compute word sets for all learnings in this category
236
+ # This optimization converts O(n²) word extraction to O(n)
237
+ for i, learning in enumerate(category_learnings):
238
+ content = learning.get("content", "").lower()
239
+ if isinstance(self.algorithm, JaccardContainmentSimilarity):
240
+ words = self.algorithm._extract_words(content)
241
+ self._word_cache[i] = words
242
+
243
+ to_remove = []
244
+
245
+ for i, learning_a in enumerate(category_learnings):
246
+ if i in to_remove:
247
+ continue
248
+
249
+ for j in range(i + 1, len(category_learnings)):
250
+ if j in to_remove:
251
+ continue
252
+
253
+ learning_b = category_learnings[j]
254
+
255
+ if self.are_similar(learning_a, learning_b):
256
+ self._merge_learning(learning_a, learning_b)
257
+ to_remove.append(j)
258
+ merged_count += 1
259
+ logger.debug(
260
+ f"Merged similar learnings in '{category_name}': "
261
+ f"{learning_a.get('id')} <- {learning_b.get('id')}"
262
+ )
263
+
264
+ # Remove merged learnings
265
+ for idx in sorted(to_remove, reverse=True):
266
+ category_learnings.pop(idx)
267
+
268
+ logger.info(f"Merged {merged_count} similar learnings")
269
+ return merged_count
270
+
271
+ def get_related_learnings(
272
+ self, learnings: dict, learning_id: str, limit: int = 5
273
+ ) -> list[dict]:
274
+ """
275
+ Find learnings related to a specific learning
276
+
277
+ Args:
278
+ learnings: All learnings dict
279
+ learning_id: ID of target learning
280
+ limit: Maximum number of related learnings to return
281
+
282
+ Returns:
283
+ List of related learnings with similarity scores
284
+ """
285
+ # Find target learning
286
+ target_learning = self._find_learning_by_id(learnings, learning_id)
287
+ if not target_learning:
288
+ return []
289
+
290
+ # Calculate similarity scores for all other learnings
291
+ similarities = []
292
+ categories = learnings.get("categories", {})
293
+
294
+ for category_learnings in categories.values():
295
+ for learning in category_learnings:
296
+ if learning.get("id") != learning_id:
297
+ score = self.get_similarity_score(target_learning, learning)
298
+ if score > 0.3: # Only include somewhat similar learnings
299
+ similarities.append((score, learning))
300
+
301
+ # Sort by similarity and return top matches
302
+ similarities.sort(key=lambda x: x[0], reverse=True)
303
+ return [{**learning, "similarity_score": score} for score, learning in similarities[:limit]]
304
+
305
+ def clear_cache(self) -> None:
306
+ """Clear the similarity cache"""
307
+ self._cache.clear()
308
+ self._word_cache.clear()
309
+ logger.debug("Similarity cache cleared")
310
+
311
+ def _make_cache_key(self, text_a: str, text_b: str) -> tuple[str, str]:
312
+ """Create cache key for two texts (order-independent)"""
313
+ # Use sorted tuple for order-independent caching
314
+ sorted_texts = sorted([text_a, text_b])
315
+ return (sorted_texts[0], sorted_texts[1])
316
+
317
+ def _merge_learning(self, target: dict, source: dict) -> None:
318
+ """Merge source learning into target"""
319
+ # Merge applies_to
320
+ target_applies = set(target.get("applies_to", []))
321
+ source_applies = set(source.get("applies_to", []))
322
+ target["applies_to"] = list(target_applies | source_applies)
323
+
324
+ # Merge tags
325
+ target_tags = set(target.get("tags", []))
326
+ source_tags = set(source.get("tags", []))
327
+ target["tags"] = list(target_tags | source_tags)
328
+
329
+ # Use longer content
330
+ if len(source.get("content", "")) > len(target.get("content", "")):
331
+ target["content"] = source["content"]
332
+
333
+ def _find_learning_by_id(
334
+ self, learnings: dict[str, Any], learning_id: str
335
+ ) -> Optional[dict[str, Any]]:
336
+ """Find a learning by its ID"""
337
+ categories = learnings.get("categories", {})
338
+ for category_learnings in categories.values():
339
+ for learning in category_learnings:
340
+ if learning.get("id") == learning_id:
341
+ return learning # type: ignore[no-any-return]
342
+ return None