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