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,352 @@
1
+ """Learning extraction module for various sources"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from solokit.core.command_runner import CommandRunner
10
+ from solokit.core.constants import GIT_STANDARD_TIMEOUT
11
+ from solokit.core.error_handlers import log_errors
12
+ from solokit.core.exceptions import FileOperationError
13
+ from solokit.core.file_ops import load_json
14
+ from solokit.core.logging_config import get_logger
15
+
16
+ logger = get_logger(__name__)
17
+
18
+
19
+ class LearningExtractor:
20
+ """Extracts learnings from various sources (sessions, git commits, code comments)"""
21
+
22
+ def __init__(self, session_dir: Path, project_root: Path | None = None):
23
+ """
24
+ Initialize learning extractor
25
+
26
+ Args:
27
+ session_dir: Path to .session directory
28
+ project_root: Path to project root (for git operations)
29
+ """
30
+ self.session_dir = session_dir
31
+ self.project_root = project_root or Path.cwd()
32
+ self.runner = CommandRunner(
33
+ default_timeout=GIT_STANDARD_TIMEOUT, working_dir=self.project_root
34
+ )
35
+
36
+ def extract_from_sessions(self) -> list[dict[str, Any]]:
37
+ """
38
+ Extract learnings from session summary JSON files
39
+
40
+ Returns:
41
+ List of learning dictionaries
42
+ """
43
+ learnings: list[dict[str, Any]] = []
44
+ summaries_dir = self.session_dir / "summaries"
45
+
46
+ if not summaries_dir.exists():
47
+ return learnings
48
+
49
+ # Look for session summary files
50
+ for summary_file in summaries_dir.glob("session_*.json"):
51
+ try:
52
+ summary_data = load_json(summary_file)
53
+
54
+ # Extract learnings from various fields
55
+ session_id = summary_file.stem.replace("session_", "")
56
+
57
+ # Check for explicit learnings field
58
+ if "learnings" in summary_data:
59
+ for learning_text in summary_data["learnings"]:
60
+ learnings.append(
61
+ {
62
+ "content": learning_text,
63
+ "learned_in": session_id,
64
+ "timestamp": summary_data.get("timestamp", ""),
65
+ }
66
+ )
67
+
68
+ # Extract from challenges as potential gotchas
69
+ if "challenges_encountered" in summary_data:
70
+ for challenge in summary_data["challenges_encountered"]:
71
+ learnings.append(
72
+ {
73
+ "content": f"Challenge: {challenge}",
74
+ "learned_in": session_id,
75
+ "timestamp": summary_data.get("timestamp", ""),
76
+ "suggested_type": "gotcha",
77
+ }
78
+ )
79
+
80
+ except (ValueError, KeyError, FileOperationError) as e:
81
+ # Skip invalid summary files
82
+ logger.warning(f"Failed to extract learnings from {summary_file}: {e}")
83
+ continue
84
+
85
+ logger.info(f"Extracted {len(learnings)} learnings from session summaries")
86
+ return learnings
87
+
88
+ @log_errors()
89
+ def extract_from_session_summary(
90
+ self, session_file: Path, validator: Any = None
91
+ ) -> list[dict[str, Any]]:
92
+ """
93
+ Extract learnings from a session summary markdown file
94
+
95
+ Args:
96
+ session_file: Path to session summary markdown file
97
+ validator: Optional validator instance with create_learning_entry and validate_learning methods
98
+
99
+ Returns:
100
+ List of learning dictionaries extracted from the file
101
+ """
102
+ if not session_file.exists():
103
+ return []
104
+
105
+ try:
106
+ with open(session_file) as f:
107
+ content = f.read()
108
+ except (OSError, Exception) as e:
109
+ logger.warning(f"Failed to read session summary {session_file}: {e}")
110
+ return []
111
+
112
+ learnings = []
113
+
114
+ # Extract session number from filename (e.g., session_005_summary.md)
115
+ session_match = re.search(r"session_(\d+)", session_file.name)
116
+ session_num = int(session_match.group(1)) if session_match else 0
117
+
118
+ # Look for "Challenges Encountered" or "Learnings Captured" sections
119
+ patterns = [
120
+ r"##\s*Challenges?\s*Encountered\s*\n(.*?)(?=\n##|\Z)",
121
+ r"##\s*Learnings?\s*Captured\s*\n(.*?)(?=\n##|\Z)",
122
+ ]
123
+
124
+ for pattern in patterns:
125
+ matches = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)
126
+ for match in matches:
127
+ # Each bullet point is a potential learning
128
+ for line in match.split("\n"):
129
+ line = line.strip()
130
+ if line.startswith("-") or line.startswith("*"):
131
+ learning_text = line.lstrip("-*").strip()
132
+ # Basic validation
133
+ if learning_text and self._is_valid_content(learning_text):
134
+ if validator:
135
+ # Use validator for standardized entry creation
136
+ entry = validator.create_learning_entry(
137
+ content=learning_text,
138
+ source="session_summary",
139
+ session_id=f"session_{session_num:03d}",
140
+ context=f"Session summary file: {session_file.name}",
141
+ )
142
+ if validator.validate_learning(entry):
143
+ learnings.append(entry)
144
+ else:
145
+ # Simple entry without validation
146
+ learnings.append(
147
+ {
148
+ "content": learning_text,
149
+ "learned_in": f"session_{session_num:03d}",
150
+ "source": "session_summary",
151
+ "context": f"Session summary file: {session_file.name}",
152
+ }
153
+ )
154
+
155
+ logger.info(f"Extracted {len(learnings)} learnings from {session_file.name}")
156
+ return learnings
157
+
158
+ @log_errors()
159
+ def extract_from_git_commits(
160
+ self, since_session: int = 0, session_id: str | None = None, validator: Any = None
161
+ ) -> list[dict[str, Any]]:
162
+ """
163
+ Extract learnings from git commit messages with LEARNING: annotations
164
+
165
+ Args:
166
+ since_session: Extract only commits after this session number
167
+ session_id: Session ID to tag learnings with
168
+ validator: Optional validator instance
169
+
170
+ Returns:
171
+ List of learning dictionaries extracted from commit messages
172
+ """
173
+ try:
174
+ # Get recent commits
175
+ result = self.runner.run(["git", "log", "--format=%H|||%B", "-n", "100"])
176
+
177
+ if not result.success:
178
+ return []
179
+
180
+ learnings = []
181
+ # Updated regex to capture multi-line LEARNING statements
182
+ # Captures until: double newline (blank line) OR end of string
183
+ learning_pattern = r"LEARNING:\s*([\s\S]+?)(?=\n\n|\Z)"
184
+
185
+ # Parse commit messages
186
+ commits_raw = result.stdout.strip()
187
+ if not commits_raw:
188
+ return []
189
+
190
+ # Each commit starts with hash|||, split on newline followed by hash pattern
191
+ commit_blocks = re.split(r"\n(?=[a-f0-9]{40}\|\|\|)", commits_raw)
192
+
193
+ for commit_block in commit_blocks:
194
+ if "|||" not in commit_block:
195
+ continue
196
+
197
+ commit_hash, message = commit_block.split("|||", 1)
198
+
199
+ # Find LEARNING: annotations
200
+ for match in re.finditer(learning_pattern, message, re.MULTILINE):
201
+ learning_text = match.group(1).strip()
202
+ # Basic validation
203
+ if learning_text and self._is_valid_content(learning_text):
204
+ if validator:
205
+ # Use validator for standardized entry creation
206
+ entry = validator.create_learning_entry(
207
+ content=learning_text,
208
+ source="git_commit",
209
+ session_id=session_id,
210
+ context=f"Commit {commit_hash[:8]}",
211
+ )
212
+ if validator.validate_learning(entry):
213
+ learnings.append(entry)
214
+ else:
215
+ # Simple entry without validation
216
+ learnings.append(
217
+ {
218
+ "content": learning_text,
219
+ "learned_in": session_id or "unknown",
220
+ "source": "git_commit",
221
+ "context": f"Commit {commit_hash[:8]}",
222
+ }
223
+ )
224
+
225
+ logger.info(f"Extracted {len(learnings)} learnings from git commits")
226
+ return learnings
227
+
228
+ except Exception as e:
229
+ logger.warning(f"Failed to extract learnings from git commits: {e}")
230
+ return []
231
+
232
+ @log_errors()
233
+ def extract_from_code_comments(
234
+ self,
235
+ changed_files: list[Path] | None = None,
236
+ session_id: str | None = None,
237
+ validator: Any = None,
238
+ ) -> list[dict[str, Any]]:
239
+ """
240
+ Extract learnings from inline code comments (not documentation)
241
+
242
+ Args:
243
+ changed_files: List of file paths to scan (or None to auto-detect from git)
244
+ session_id: Session ID to tag learnings with
245
+ validator: Optional validator instance
246
+
247
+ Returns:
248
+ List of learning dictionaries extracted from code comments
249
+ """
250
+ if changed_files is None:
251
+ # Get recently changed files from git
252
+ try:
253
+ result = self.runner.run(["git", "diff", "--name-only", "HEAD~5", "HEAD"])
254
+
255
+ if result.success:
256
+ changed_files = [
257
+ self.project_root / f.strip()
258
+ for f in result.stdout.split("\n")
259
+ if f.strip()
260
+ ]
261
+ else:
262
+ changed_files = []
263
+ except Exception as e:
264
+ logger.warning(f"Failed to get changed files from git: {e}")
265
+ changed_files = []
266
+
267
+ learnings = []
268
+ # Pattern must match actual comment lines (starting with #), not string literals
269
+ learning_pattern = r"^\s*#\s*LEARNING:\s*(.+?)$"
270
+
271
+ # Only scan actual code files, not documentation
272
+ code_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".go", ".rs"}
273
+ doc_extensions = {".md", ".txt", ".rst"}
274
+ excluded_dirs = {"examples", "templates", "tests", "test", "__tests__", "spec"}
275
+
276
+ for file_path in changed_files:
277
+ if not file_path.exists() or not file_path.is_file():
278
+ continue
279
+
280
+ # Skip documentation files
281
+ if file_path.suffix in doc_extensions:
282
+ continue
283
+
284
+ # Skip example/template/test directories
285
+ if any(excluded_dir in file_path.parts for excluded_dir in excluded_dirs):
286
+ continue
287
+
288
+ # Only process code files
289
+ if file_path.suffix not in code_extensions:
290
+ continue
291
+
292
+ # Skip binary files and large files
293
+ if file_path.stat().st_size > 1_000_000:
294
+ continue
295
+
296
+ try:
297
+ with open(file_path, encoding="utf-8", errors="ignore") as f:
298
+ for line_num, line in enumerate(f, 1):
299
+ match = re.search(learning_pattern, line)
300
+ if match:
301
+ learning_text = match.group(1).strip()
302
+ # Basic validation
303
+ if learning_text and self._is_valid_content(learning_text):
304
+ if validator:
305
+ # Use validator for standardized entry creation
306
+ entry = validator.create_learning_entry(
307
+ content=learning_text,
308
+ source="inline_comment",
309
+ session_id=session_id,
310
+ context=f"{file_path.name}:{line_num}",
311
+ )
312
+ if validator.validate_learning(entry):
313
+ learnings.append(entry)
314
+ else:
315
+ # Simple entry without validation
316
+ learnings.append(
317
+ {
318
+ "content": learning_text,
319
+ "learned_in": session_id or "unknown",
320
+ "source": "inline_comment",
321
+ "context": f"{file_path.name}:{line_num}",
322
+ }
323
+ )
324
+ except (OSError, UnicodeDecodeError) as e:
325
+ logger.warning(f"Failed to read file {file_path}: {e}")
326
+ continue
327
+
328
+ logger.info(f"Extracted {len(learnings)} learnings from code comments")
329
+ return learnings
330
+
331
+ def _is_valid_content(self, content: str) -> bool:
332
+ """
333
+ Basic validation for learning content
334
+
335
+ Args:
336
+ content: Content to validate
337
+
338
+ Returns:
339
+ True if content appears valid
340
+ """
341
+ if not content or not isinstance(content, str):
342
+ return False
343
+
344
+ # Skip placeholders and examples
345
+ if "<" in content or ">" in content:
346
+ return False
347
+
348
+ # Must have substance (more than just a few words)
349
+ if len(content.split()) < 5:
350
+ return False
351
+
352
+ return True
@@ -0,0 +1,351 @@
1
+ """Learning reporting and statistics module"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+ from typing import Any
7
+
8
+ from solokit.core.logging_config import get_logger
9
+ from solokit.core.output import get_output
10
+
11
+ logger = get_logger(__name__)
12
+ output = get_output()
13
+
14
+
15
+ class LearningReporter:
16
+ """Handles learning reports, statistics, searches, and display functionality"""
17
+
18
+ def __init__(self, repository: Any):
19
+ """
20
+ Initialize reporter
21
+
22
+ Args:
23
+ repository: LearningRepository instance for data access
24
+ """
25
+ self.repository = repository
26
+
27
+ def generate_report(self) -> None:
28
+ """Generate learning summary report"""
29
+ output.section("Learning Summary Report")
30
+
31
+ learnings = self.repository.load_learnings()
32
+
33
+ # Create table
34
+ output.info("Learnings by Category:")
35
+ output.info("-" * 40)
36
+
37
+ categories = learnings.get("categories", {})
38
+ total = 0
39
+
40
+ for category_name, category_learnings in categories.items():
41
+ count = len(category_learnings)
42
+ total += count
43
+ formatted_name = category_name.replace("_", " ").title()
44
+ output.info(f"{formatted_name:<30} {count:>5}")
45
+
46
+ # Add archived
47
+ archived_count = len(learnings.get("archived", []))
48
+ if archived_count > 0:
49
+ output.info(f"{'Archived':<30} {archived_count:>5}")
50
+
51
+ # Add total
52
+ output.info("-" * 40)
53
+ output.info(f"{'Total':<30} {total:>5}")
54
+ output.info("")
55
+
56
+ # Show last curated
57
+ last_curated = learnings.get("last_curated")
58
+ if last_curated:
59
+ output.info(f"Last curated: {last_curated}\n")
60
+ else:
61
+ output.info("Never curated\n")
62
+
63
+ def search_learnings(self, query: str) -> None:
64
+ """
65
+ Search learnings by keyword
66
+
67
+ Args:
68
+ query: Search query string
69
+ """
70
+ learnings = self.repository.load_learnings()
71
+ categories = learnings.get("categories", {})
72
+
73
+ query_lower = query.lower()
74
+ matches = []
75
+
76
+ # Search through all learnings
77
+ for category_name, category_learnings in categories.items():
78
+ for learning in category_learnings:
79
+ # Search in content
80
+ content = learning.get("content", "").lower()
81
+ tags = learning.get("tags", [])
82
+ context = learning.get("context", "").lower()
83
+
84
+ if (
85
+ query_lower in content
86
+ or query_lower in context
87
+ or any(query_lower in tag.lower() for tag in tags)
88
+ ):
89
+ matches.append({**learning, "category": category_name})
90
+
91
+ # Display results
92
+ if not matches:
93
+ output.info(f"\nNo learnings found matching '{query}'\n")
94
+ return
95
+
96
+ output.info(f"\n=== Search Results for '{query}' ===\n")
97
+ output.info(f"Found {len(matches)} matching learning(s):\n")
98
+
99
+ for i, learning in enumerate(matches, 1):
100
+ output.info(f"{i}. [{learning['category'].replace('_', ' ').title()}]")
101
+ output.info(f" {learning['content']}")
102
+
103
+ if "tags" in learning:
104
+ output.info(f" Tags: {', '.join(learning['tags'])}")
105
+
106
+ output.info(f" Session: {learning.get('learned_in', 'unknown')}")
107
+ output.info(f" ID: {learning.get('id', 'N/A')}")
108
+ output.info("")
109
+
110
+ def show_learnings(
111
+ self,
112
+ category: str | None = None,
113
+ tag: str | None = None,
114
+ session: int | None = None,
115
+ date_from: str | None = None,
116
+ date_to: str | None = None,
117
+ include_archived: bool = False,
118
+ ) -> None:
119
+ """
120
+ Show learnings with optional filters
121
+
122
+ Args:
123
+ category: Filter by category name
124
+ tag: Filter by tag
125
+ session: Filter by session number
126
+ date_from: Filter by start date
127
+ date_to: Filter by end date
128
+ include_archived: Include archived learnings
129
+ """
130
+ learnings = self.repository.load_learnings()
131
+ categories = learnings.get("categories", {})
132
+
133
+ # Apply filters
134
+ filtered = []
135
+ for category_name, category_learnings in categories.items():
136
+ # Category filter
137
+ if category and category_name != category:
138
+ continue
139
+
140
+ for learning in category_learnings:
141
+ # Tag filter
142
+ if tag and tag not in learning.get("tags", []):
143
+ continue
144
+
145
+ # Session filter
146
+ if session:
147
+ learned_in = learning.get("learned_in", "")
148
+ session_num = self._extract_session_number(learned_in)
149
+ if session_num != session:
150
+ continue
151
+
152
+ # Date range filter
153
+ if date_from or date_to:
154
+ learning_date = learning.get("timestamp", "")
155
+ if date_from and learning_date < date_from:
156
+ continue
157
+ if date_to and learning_date > date_to:
158
+ continue
159
+
160
+ filtered.append({**learning, "category": category_name})
161
+
162
+ # Display results
163
+ if not filtered:
164
+ output.info("\nNo learnings found matching the filters\n")
165
+ return
166
+
167
+ if category:
168
+ # Show specific category
169
+ output.info(f"\n{category.replace('_', ' ').title()}\n")
170
+ output.info("=" * 50)
171
+
172
+ for i, learning in enumerate(filtered, 1):
173
+ output.info(f"\n{i}. {learning.get('content', 'N/A')}")
174
+ if "tags" in learning:
175
+ output.info(f" Tags: {', '.join(learning['tags'])}")
176
+ if "learned_in" in learning:
177
+ output.info(f" Learned in: {learning['learned_in']}")
178
+ if "timestamp" in learning:
179
+ output.info(f" Date: {learning['timestamp']}")
180
+ output.info(f" ID: {learning.get('id', 'N/A')}")
181
+ else:
182
+ # Show all categories
183
+ grouped: dict[str, list[Any]] = {}
184
+ for learning in filtered:
185
+ cat = learning["category"]
186
+ if cat not in grouped:
187
+ grouped[cat] = []
188
+ grouped[cat].append(learning)
189
+
190
+ for category_name, category_learnings in grouped.items():
191
+ output.info(f"\n{category_name.replace('_', ' ').title()}")
192
+ output.info(f"Count: {len(category_learnings)}\n")
193
+
194
+ # Show first 3
195
+ for learning in category_learnings[:3]:
196
+ output.info(f" • {learning.get('content', 'N/A')}")
197
+ if "tags" in learning:
198
+ output.info(f" Tags: {', '.join(learning['tags'])}")
199
+
200
+ if len(category_learnings) > 3:
201
+ output.info(f" ... and {len(category_learnings) - 3} more")
202
+
203
+ output.info("")
204
+
205
+ def generate_statistics(self) -> dict[str, Any]:
206
+ """
207
+ Generate learning statistics
208
+
209
+ Returns:
210
+ Dictionary with statistics (total, by_category, by_tag, top_tags, by_session)
211
+ """
212
+ learnings = self.repository.load_learnings()
213
+ categories = learnings.get("categories", {})
214
+
215
+ stats: dict[str, Any] = {
216
+ "total": 0,
217
+ "by_category": {},
218
+ "by_tag": {},
219
+ "top_tags": [],
220
+ "by_session": {},
221
+ }
222
+
223
+ # Count by category
224
+ for cat, items in categories.items():
225
+ count = len(items)
226
+ stats["by_category"][cat] = count
227
+ stats["total"] += count
228
+
229
+ # Count by tag and session
230
+ tag_counts: dict[str, int] = {}
231
+ session_counts: dict[int, int] = {}
232
+
233
+ for items in categories.values():
234
+ for learning in items:
235
+ # Tag counts
236
+ for tag in learning.get("tags", []):
237
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
238
+
239
+ # Session counts
240
+ learned_in = learning.get("learned_in", "unknown")
241
+ session_num = self._extract_session_number(learned_in)
242
+ if session_num > 0:
243
+ session_counts[session_num] = session_counts.get(session_num, 0) + 1
244
+
245
+ # Top tags
246
+ stats["top_tags"] = sorted(tag_counts.items(), key=lambda x: x[1], reverse=True)[:10]
247
+ stats["by_tag"] = tag_counts
248
+ stats["by_session"] = session_counts
249
+
250
+ return stats
251
+
252
+ def show_statistics(self) -> None:
253
+ """Display learning statistics"""
254
+ stats = self.generate_statistics()
255
+
256
+ output.section("Learning Statistics")
257
+
258
+ # Total
259
+ output.info(f"Total learnings: {stats['total']}\n")
260
+
261
+ # By category
262
+ output.info("By Category:")
263
+ output.info("-" * 40)
264
+ for cat, count in stats["by_category"].items():
265
+ cat_name = cat.replace("_", " ").title()
266
+ output.info(f" {cat_name:<30} {count:>5}")
267
+
268
+ # Top tags
269
+ if stats["top_tags"]:
270
+ output.info("\nTop Tags:")
271
+ output.info("-" * 40)
272
+ for tag, count in stats["top_tags"]:
273
+ output.info(f" {tag:<30} {count:>5}")
274
+
275
+ # Sessions with most learnings
276
+ if stats["by_session"]:
277
+ top_sessions = sorted(stats["by_session"].items(), key=lambda x: x[1], reverse=True)[:5]
278
+ output.info("\nSessions with Most Learnings:")
279
+ output.info("-" * 40)
280
+ for session_num, count in top_sessions:
281
+ output.info(f" Session {session_num:<22} {count:>5}")
282
+
283
+ output.info("")
284
+
285
+ def show_timeline(self, sessions: int = 10) -> None:
286
+ """
287
+ Show learning timeline for recent sessions
288
+
289
+ Args:
290
+ sessions: Number of recent sessions to display
291
+ """
292
+ learnings = self.repository.load_learnings()
293
+ categories = learnings.get("categories", {})
294
+
295
+ # Group by session
296
+ by_session: dict[int, list[Any]] = {}
297
+ for items in categories.values():
298
+ for learning in items:
299
+ learned_in = learning.get("learned_in", "unknown")
300
+ session = self._extract_session_number(learned_in)
301
+ if session > 0:
302
+ if session not in by_session:
303
+ by_session[session] = []
304
+ by_session[session].append(learning)
305
+
306
+ if not by_session:
307
+ output.info("\nNo session timeline available\n")
308
+ return
309
+
310
+ # Display recent sessions
311
+ recent = sorted(by_session.keys(), reverse=True)[:sessions]
312
+
313
+ output.info(f"\n=== Learning Timeline (Last {min(len(recent), sessions)} Sessions) ===\n")
314
+
315
+ for session in recent:
316
+ session_learnings = by_session[session]
317
+ count = len(session_learnings)
318
+
319
+ output.info(f"Session {session:03d}: {count} learning(s)")
320
+
321
+ # Show first 3 learnings
322
+ for learning in session_learnings[:3]:
323
+ content = learning.get("content", "")
324
+ # Truncate long learnings
325
+ if len(content) > 60:
326
+ content = content[:57] + "..."
327
+ output.info(f" - {content}")
328
+
329
+ if len(session_learnings) > 3:
330
+ output.info(f" ... and {len(session_learnings) - 3} more")
331
+
332
+ output.info("")
333
+
334
+ def _extract_session_number(self, session_id: str) -> int:
335
+ """
336
+ Extract numeric session number from session ID
337
+
338
+ Args:
339
+ session_id: Session ID string
340
+
341
+ Returns:
342
+ Extracted session number or 0
343
+ """
344
+ try:
345
+ match = re.search(r"\d+", session_id)
346
+ if match:
347
+ return int(match.group())
348
+ except (ValueError, AttributeError) as e:
349
+ logger.debug(f"Failed to extract session number from '{session_id}': {e}")
350
+ return 0
351
+ return 0