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.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- solokit-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Template Installer Module
|
|
3
|
+
|
|
4
|
+
Handles template file installation with tier-based structure.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, cast
|
|
15
|
+
|
|
16
|
+
from solokit.core.exceptions import FileOperationError, TemplateNotFoundError
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_template_registry() -> dict[str, Any]:
|
|
22
|
+
"""
|
|
23
|
+
Load template registry from templates/template-registry.json.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Template registry dictionary
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
TemplateNotFoundError: If registry file not found
|
|
30
|
+
FileOperationError: If registry file is invalid JSON
|
|
31
|
+
"""
|
|
32
|
+
registry_path = Path(__file__).parent.parent / "templates" / "template-registry.json"
|
|
33
|
+
|
|
34
|
+
if not registry_path.exists():
|
|
35
|
+
raise TemplateNotFoundError(
|
|
36
|
+
template_name="template-registry.json",
|
|
37
|
+
template_path=str(registry_path.parent),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
with open(registry_path) as f:
|
|
42
|
+
return cast(dict[str, Any], json.load(f))
|
|
43
|
+
except json.JSONDecodeError as e:
|
|
44
|
+
raise FileOperationError(
|
|
45
|
+
operation="parse",
|
|
46
|
+
file_path=str(registry_path),
|
|
47
|
+
details=f"Invalid JSON in template registry: {str(e)}",
|
|
48
|
+
cause=e,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def get_template_info(template_id: str) -> dict[str, Any]:
|
|
53
|
+
"""
|
|
54
|
+
Get template information from registry.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
template_id: Template identifier (e.g., "saas_t3")
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Template metadata dictionary
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TemplateNotFoundError: If template not found in registry
|
|
64
|
+
"""
|
|
65
|
+
registry = load_template_registry()
|
|
66
|
+
|
|
67
|
+
if template_id not in registry["templates"]:
|
|
68
|
+
available = ", ".join(registry["templates"].keys())
|
|
69
|
+
raise TemplateNotFoundError(
|
|
70
|
+
template_name=template_id,
|
|
71
|
+
template_path=f"Available templates: {available}",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
return cast(dict[str, Any], registry["templates"][template_id])
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def get_template_directory(template_id: str) -> Path:
|
|
78
|
+
"""
|
|
79
|
+
Get path to template directory.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
template_id: Template identifier (e.g., "saas_t3")
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Path to template directory
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
TemplateNotFoundError: If template directory doesn't exist
|
|
89
|
+
"""
|
|
90
|
+
templates_root = Path(__file__).parent.parent / "templates"
|
|
91
|
+
template_dir = templates_root / template_id
|
|
92
|
+
|
|
93
|
+
if not template_dir.exists():
|
|
94
|
+
raise TemplateNotFoundError(template_name=template_id, template_path=str(templates_root))
|
|
95
|
+
|
|
96
|
+
return template_dir
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def copy_directory_tree(src: Path, dst: Path, skip_patterns: list[str] | None = None) -> int:
|
|
100
|
+
"""
|
|
101
|
+
Recursively copy directory tree from src to dst.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
src: Source directory
|
|
105
|
+
dst: Destination directory
|
|
106
|
+
skip_patterns: List of filename patterns to skip (e.g., [".template", "__pycache__"])
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Number of files copied
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
FileOperationError: If copy operation fails
|
|
113
|
+
"""
|
|
114
|
+
if skip_patterns is None:
|
|
115
|
+
skip_patterns = []
|
|
116
|
+
|
|
117
|
+
files_copied = 0
|
|
118
|
+
|
|
119
|
+
try:
|
|
120
|
+
# Create destination directory
|
|
121
|
+
dst.mkdir(parents=True, exist_ok=True)
|
|
122
|
+
|
|
123
|
+
# Copy all files and subdirectories
|
|
124
|
+
for item in src.iterdir():
|
|
125
|
+
# Skip patterns
|
|
126
|
+
if any(pattern in item.name for pattern in skip_patterns):
|
|
127
|
+
continue
|
|
128
|
+
|
|
129
|
+
src_item = src / item.name
|
|
130
|
+
dst_item = dst / item.name
|
|
131
|
+
|
|
132
|
+
if src_item.is_dir():
|
|
133
|
+
files_copied += copy_directory_tree(src_item, dst_item, skip_patterns)
|
|
134
|
+
else:
|
|
135
|
+
shutil.copy2(src_item, dst_item)
|
|
136
|
+
files_copied += 1
|
|
137
|
+
logger.debug(f"Copied {src_item} -> {dst_item}")
|
|
138
|
+
|
|
139
|
+
except Exception as e:
|
|
140
|
+
raise FileOperationError(
|
|
141
|
+
operation="copy",
|
|
142
|
+
file_path=str(src),
|
|
143
|
+
details=f"Failed to copy directory tree: {str(e)}",
|
|
144
|
+
cause=e,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
return files_copied
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def replace_placeholders(content: str, replacements: dict[str, str]) -> str:
|
|
151
|
+
"""
|
|
152
|
+
Replace placeholders in template content.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
content: Template content with placeholders like {project_name}
|
|
156
|
+
replacements: Dictionary of placeholder -> value
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
Content with placeholders replaced
|
|
160
|
+
"""
|
|
161
|
+
result = content
|
|
162
|
+
for placeholder, value in replacements.items():
|
|
163
|
+
result = result.replace(f"{{{placeholder}}}", value)
|
|
164
|
+
return result
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def install_base_template(
|
|
168
|
+
template_id: str, project_root: Path, replacements: dict[str, str]
|
|
169
|
+
) -> int:
|
|
170
|
+
"""
|
|
171
|
+
Install base template files.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
template_id: Template identifier
|
|
175
|
+
project_root: Project root directory
|
|
176
|
+
replacements: Placeholder replacements (e.g., {"project_name": "my-app"})
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Number of files copied
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
TemplateNotFoundError: If template not found
|
|
183
|
+
FileOperationError: If installation fails
|
|
184
|
+
"""
|
|
185
|
+
template_dir = get_template_directory(template_id)
|
|
186
|
+
base_dir = template_dir / "base"
|
|
187
|
+
|
|
188
|
+
if not base_dir.exists():
|
|
189
|
+
raise TemplateNotFoundError(
|
|
190
|
+
template_name=f"{template_id}/base", template_path=str(template_dir)
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
logger.info(f"Installing base template files from {template_id}...")
|
|
194
|
+
|
|
195
|
+
# Copy base files
|
|
196
|
+
files_copied = copy_directory_tree(
|
|
197
|
+
base_dir, project_root, skip_patterns=[".template", "__pycache__", ".pyc"]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Process template files (files ending in .template)
|
|
201
|
+
for template_file in base_dir.rglob("*.template"):
|
|
202
|
+
relative_path = template_file.relative_to(base_dir)
|
|
203
|
+
# Remove .template extension for output file
|
|
204
|
+
output_path = project_root / relative_path.parent / relative_path.stem
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
content = template_file.read_text()
|
|
208
|
+
processed_content = replace_placeholders(content, replacements)
|
|
209
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
output_path.write_text(processed_content)
|
|
211
|
+
logger.debug(f"Processed template: {template_file.name} -> {output_path.name}")
|
|
212
|
+
files_copied += 1
|
|
213
|
+
except Exception as e:
|
|
214
|
+
raise FileOperationError(
|
|
215
|
+
operation="process",
|
|
216
|
+
file_path=str(template_file),
|
|
217
|
+
details=f"Failed to process template file: {str(e)}",
|
|
218
|
+
cause=e,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
logger.info(f"Installed {files_copied} base files")
|
|
222
|
+
return files_copied
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def install_tier_files(
|
|
226
|
+
template_id: str, tier: str, project_root: Path, replacements: dict[str, str]
|
|
227
|
+
) -> int:
|
|
228
|
+
"""
|
|
229
|
+
Install tier-specific files.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
template_id: Template identifier
|
|
233
|
+
tier: Tier name (e.g., "tier-1-essential")
|
|
234
|
+
project_root: Project root directory
|
|
235
|
+
replacements: Placeholder replacements
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
Number of files copied/processed
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
TemplateNotFoundError: If tier directory not found
|
|
242
|
+
FileOperationError: If installation fails
|
|
243
|
+
"""
|
|
244
|
+
template_dir = get_template_directory(template_id)
|
|
245
|
+
tier_dir = template_dir / tier
|
|
246
|
+
|
|
247
|
+
if not tier_dir.exists():
|
|
248
|
+
logger.warning(f"Tier directory not found: {tier_dir}")
|
|
249
|
+
return 0
|
|
250
|
+
|
|
251
|
+
logger.info(f"Installing {tier} files...")
|
|
252
|
+
|
|
253
|
+
# Copy tier files
|
|
254
|
+
files_copied = copy_directory_tree(
|
|
255
|
+
tier_dir, project_root, skip_patterns=[".template", "__pycache__", ".pyc"]
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
# Process template files
|
|
259
|
+
for template_file in tier_dir.rglob("*.template"):
|
|
260
|
+
relative_path = template_file.relative_to(tier_dir)
|
|
261
|
+
|
|
262
|
+
# Strip tier suffix from filename if present (e.g., package.json.tier4.template -> package.json)
|
|
263
|
+
# This allows tier files to overwrite the base file instead of creating separate tier files
|
|
264
|
+
filename_without_template = relative_path.stem # Removes .template
|
|
265
|
+
|
|
266
|
+
# Remove tier suffixes: .tier1, .tier2, .tier3, .tier4
|
|
267
|
+
filename_without_tier = re.sub(r"\.tier[1-4]$", "", filename_without_template)
|
|
268
|
+
|
|
269
|
+
output_path = project_root / relative_path.parent / filename_without_tier
|
|
270
|
+
|
|
271
|
+
try:
|
|
272
|
+
content = template_file.read_text()
|
|
273
|
+
processed_content = replace_placeholders(content, replacements)
|
|
274
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
275
|
+
output_path.write_text(processed_content)
|
|
276
|
+
logger.debug(f"Processed template: {template_file.name} -> {output_path.name}")
|
|
277
|
+
files_copied += 1
|
|
278
|
+
except Exception as e:
|
|
279
|
+
raise FileOperationError(
|
|
280
|
+
operation="process",
|
|
281
|
+
file_path=str(template_file),
|
|
282
|
+
details=f"Failed to process template file: {str(e)}",
|
|
283
|
+
cause=e,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
logger.info(f"Installed {files_copied} files from {tier}")
|
|
287
|
+
return files_copied
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def install_additional_option(
|
|
291
|
+
template_id: str, option: str, project_root: Path, replacements: dict[str, str]
|
|
292
|
+
) -> int:
|
|
293
|
+
"""
|
|
294
|
+
Install additional option files (CI/CD, Docker, etc.).
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
template_id: Template identifier
|
|
298
|
+
option: Option name (e.g., "ci-cd", "docker")
|
|
299
|
+
project_root: Project root directory
|
|
300
|
+
replacements: Placeholder replacements
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
Number of files copied
|
|
304
|
+
|
|
305
|
+
Raises:
|
|
306
|
+
FileOperationError: If installation fails
|
|
307
|
+
"""
|
|
308
|
+
template_dir = get_template_directory(template_id)
|
|
309
|
+
option_dir = template_dir / option
|
|
310
|
+
|
|
311
|
+
if not option_dir.exists():
|
|
312
|
+
logger.warning(f"Option directory not found: {option_dir}")
|
|
313
|
+
return 0
|
|
314
|
+
|
|
315
|
+
logger.info(f"Installing {option} files...")
|
|
316
|
+
|
|
317
|
+
# Copy option files
|
|
318
|
+
files_copied = copy_directory_tree(
|
|
319
|
+
option_dir, project_root, skip_patterns=[".template", "__pycache__", ".pyc"]
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
# Process template files
|
|
323
|
+
for template_file in option_dir.rglob("*.template"):
|
|
324
|
+
relative_path = template_file.relative_to(option_dir)
|
|
325
|
+
output_path = project_root / relative_path.parent / relative_path.stem
|
|
326
|
+
|
|
327
|
+
try:
|
|
328
|
+
content = template_file.read_text()
|
|
329
|
+
processed_content = replace_placeholders(content, replacements)
|
|
330
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
331
|
+
output_path.write_text(processed_content)
|
|
332
|
+
logger.debug(f"Processed template: {template_file.name} -> {output_path.name}")
|
|
333
|
+
files_copied += 1
|
|
334
|
+
except Exception as e:
|
|
335
|
+
raise FileOperationError(
|
|
336
|
+
operation="process",
|
|
337
|
+
file_path=str(template_file),
|
|
338
|
+
details=f"Failed to process template file: {str(e)}",
|
|
339
|
+
cause=e,
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
logger.info(f"Installed {files_copied} files from {option}")
|
|
343
|
+
return files_copied
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def install_template(
|
|
347
|
+
template_id: str,
|
|
348
|
+
tier: str,
|
|
349
|
+
additional_options: list[str],
|
|
350
|
+
project_root: Path | None = None,
|
|
351
|
+
) -> dict[str, Any]:
|
|
352
|
+
"""
|
|
353
|
+
Install complete template with base + tier + options.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
template_id: Template identifier (e.g., "saas_t3")
|
|
357
|
+
tier: Quality tier (e.g., "tier-2-standard")
|
|
358
|
+
additional_options: List of option names (e.g., ["ci-cd", "docker"])
|
|
359
|
+
project_root: Project root directory (defaults to current directory)
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Installation summary dictionary with file counts and paths
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
TemplateNotFoundError: If template not found
|
|
366
|
+
FileOperationError: If installation fails
|
|
367
|
+
"""
|
|
368
|
+
if project_root is None:
|
|
369
|
+
project_root = Path.cwd()
|
|
370
|
+
|
|
371
|
+
# Get template info
|
|
372
|
+
template_info = get_template_info(template_id)
|
|
373
|
+
|
|
374
|
+
# Prepare placeholder replacements
|
|
375
|
+
project_name = project_root.name
|
|
376
|
+
replacements = {
|
|
377
|
+
"project_name": project_name,
|
|
378
|
+
"project_description": f"A {template_info['display_name']} project",
|
|
379
|
+
"template_id": template_id,
|
|
380
|
+
"template_name": template_info["display_name"],
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
total_files = 0
|
|
384
|
+
|
|
385
|
+
# Install base template
|
|
386
|
+
total_files += install_base_template(template_id, project_root, replacements)
|
|
387
|
+
|
|
388
|
+
# Install tier files (cumulative - install all tiers up to selected)
|
|
389
|
+
tier_order = [
|
|
390
|
+
"tier-1-essential",
|
|
391
|
+
"tier-2-standard",
|
|
392
|
+
"tier-3-comprehensive",
|
|
393
|
+
"tier-4-production",
|
|
394
|
+
]
|
|
395
|
+
selected_tier_index = tier_order.index(tier)
|
|
396
|
+
|
|
397
|
+
for i in range(selected_tier_index + 1):
|
|
398
|
+
tier_to_install = tier_order[i]
|
|
399
|
+
total_files += install_tier_files(template_id, tier_to_install, project_root, replacements)
|
|
400
|
+
|
|
401
|
+
# Install additional options
|
|
402
|
+
for option in additional_options:
|
|
403
|
+
# Map option keys to directory names
|
|
404
|
+
option_dir_map = {
|
|
405
|
+
"ci_cd": "ci-cd",
|
|
406
|
+
"docker": "docker",
|
|
407
|
+
"pre_commit": "pre-commit",
|
|
408
|
+
"env_templates": "env-templates",
|
|
409
|
+
}
|
|
410
|
+
option_dir = option_dir_map.get(option, option)
|
|
411
|
+
total_files += install_additional_option(
|
|
412
|
+
template_id, option_dir, project_root, replacements
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
logger.info(f"Template installation complete: {total_files} files installed")
|
|
416
|
+
|
|
417
|
+
return {
|
|
418
|
+
"template_id": template_id,
|
|
419
|
+
"template_name": template_info["display_name"],
|
|
420
|
+
"tier": tier,
|
|
421
|
+
"additional_options": additional_options,
|
|
422
|
+
"files_installed": total_files,
|
|
423
|
+
"project_root": str(project_root),
|
|
424
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Learning system for capturing and curating development insights."""
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Learning archiving module"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from solokit.core.constants import MAX_LEARNING_AGE_SESSIONS
|
|
11
|
+
from solokit.core.file_ops import load_json
|
|
12
|
+
from solokit.core.logging_config import get_logger
|
|
13
|
+
|
|
14
|
+
logger = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class LearningArchiver:
|
|
18
|
+
"""Handles archiving of old, unreferenced learnings"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, session_dir: Path, max_age_sessions: int = MAX_LEARNING_AGE_SESSIONS):
|
|
21
|
+
"""
|
|
22
|
+
Initialize archiver
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session_dir: Path to .session directory
|
|
26
|
+
max_age_sessions: Maximum age in sessions before archiving (default: 50)
|
|
27
|
+
"""
|
|
28
|
+
self.session_dir = session_dir
|
|
29
|
+
self.max_age_sessions = max_age_sessions
|
|
30
|
+
|
|
31
|
+
def archive_old_learnings(
|
|
32
|
+
self, learnings: dict[str, Any], max_age_sessions: int | None = None
|
|
33
|
+
) -> int:
|
|
34
|
+
"""
|
|
35
|
+
Archive old, unreferenced learnings
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
learnings: Learnings dict with 'categories' key
|
|
39
|
+
max_age_sessions: Override default max age (optional)
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
Number of learnings archived
|
|
43
|
+
"""
|
|
44
|
+
max_age = max_age_sessions if max_age_sessions is not None else self.max_age_sessions
|
|
45
|
+
archived_count = 0
|
|
46
|
+
categories = learnings.get("categories", {})
|
|
47
|
+
|
|
48
|
+
# Get current session number from tracking
|
|
49
|
+
current_session = self._get_current_session_number()
|
|
50
|
+
|
|
51
|
+
for category_name, category_learnings in categories.items():
|
|
52
|
+
to_archive = []
|
|
53
|
+
|
|
54
|
+
for i, learning in enumerate(category_learnings):
|
|
55
|
+
# Extract session number from learned_in field
|
|
56
|
+
session_num = self._extract_session_number(learning.get("learned_in", ""))
|
|
57
|
+
|
|
58
|
+
# Archive if too old
|
|
59
|
+
if session_num and current_session > 0 and current_session - session_num > max_age:
|
|
60
|
+
to_archive.append(i)
|
|
61
|
+
|
|
62
|
+
# Move to archive
|
|
63
|
+
archived = learnings.setdefault("archived", [])
|
|
64
|
+
for idx in sorted(to_archive, reverse=True):
|
|
65
|
+
learning = category_learnings.pop(idx)
|
|
66
|
+
learning["archived_from"] = category_name
|
|
67
|
+
learning["archived_at"] = datetime.now().isoformat()
|
|
68
|
+
archived.append(learning)
|
|
69
|
+
archived_count += 1
|
|
70
|
+
|
|
71
|
+
logger.info(f"Archived {archived_count} old learnings")
|
|
72
|
+
return archived_count
|
|
73
|
+
|
|
74
|
+
def _get_current_session_number(self) -> int:
|
|
75
|
+
"""
|
|
76
|
+
Get the current session number from work items
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Maximum session number found, or 0 if none found
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
work_items_path = self.session_dir / "tracking" / "work_items.json"
|
|
83
|
+
if work_items_path.exists():
|
|
84
|
+
data = load_json(work_items_path)
|
|
85
|
+
# Find max session number across all work items
|
|
86
|
+
max_session = 0
|
|
87
|
+
for item in data.get("work_items", {}).values():
|
|
88
|
+
sessions = item.get("sessions", [])
|
|
89
|
+
if sessions and isinstance(sessions, list):
|
|
90
|
+
max_session = max(max_session, max(sessions))
|
|
91
|
+
return max_session
|
|
92
|
+
except (ValueError, KeyError, TypeError) as e:
|
|
93
|
+
logger.warning(f"Failed to get current session number: {e}")
|
|
94
|
+
return 0
|
|
95
|
+
return 0
|
|
96
|
+
|
|
97
|
+
def _extract_session_number(self, session_id: str) -> int:
|
|
98
|
+
"""
|
|
99
|
+
Extract numeric session number from session ID
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
session_id: Session ID string (e.g., "session_001", "001", "1")
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
Extracted session number or 0 if extraction fails
|
|
106
|
+
"""
|
|
107
|
+
try:
|
|
108
|
+
# Handle formats like "session_001", "001", "1", etc.
|
|
109
|
+
match = re.search(r"\d+", session_id)
|
|
110
|
+
if match:
|
|
111
|
+
return int(match.group())
|
|
112
|
+
except (ValueError, AttributeError) as e:
|
|
113
|
+
logger.debug(f"Failed to extract session number from '{session_id}': {e}")
|
|
114
|
+
return 0
|
|
115
|
+
return 0
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Learning categorization module"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from solokit.core.logging_config import get_logger
|
|
8
|
+
|
|
9
|
+
logger = get_logger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class LearningCategorizer:
|
|
13
|
+
"""Handles automatic categorization of learnings based on content analysis"""
|
|
14
|
+
|
|
15
|
+
# Keyword sets for each category
|
|
16
|
+
ARCHITECTURE_KEYWORDS = [
|
|
17
|
+
"architecture",
|
|
18
|
+
"design",
|
|
19
|
+
"pattern",
|
|
20
|
+
"structure",
|
|
21
|
+
"component",
|
|
22
|
+
"module",
|
|
23
|
+
"layer",
|
|
24
|
+
"service",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
GOTCHA_KEYWORDS = [
|
|
28
|
+
"gotcha",
|
|
29
|
+
"trap",
|
|
30
|
+
"pitfall",
|
|
31
|
+
"mistake",
|
|
32
|
+
"error",
|
|
33
|
+
"bug",
|
|
34
|
+
"issue",
|
|
35
|
+
"problem",
|
|
36
|
+
"challenge",
|
|
37
|
+
"warning",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
PRACTICE_KEYWORDS = [
|
|
41
|
+
"best practice",
|
|
42
|
+
"convention",
|
|
43
|
+
"standard",
|
|
44
|
+
"guideline",
|
|
45
|
+
"recommendation",
|
|
46
|
+
"should",
|
|
47
|
+
"always",
|
|
48
|
+
"never",
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
DEBT_KEYWORDS = [
|
|
52
|
+
"technical debt",
|
|
53
|
+
"refactor",
|
|
54
|
+
"cleanup",
|
|
55
|
+
"legacy",
|
|
56
|
+
"deprecated",
|
|
57
|
+
"workaround",
|
|
58
|
+
"hack",
|
|
59
|
+
"todo",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
PERFORMANCE_KEYWORDS = [
|
|
63
|
+
"performance",
|
|
64
|
+
"optimization",
|
|
65
|
+
"speed",
|
|
66
|
+
"slow",
|
|
67
|
+
"fast",
|
|
68
|
+
"efficient",
|
|
69
|
+
"memory",
|
|
70
|
+
"cpu",
|
|
71
|
+
"benchmark",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
def categorize_learning(self, learning: dict[str, Any]) -> str:
|
|
75
|
+
"""
|
|
76
|
+
Automatically categorize a single learning based on content analysis
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
learning: Learning dict with 'content' field
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
Category name string
|
|
83
|
+
"""
|
|
84
|
+
content = learning.get("content", "").lower()
|
|
85
|
+
|
|
86
|
+
# Check for suggested type first
|
|
87
|
+
if "suggested_type" in learning:
|
|
88
|
+
suggested = learning["suggested_type"]
|
|
89
|
+
if suggested in [
|
|
90
|
+
"architecture_pattern",
|
|
91
|
+
"gotcha",
|
|
92
|
+
"best_practice",
|
|
93
|
+
"technical_debt",
|
|
94
|
+
"performance_insight",
|
|
95
|
+
]:
|
|
96
|
+
return f"{suggested}s" # Pluralize
|
|
97
|
+
|
|
98
|
+
# Score each category based on keywords
|
|
99
|
+
scores = {
|
|
100
|
+
"architecture_patterns": self._keyword_score(content, self.ARCHITECTURE_KEYWORDS),
|
|
101
|
+
"gotchas": self._keyword_score(content, self.GOTCHA_KEYWORDS),
|
|
102
|
+
"best_practices": self._keyword_score(content, self.PRACTICE_KEYWORDS),
|
|
103
|
+
"technical_debt": self._keyword_score(content, self.DEBT_KEYWORDS),
|
|
104
|
+
"performance_insights": self._keyword_score(content, self.PERFORMANCE_KEYWORDS),
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
# Return category with highest score, default to best_practices
|
|
108
|
+
max_category = max(scores.items(), key=lambda x: x[1])
|
|
109
|
+
return max_category[0] if max_category[1] > 0 else "best_practices"
|
|
110
|
+
|
|
111
|
+
def _keyword_score(self, text: str, keywords: list[str]) -> int:
|
|
112
|
+
"""
|
|
113
|
+
Calculate keyword match score for text
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
text: Text to analyze (should be lowercased)
|
|
117
|
+
keywords: List of keywords to search for
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
Number of keyword matches found
|
|
121
|
+
"""
|
|
122
|
+
score = 0
|
|
123
|
+
for keyword in keywords:
|
|
124
|
+
if keyword in text:
|
|
125
|
+
score += 1
|
|
126
|
+
return score
|