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,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
|