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
solokit/core/config.py
ADDED
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
"""Centralized configuration management with caching and validation.
|
|
2
|
+
|
|
3
|
+
This module provides a singleton ConfigManager that loads, validates, and caches
|
|
4
|
+
configuration from .session/config.json. It replaces the duplicated configuration
|
|
5
|
+
loading logic scattered across multiple modules.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import logging
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from solokit.core.exceptions import (
|
|
15
|
+
ConfigurationError,
|
|
16
|
+
ConfigValidationError,
|
|
17
|
+
ErrorCode,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ExecutionConfig:
|
|
25
|
+
"""Test execution configuration."""
|
|
26
|
+
|
|
27
|
+
enabled: bool = True
|
|
28
|
+
required: bool = True
|
|
29
|
+
coverage_threshold: int = 80
|
|
30
|
+
commands: dict[str, str] = field(
|
|
31
|
+
default_factory=lambda: {
|
|
32
|
+
"python": "pytest --cov=src/solokit --cov-report=json",
|
|
33
|
+
"javascript": "npm test -- --coverage",
|
|
34
|
+
"typescript": "npm test -- --coverage",
|
|
35
|
+
}
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class LintingConfig:
|
|
41
|
+
"""Linting configuration."""
|
|
42
|
+
|
|
43
|
+
enabled: bool = True
|
|
44
|
+
required: bool = False
|
|
45
|
+
auto_fix: bool = True
|
|
46
|
+
commands: dict[str, str] = field(
|
|
47
|
+
default_factory=lambda: {
|
|
48
|
+
"python": "ruff check .",
|
|
49
|
+
"javascript": "npx eslint .",
|
|
50
|
+
"typescript": "npx eslint .",
|
|
51
|
+
}
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class FormattingConfig:
|
|
57
|
+
"""Formatting configuration."""
|
|
58
|
+
|
|
59
|
+
enabled: bool = True
|
|
60
|
+
required: bool = False
|
|
61
|
+
auto_fix: bool = True
|
|
62
|
+
commands: dict[str, str] = field(
|
|
63
|
+
default_factory=lambda: {
|
|
64
|
+
"python": "ruff format .",
|
|
65
|
+
"javascript": "npx prettier .",
|
|
66
|
+
"typescript": "npx prettier .",
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class SecurityConfig:
|
|
73
|
+
"""Security scanning configuration."""
|
|
74
|
+
|
|
75
|
+
enabled: bool = True
|
|
76
|
+
required: bool = True
|
|
77
|
+
fail_on: str = "high" # critical, high, medium, low
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class DocumentationConfig:
|
|
82
|
+
"""Documentation validation configuration."""
|
|
83
|
+
|
|
84
|
+
enabled: bool = True
|
|
85
|
+
required: bool = False
|
|
86
|
+
check_changelog: bool = True
|
|
87
|
+
check_docstrings: bool = True
|
|
88
|
+
check_readme: bool = False
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class SpecCompletenessConfig:
|
|
93
|
+
"""Spec completeness validation configuration."""
|
|
94
|
+
|
|
95
|
+
enabled: bool = True
|
|
96
|
+
required: bool = True
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class Context7Config:
|
|
101
|
+
"""Context7 library verification configuration."""
|
|
102
|
+
|
|
103
|
+
enabled: bool = False
|
|
104
|
+
important_libraries: list[str] = field(default_factory=list)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@dataclass
|
|
108
|
+
class IntegrationConfig:
|
|
109
|
+
"""Integration test validation configuration."""
|
|
110
|
+
|
|
111
|
+
enabled: bool = True
|
|
112
|
+
documentation: dict[str, bool] = field(
|
|
113
|
+
default_factory=lambda: {
|
|
114
|
+
"enabled": True,
|
|
115
|
+
"architecture_diagrams": True,
|
|
116
|
+
"sequence_diagrams": True,
|
|
117
|
+
"contract_documentation": True,
|
|
118
|
+
"performance_baseline_docs": True,
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass
|
|
124
|
+
class DeploymentConfig:
|
|
125
|
+
"""Deployment quality gates configuration."""
|
|
126
|
+
|
|
127
|
+
enabled: bool = True
|
|
128
|
+
integration_tests: dict[str, bool] = field(default_factory=lambda: {"enabled": True})
|
|
129
|
+
security_scans: dict[str, bool] = field(default_factory=lambda: {"enabled": True})
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
@dataclass
|
|
133
|
+
class QualityGatesConfig:
|
|
134
|
+
"""Quality gates configuration."""
|
|
135
|
+
|
|
136
|
+
test_execution: ExecutionConfig = field(default_factory=ExecutionConfig)
|
|
137
|
+
linting: LintingConfig = field(default_factory=LintingConfig)
|
|
138
|
+
formatting: FormattingConfig = field(default_factory=FormattingConfig)
|
|
139
|
+
security: SecurityConfig = field(default_factory=SecurityConfig)
|
|
140
|
+
documentation: DocumentationConfig = field(default_factory=DocumentationConfig)
|
|
141
|
+
spec_completeness: SpecCompletenessConfig = field(default_factory=SpecCompletenessConfig)
|
|
142
|
+
context7: Context7Config = field(default_factory=Context7Config)
|
|
143
|
+
integration: IntegrationConfig = field(default_factory=IntegrationConfig)
|
|
144
|
+
deployment: DeploymentConfig = field(default_factory=DeploymentConfig)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class GitWorkflowConfig:
|
|
149
|
+
"""Git workflow configuration."""
|
|
150
|
+
|
|
151
|
+
mode: str = "pr"
|
|
152
|
+
auto_push: bool = True
|
|
153
|
+
auto_create_pr: bool = True
|
|
154
|
+
delete_branch_after_merge: bool = True
|
|
155
|
+
pr_title_template: str = "{type}: {title}"
|
|
156
|
+
pr_body_template: str = "## Work Item: {work_item_id}\n\n{description}\n\n🤖 Generated with [Claude Code](https://claude.com/claude-code)"
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@dataclass
|
|
160
|
+
class CurationConfig:
|
|
161
|
+
"""Learning curation configuration."""
|
|
162
|
+
|
|
163
|
+
auto_curate: bool = False
|
|
164
|
+
frequency: int = 5
|
|
165
|
+
dry_run: bool = False
|
|
166
|
+
similarity_threshold: float = 0.7
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class SolokitConfig:
|
|
171
|
+
"""Main Solokit configuration."""
|
|
172
|
+
|
|
173
|
+
quality_gates: QualityGatesConfig = field(default_factory=QualityGatesConfig)
|
|
174
|
+
git_workflow: GitWorkflowConfig = field(default_factory=GitWorkflowConfig)
|
|
175
|
+
curation: CurationConfig = field(default_factory=CurationConfig)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class ConfigManager:
|
|
179
|
+
"""Centralized configuration management with caching and validation.
|
|
180
|
+
|
|
181
|
+
This class implements the Singleton pattern to ensure a single source of truth
|
|
182
|
+
for configuration across the entire application. It loads configuration from
|
|
183
|
+
.session/config.json, validates it, and caches it for performance.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> config_mgr = get_config_manager()
|
|
187
|
+
>>> config_mgr.load_config(Path(".session/config.json"))
|
|
188
|
+
>>> quality_config = config_mgr.quality_gates
|
|
189
|
+
>>> print(quality_config.test_execution.coverage_threshold)
|
|
190
|
+
80
|
|
191
|
+
"""
|
|
192
|
+
|
|
193
|
+
_instance: Optional["ConfigManager"] = None
|
|
194
|
+
_config: Optional[SolokitConfig] = None
|
|
195
|
+
_config_path: Optional[Path] = None
|
|
196
|
+
|
|
197
|
+
def __new__(cls) -> "ConfigManager":
|
|
198
|
+
"""Ensure only one instance of ConfigManager exists."""
|
|
199
|
+
if cls._instance is None:
|
|
200
|
+
cls._instance = super().__new__(cls)
|
|
201
|
+
return cls._instance
|
|
202
|
+
|
|
203
|
+
def __init__(self) -> None:
|
|
204
|
+
"""Initialize with default configuration if not already initialized."""
|
|
205
|
+
if self._config is None:
|
|
206
|
+
self._config = SolokitConfig()
|
|
207
|
+
|
|
208
|
+
def load_config(self, config_path: Path, force_reload: bool = False) -> None:
|
|
209
|
+
"""Load configuration from file.
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
config_path: Path to config.json file
|
|
213
|
+
force_reload: Force reload even if already cached
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
ConfigurationError: If JSON is invalid or file cannot be read
|
|
217
|
+
ConfigValidationError: If configuration structure is invalid
|
|
218
|
+
|
|
219
|
+
Note:
|
|
220
|
+
If the config file doesn't exist, default configuration is used.
|
|
221
|
+
"""
|
|
222
|
+
# Skip if already loaded and not forcing reload
|
|
223
|
+
if not force_reload and self._config_path == config_path:
|
|
224
|
+
logger.debug("Config already loaded from %s, using cache", config_path)
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
self._config_path = config_path
|
|
228
|
+
|
|
229
|
+
if not config_path.exists():
|
|
230
|
+
logger.info("Config file not found at %s, using defaults", config_path)
|
|
231
|
+
self._config = SolokitConfig()
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
try:
|
|
235
|
+
with open(config_path, encoding="utf-8") as f:
|
|
236
|
+
data = json.load(f)
|
|
237
|
+
|
|
238
|
+
# Parse config sections with defaults
|
|
239
|
+
quality_gates_dict = data.get("quality_gates", {})
|
|
240
|
+
|
|
241
|
+
# Handle integration_tests at top level (for backward compatibility)
|
|
242
|
+
# Merge it into quality_gates if it exists
|
|
243
|
+
if "integration_tests" in data:
|
|
244
|
+
if "integration" not in quality_gates_dict:
|
|
245
|
+
quality_gates_dict["integration"] = data["integration_tests"]
|
|
246
|
+
|
|
247
|
+
quality_gates_data = self._parse_quality_gates(quality_gates_dict)
|
|
248
|
+
git_workflow_data = data.get("git_workflow", {})
|
|
249
|
+
curation_data = data.get("curation", {})
|
|
250
|
+
|
|
251
|
+
# Filter curation_data to only include valid CurationConfig fields
|
|
252
|
+
# This maintains backward compatibility with old configs
|
|
253
|
+
valid_curation_fields = {
|
|
254
|
+
"auto_curate",
|
|
255
|
+
"frequency",
|
|
256
|
+
"dry_run",
|
|
257
|
+
"similarity_threshold",
|
|
258
|
+
}
|
|
259
|
+
filtered_curation_data = (
|
|
260
|
+
{k: v for k, v in curation_data.items() if k in valid_curation_fields}
|
|
261
|
+
if curation_data
|
|
262
|
+
else {}
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Create config with parsed data
|
|
266
|
+
self._config = SolokitConfig(
|
|
267
|
+
quality_gates=quality_gates_data,
|
|
268
|
+
git_workflow=(
|
|
269
|
+
GitWorkflowConfig(**git_workflow_data)
|
|
270
|
+
if git_workflow_data
|
|
271
|
+
else GitWorkflowConfig()
|
|
272
|
+
),
|
|
273
|
+
curation=(
|
|
274
|
+
CurationConfig(**filtered_curation_data)
|
|
275
|
+
if filtered_curation_data
|
|
276
|
+
else CurationConfig()
|
|
277
|
+
),
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
logger.info("Loaded configuration from %s", config_path)
|
|
281
|
+
|
|
282
|
+
except json.JSONDecodeError as e:
|
|
283
|
+
raise ConfigurationError(
|
|
284
|
+
message=f"Invalid JSON in configuration file: {config_path}",
|
|
285
|
+
code=ErrorCode.INVALID_JSON,
|
|
286
|
+
context={"config_path": str(config_path), "error": str(e)},
|
|
287
|
+
remediation="Check the JSON syntax in your config file. Ensure all quotes, brackets, and commas are properly placed.",
|
|
288
|
+
cause=e,
|
|
289
|
+
)
|
|
290
|
+
except TypeError as e:
|
|
291
|
+
raise ConfigValidationError(
|
|
292
|
+
config_path=str(config_path),
|
|
293
|
+
errors=[f"Invalid configuration structure: {str(e)}"],
|
|
294
|
+
)
|
|
295
|
+
except PermissionError as e:
|
|
296
|
+
raise ConfigurationError(
|
|
297
|
+
message=f"Permission denied reading configuration file: {config_path}",
|
|
298
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
299
|
+
context={"config_path": str(config_path)},
|
|
300
|
+
remediation="Check file permissions and ensure you have read access to the config file.",
|
|
301
|
+
cause=e,
|
|
302
|
+
)
|
|
303
|
+
except OSError as e:
|
|
304
|
+
raise ConfigurationError(
|
|
305
|
+
message=f"Error reading configuration file: {config_path}",
|
|
306
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
307
|
+
context={"config_path": str(config_path), "error": str(e)},
|
|
308
|
+
remediation="Ensure the config file is not corrupted and the file system is accessible.",
|
|
309
|
+
cause=e,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
def _parse_quality_gates(self, data: dict) -> QualityGatesConfig:
|
|
313
|
+
"""Parse quality gates configuration with nested structures.
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
data: Raw quality gates configuration dict
|
|
317
|
+
|
|
318
|
+
Returns:
|
|
319
|
+
Validated QualityGatesConfig with defaults for missing values
|
|
320
|
+
|
|
321
|
+
Raises:
|
|
322
|
+
ConfigValidationError: If quality_gates structure is invalid
|
|
323
|
+
"""
|
|
324
|
+
try:
|
|
325
|
+
# Parse nested configs with field filtering
|
|
326
|
+
# Helper to filter only valid fields for a dataclass
|
|
327
|
+
def filter_fields(data: dict, config_class: type) -> dict:
|
|
328
|
+
"""Filter dict to only include fields that exist in the dataclass."""
|
|
329
|
+
if not data:
|
|
330
|
+
return {}
|
|
331
|
+
import dataclasses
|
|
332
|
+
|
|
333
|
+
valid_fields = {f.name for f in dataclasses.fields(config_class)}
|
|
334
|
+
return {k: v for k, v in data.items() if k in valid_fields}
|
|
335
|
+
|
|
336
|
+
test_exec_data = filter_fields(data.get("test_execution", {}), ExecutionConfig)
|
|
337
|
+
linting_data = filter_fields(data.get("linting", {}), LintingConfig)
|
|
338
|
+
formatting_data = filter_fields(data.get("formatting", {}), FormattingConfig)
|
|
339
|
+
security_data = filter_fields(data.get("security", {}), SecurityConfig)
|
|
340
|
+
documentation_data = filter_fields(data.get("documentation", {}), DocumentationConfig)
|
|
341
|
+
spec_completeness_data = filter_fields(
|
|
342
|
+
data.get("spec_completeness", {}), SpecCompletenessConfig
|
|
343
|
+
)
|
|
344
|
+
context7_data = filter_fields(data.get("context7", {}), Context7Config)
|
|
345
|
+
integration_data = filter_fields(data.get("integration", {}), IntegrationConfig)
|
|
346
|
+
deployment_data = filter_fields(data.get("deployment", {}), DeploymentConfig)
|
|
347
|
+
|
|
348
|
+
return QualityGatesConfig(
|
|
349
|
+
test_execution=(
|
|
350
|
+
ExecutionConfig(**test_exec_data) if test_exec_data else ExecutionConfig()
|
|
351
|
+
),
|
|
352
|
+
linting=(LintingConfig(**linting_data) if linting_data else LintingConfig()),
|
|
353
|
+
formatting=(
|
|
354
|
+
FormattingConfig(**formatting_data) if formatting_data else FormattingConfig()
|
|
355
|
+
),
|
|
356
|
+
security=(SecurityConfig(**security_data) if security_data else SecurityConfig()),
|
|
357
|
+
documentation=(
|
|
358
|
+
DocumentationConfig(**documentation_data)
|
|
359
|
+
if documentation_data
|
|
360
|
+
else DocumentationConfig()
|
|
361
|
+
),
|
|
362
|
+
spec_completeness=(
|
|
363
|
+
SpecCompletenessConfig(**spec_completeness_data)
|
|
364
|
+
if spec_completeness_data
|
|
365
|
+
else SpecCompletenessConfig()
|
|
366
|
+
),
|
|
367
|
+
context7=(Context7Config(**context7_data) if context7_data else Context7Config()),
|
|
368
|
+
integration=(
|
|
369
|
+
IntegrationConfig(**integration_data)
|
|
370
|
+
if integration_data
|
|
371
|
+
else IntegrationConfig()
|
|
372
|
+
),
|
|
373
|
+
deployment=(
|
|
374
|
+
DeploymentConfig(**deployment_data) if deployment_data else DeploymentConfig()
|
|
375
|
+
),
|
|
376
|
+
)
|
|
377
|
+
except TypeError as e:
|
|
378
|
+
# Collect validation errors
|
|
379
|
+
errors = [f"Invalid quality_gates structure: {str(e)}"]
|
|
380
|
+
raise ConfigValidationError(
|
|
381
|
+
config_path=str(self._config_path) if self._config_path else "unknown",
|
|
382
|
+
errors=errors,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def quality_gates(self) -> QualityGatesConfig:
|
|
387
|
+
"""Get quality gates configuration.
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
Quality gates configuration with all sub-configurations
|
|
391
|
+
"""
|
|
392
|
+
assert self._config is not None, "Config not initialized"
|
|
393
|
+
return self._config.quality_gates
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def git_workflow(self) -> GitWorkflowConfig:
|
|
397
|
+
"""Get git workflow configuration.
|
|
398
|
+
|
|
399
|
+
Returns:
|
|
400
|
+
Git workflow configuration
|
|
401
|
+
"""
|
|
402
|
+
assert self._config is not None, "Config not initialized"
|
|
403
|
+
return self._config.git_workflow
|
|
404
|
+
|
|
405
|
+
@property
|
|
406
|
+
def curation(self) -> CurationConfig:
|
|
407
|
+
"""Get curation configuration.
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Learning curation configuration
|
|
411
|
+
"""
|
|
412
|
+
assert self._config is not None, "Config not initialized"
|
|
413
|
+
return self._config.curation
|
|
414
|
+
|
|
415
|
+
def get_config(self) -> SolokitConfig:
|
|
416
|
+
"""Get full configuration.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
Complete Solokit configuration
|
|
420
|
+
"""
|
|
421
|
+
assert self._config is not None, "Config not initialized"
|
|
422
|
+
return self._config
|
|
423
|
+
|
|
424
|
+
def invalidate_cache(self) -> None:
|
|
425
|
+
"""Invalidate cached configuration.
|
|
426
|
+
|
|
427
|
+
Forces next load_config() call to reload from disk.
|
|
428
|
+
Useful for testing and when config file changes.
|
|
429
|
+
"""
|
|
430
|
+
self._config_path = None
|
|
431
|
+
logger.debug("Configuration cache invalidated")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
# Global instance
|
|
435
|
+
_config_manager: Optional[ConfigManager] = None
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
def get_config_manager() -> ConfigManager:
|
|
439
|
+
"""Get global ConfigManager instance.
|
|
440
|
+
|
|
441
|
+
Returns:
|
|
442
|
+
Singleton ConfigManager instance
|
|
443
|
+
|
|
444
|
+
Example:
|
|
445
|
+
>>> config = get_config_manager()
|
|
446
|
+
>>> config.load_config(Path(".session/config.json"))
|
|
447
|
+
>>> print(config.quality_gates.test_execution.enabled)
|
|
448
|
+
True
|
|
449
|
+
"""
|
|
450
|
+
global _config_manager
|
|
451
|
+
if _config_manager is None:
|
|
452
|
+
_config_manager = ConfigManager()
|
|
453
|
+
return _config_manager
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Configuration validation using JSON schema."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from solokit.core.error_handlers import log_errors
|
|
10
|
+
from solokit.core.exceptions import (
|
|
11
|
+
ConfigurationError,
|
|
12
|
+
ConfigValidationError,
|
|
13
|
+
ErrorCode,
|
|
14
|
+
ValidationError,
|
|
15
|
+
)
|
|
16
|
+
from solokit.core.exceptions import (
|
|
17
|
+
FileNotFoundError as SolokitFileNotFoundError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@log_errors()
|
|
24
|
+
def validate_config(config_path: Path, schema_path: Path) -> dict[str, Any]:
|
|
25
|
+
"""
|
|
26
|
+
Validate configuration against JSON schema.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
config_path: Path to config.json
|
|
30
|
+
schema_path: Path to config.schema.json
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
Validated configuration dictionary
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
FileNotFoundError: If config file doesn't exist
|
|
37
|
+
ValidationError: If config JSON is malformed
|
|
38
|
+
ConfigurationError: If schema is invalid or malformed
|
|
39
|
+
ConfigValidationError: If config fails schema validation
|
|
40
|
+
"""
|
|
41
|
+
try:
|
|
42
|
+
import jsonschema
|
|
43
|
+
except ImportError:
|
|
44
|
+
# If jsonschema not installed, skip validation but warn
|
|
45
|
+
logger.warning("jsonschema not installed, skipping validation")
|
|
46
|
+
# Still try to load and return config
|
|
47
|
+
if not config_path.exists():
|
|
48
|
+
raise SolokitFileNotFoundError(str(config_path), file_type="config")
|
|
49
|
+
try:
|
|
50
|
+
with open(config_path) as f:
|
|
51
|
+
config: dict[str, Any] = json.load(f)
|
|
52
|
+
return config
|
|
53
|
+
except json.JSONDecodeError as e:
|
|
54
|
+
raise ValidationError(
|
|
55
|
+
message=f"Invalid JSON in config file: {config_path}",
|
|
56
|
+
code=ErrorCode.INVALID_JSON,
|
|
57
|
+
context={"file_path": str(config_path), "error": str(e)},
|
|
58
|
+
remediation="Fix JSON syntax errors in config file",
|
|
59
|
+
cause=e,
|
|
60
|
+
) from e
|
|
61
|
+
|
|
62
|
+
# Load config
|
|
63
|
+
if not config_path.exists():
|
|
64
|
+
raise SolokitFileNotFoundError(str(config_path), file_type="config")
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
with open(config_path) as f:
|
|
68
|
+
config = json.load(f)
|
|
69
|
+
except json.JSONDecodeError as e:
|
|
70
|
+
raise ValidationError(
|
|
71
|
+
message=f"Invalid JSON in config file: {config_path}",
|
|
72
|
+
code=ErrorCode.INVALID_JSON,
|
|
73
|
+
context={"file_path": str(config_path), "error": str(e)},
|
|
74
|
+
remediation="Fix JSON syntax errors in config file",
|
|
75
|
+
cause=e,
|
|
76
|
+
) from e
|
|
77
|
+
|
|
78
|
+
# Load schema
|
|
79
|
+
if not schema_path.exists():
|
|
80
|
+
# Schema missing is a warning, not an error - allow validation to be skipped
|
|
81
|
+
logger.warning(f"Schema file not found: {schema_path}, skipping validation")
|
|
82
|
+
config_dict: dict[str, Any] = config
|
|
83
|
+
return config_dict
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(schema_path) as f:
|
|
87
|
+
schema = json.load(f)
|
|
88
|
+
except json.JSONDecodeError as e:
|
|
89
|
+
raise ConfigurationError(
|
|
90
|
+
message=f"Invalid JSON in schema file: {schema_path}",
|
|
91
|
+
code=ErrorCode.INVALID_CONFIG_VALUE,
|
|
92
|
+
context={"file_path": str(schema_path), "error": str(e)},
|
|
93
|
+
remediation="Fix JSON syntax errors in schema file",
|
|
94
|
+
cause=e,
|
|
95
|
+
) from e
|
|
96
|
+
|
|
97
|
+
# Validate
|
|
98
|
+
try:
|
|
99
|
+
jsonschema.validate(instance=config, schema=schema)
|
|
100
|
+
validated_config: dict[str, Any] = config
|
|
101
|
+
return validated_config
|
|
102
|
+
except jsonschema.ValidationError as e:
|
|
103
|
+
error_msg = _format_validation_error(e)
|
|
104
|
+
raise ConfigValidationError(config_path=str(config_path), errors=[error_msg]) from e
|
|
105
|
+
except jsonschema.SchemaError as e:
|
|
106
|
+
raise ConfigurationError(
|
|
107
|
+
message=f"Invalid schema structure: {schema_path}",
|
|
108
|
+
code=ErrorCode.INVALID_CONFIG_VALUE,
|
|
109
|
+
context={"file_path": str(schema_path), "error": e.message},
|
|
110
|
+
remediation="Fix schema structure errors in schema file",
|
|
111
|
+
cause=e,
|
|
112
|
+
) from e
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _format_validation_error(error: Any) -> str:
|
|
116
|
+
"""Format validation error for user-friendly display."""
|
|
117
|
+
path = " -> ".join(str(p) for p in error.path) if error.path else "root"
|
|
118
|
+
return f"Validation error at '{path}': {error.message}"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@log_errors()
|
|
122
|
+
def load_and_validate_config(config_path: Path, schema_path: Path) -> dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Load and validate configuration.
|
|
125
|
+
|
|
126
|
+
This is a convenience function that wraps validate_config.
|
|
127
|
+
Use validate_config directly for better error handling.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
config_path: Path to config.json
|
|
131
|
+
schema_path: Path to config.schema.json
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Loaded and validated configuration
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
FileNotFoundError: If config file doesn't exist
|
|
138
|
+
ValidationError: If config JSON is malformed
|
|
139
|
+
ConfigurationError: If schema is invalid or malformed
|
|
140
|
+
ConfigValidationError: If config fails schema validation
|
|
141
|
+
"""
|
|
142
|
+
# validate_config now loads and validates in one step
|
|
143
|
+
return validate_config(config_path, schema_path)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main() -> None:
|
|
147
|
+
"""
|
|
148
|
+
CLI entry point for manual validation.
|
|
149
|
+
|
|
150
|
+
Exits with 0 on success, non-zero on error.
|
|
151
|
+
|
|
152
|
+
Raises:
|
|
153
|
+
SystemExit: Always exits with status code
|
|
154
|
+
"""
|
|
155
|
+
import sys
|
|
156
|
+
|
|
157
|
+
from solokit.core.exceptions import SolokitError
|
|
158
|
+
from solokit.core.output import get_output
|
|
159
|
+
|
|
160
|
+
output = get_output()
|
|
161
|
+
|
|
162
|
+
if len(sys.argv) < 2:
|
|
163
|
+
output.info("Usage: config_validator.py <config_path> [schema_path]")
|
|
164
|
+
output.info("\nValidate Solokit configuration against JSON schema.")
|
|
165
|
+
output.info("\nExample:")
|
|
166
|
+
output.info(" python3 config_validator.py .session/config.json")
|
|
167
|
+
output.info(
|
|
168
|
+
" python3 config_validator.py .session/config.json .session/config.schema.json"
|
|
169
|
+
)
|
|
170
|
+
sys.exit(1)
|
|
171
|
+
|
|
172
|
+
config_path = Path(sys.argv[1])
|
|
173
|
+
|
|
174
|
+
# Default schema path
|
|
175
|
+
if len(sys.argv) >= 3:
|
|
176
|
+
schema_path = Path(sys.argv[2])
|
|
177
|
+
else:
|
|
178
|
+
# Assume schema is in same directory as config
|
|
179
|
+
schema_path = config_path.parent / "config.schema.json"
|
|
180
|
+
|
|
181
|
+
output.info(f"Validating: {config_path}")
|
|
182
|
+
output.info(f"Against schema: {schema_path}\n")
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
validate_config(config_path, schema_path)
|
|
186
|
+
output.success("Configuration is valid!")
|
|
187
|
+
sys.exit(0)
|
|
188
|
+
except SolokitError as e:
|
|
189
|
+
output.info("✗ Configuration validation failed!\n")
|
|
190
|
+
output.info(f"Error: {e.message}")
|
|
191
|
+
if e.context:
|
|
192
|
+
output.info(f"Context: {e.context}")
|
|
193
|
+
if e.remediation:
|
|
194
|
+
output.info(f"\nRemediation: {e.remediation}")
|
|
195
|
+
output.info("\nSee docs/configuration.md for configuration reference.")
|
|
196
|
+
sys.exit(e.exit_code)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
output.info("✗ Unexpected error during validation!\n")
|
|
199
|
+
output.info(f"Error: {e}")
|
|
200
|
+
sys.exit(1)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
if __name__ == "__main__":
|
|
204
|
+
main()
|