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,1567 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive exception hierarchy for Solokit.
|
|
3
|
+
|
|
4
|
+
Provides structured exceptions with error codes, categories, and context.
|
|
5
|
+
All business logic should raise these exceptions rather than returning error tuples.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from solokit.core.exceptions import WorkItemNotFoundError
|
|
9
|
+
|
|
10
|
+
def get_work_item(item_id: str) -> WorkItem:
|
|
11
|
+
if item_id not in work_items:
|
|
12
|
+
raise WorkItemNotFoundError(item_id)
|
|
13
|
+
return work_items[item_id]
|
|
14
|
+
|
|
15
|
+
# CLI layer catches and formats
|
|
16
|
+
try:
|
|
17
|
+
item = get_work_item("invalid")
|
|
18
|
+
except WorkItemNotFoundError as e:
|
|
19
|
+
output.info(f"Error: {e}")
|
|
20
|
+
output.info(f"Remediation: {e.remediation}")
|
|
21
|
+
sys.exit(e.exit_code)
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
from enum import Enum
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
from solokit.core.logging_config import get_logger
|
|
30
|
+
from solokit.core.output import get_output
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
output = get_output()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ErrorCategory(Enum):
|
|
37
|
+
"""Error categories for classification and handling"""
|
|
38
|
+
|
|
39
|
+
VALIDATION = "validation"
|
|
40
|
+
NOT_FOUND = "not_found"
|
|
41
|
+
ALREADY_EXISTS = "already_exists"
|
|
42
|
+
CONFIGURATION = "configuration"
|
|
43
|
+
SYSTEM = "system"
|
|
44
|
+
GIT = "git"
|
|
45
|
+
DEPENDENCY = "dependency"
|
|
46
|
+
SECURITY = "security"
|
|
47
|
+
TIMEOUT = "timeout"
|
|
48
|
+
PERMISSION = "permission"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ErrorCode(Enum):
|
|
52
|
+
"""Specific error codes for programmatic handling"""
|
|
53
|
+
|
|
54
|
+
# Validation errors (1000-1999)
|
|
55
|
+
INVALID_WORK_ITEM_ID = 1001
|
|
56
|
+
INVALID_WORK_ITEM_TYPE = 1002
|
|
57
|
+
MISSING_REQUIRED_FIELD = 1003
|
|
58
|
+
INVALID_JSON = 1004
|
|
59
|
+
SPEC_VALIDATION_FAILED = 1005
|
|
60
|
+
INVALID_STATUS = 1006
|
|
61
|
+
INVALID_PRIORITY = 1007
|
|
62
|
+
INVALID_COMMAND = 1008
|
|
63
|
+
PROJECT_NOT_BLANK = 1009
|
|
64
|
+
INVALID_CONFIGURATION = 1010
|
|
65
|
+
|
|
66
|
+
# Not found errors (2000-2999)
|
|
67
|
+
WORK_ITEM_NOT_FOUND = 2001
|
|
68
|
+
FILE_NOT_FOUND = 2002
|
|
69
|
+
SESSION_NOT_FOUND = 2003
|
|
70
|
+
LEARNING_NOT_FOUND = 2004
|
|
71
|
+
CONFIG_NOT_FOUND = 2005
|
|
72
|
+
|
|
73
|
+
# Already exists errors (3000-3999)
|
|
74
|
+
WORK_ITEM_ALREADY_EXISTS = 3001
|
|
75
|
+
FILE_ALREADY_EXISTS = 3002
|
|
76
|
+
SESSION_ALREADY_ACTIVE = 3003
|
|
77
|
+
|
|
78
|
+
# Configuration errors (4000-4999)
|
|
79
|
+
CONFIG_FILE_MISSING = 4001
|
|
80
|
+
CONFIG_VALIDATION_FAILED = 4002
|
|
81
|
+
SCHEMA_MISSING = 4003
|
|
82
|
+
INVALID_CONFIG_VALUE = 4004
|
|
83
|
+
|
|
84
|
+
# System errors (5000-5999)
|
|
85
|
+
FILE_OPERATION_FAILED = 5001
|
|
86
|
+
SUBPROCESS_FAILED = 5002
|
|
87
|
+
IMPORT_FAILED = 5003
|
|
88
|
+
COMMAND_FAILED = 5004
|
|
89
|
+
MODULE_NOT_FOUND = 5005
|
|
90
|
+
FUNCTION_NOT_FOUND = 5006
|
|
91
|
+
|
|
92
|
+
# Git errors (6000-6999)
|
|
93
|
+
NOT_A_GIT_REPO = 6001
|
|
94
|
+
GIT_NOT_FOUND = 6002
|
|
95
|
+
WORKING_DIR_NOT_CLEAN = 6003
|
|
96
|
+
GIT_COMMAND_FAILED = 6004
|
|
97
|
+
BRANCH_NOT_FOUND = 6005
|
|
98
|
+
BRANCH_ALREADY_EXISTS = 6006
|
|
99
|
+
|
|
100
|
+
# Dependency errors (7000-7999)
|
|
101
|
+
CIRCULAR_DEPENDENCY = 7001
|
|
102
|
+
UNMET_DEPENDENCY = 7002
|
|
103
|
+
|
|
104
|
+
# Security errors (8000-8999)
|
|
105
|
+
SECURITY_SCAN_FAILED = 8001
|
|
106
|
+
VULNERABILITY_FOUND = 8002
|
|
107
|
+
|
|
108
|
+
# Timeout errors (9000-9999)
|
|
109
|
+
OPERATION_TIMEOUT = 9001
|
|
110
|
+
SUBPROCESS_TIMEOUT = 9002
|
|
111
|
+
|
|
112
|
+
# Quality gate errors (10000-10999)
|
|
113
|
+
TEST_FAILED = 10001
|
|
114
|
+
LINT_FAILED = 10002
|
|
115
|
+
COVERAGE_BELOW_THRESHOLD = 10003
|
|
116
|
+
QUALITY_GATE_FAILED = 10004
|
|
117
|
+
|
|
118
|
+
# Deployment errors (11000-11999)
|
|
119
|
+
DEPLOYMENT_FAILED = 11001
|
|
120
|
+
PRE_DEPLOYMENT_CHECK_FAILED = 11002
|
|
121
|
+
SMOKE_TEST_FAILED = 11003
|
|
122
|
+
ROLLBACK_FAILED = 11004
|
|
123
|
+
DEPLOYMENT_STEP_FAILED = 11005
|
|
124
|
+
|
|
125
|
+
# API validation errors (12000-12999)
|
|
126
|
+
API_VALIDATION_FAILED = 12001
|
|
127
|
+
SCHEMA_VALIDATION_FAILED = 12002
|
|
128
|
+
CONTRACT_VIOLATION = 12003
|
|
129
|
+
BREAKING_CHANGE_DETECTED = 12004
|
|
130
|
+
INVALID_OPENAPI_SPEC = 12005
|
|
131
|
+
|
|
132
|
+
# Performance testing errors (13000-13999)
|
|
133
|
+
PERFORMANCE_TEST_FAILED = 13001
|
|
134
|
+
BENCHMARK_FAILED = 13002
|
|
135
|
+
PERFORMANCE_REGRESSION = 13003
|
|
136
|
+
LOAD_TEST_FAILED = 13004
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class SolokitError(Exception):
|
|
140
|
+
"""
|
|
141
|
+
Base exception for all Solokit errors.
|
|
142
|
+
|
|
143
|
+
Attributes:
|
|
144
|
+
message: Human-readable error message
|
|
145
|
+
code: ErrorCode enum for programmatic handling
|
|
146
|
+
category: ErrorCategory enum for classification
|
|
147
|
+
context: Additional context data (file paths, IDs, etc.)
|
|
148
|
+
remediation: Suggested fix for the user
|
|
149
|
+
cause: Original exception if wrapping another error
|
|
150
|
+
exit_code: Suggested exit code for CLI
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
>>> error = SolokitError(
|
|
154
|
+
... message="Something went wrong",
|
|
155
|
+
... code=ErrorCode.FILE_OPERATION_FAILED,
|
|
156
|
+
... category=ErrorCategory.SYSTEM,
|
|
157
|
+
... context={"file": "/path/to/file"},
|
|
158
|
+
... remediation="Check file permissions"
|
|
159
|
+
... )
|
|
160
|
+
>>> print(error.to_dict())
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
message: str,
|
|
166
|
+
code: ErrorCode,
|
|
167
|
+
category: ErrorCategory,
|
|
168
|
+
context: dict[str, Any] | None = None,
|
|
169
|
+
remediation: str | None = None,
|
|
170
|
+
cause: Exception | None = None,
|
|
171
|
+
):
|
|
172
|
+
super().__init__(message)
|
|
173
|
+
self.message = message
|
|
174
|
+
self.code = code
|
|
175
|
+
self.category = category
|
|
176
|
+
self.context = context or {}
|
|
177
|
+
self.remediation = remediation
|
|
178
|
+
self.cause = cause
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def exit_code(self) -> int:
|
|
182
|
+
"""Get CLI exit code based on error category"""
|
|
183
|
+
exit_codes = {
|
|
184
|
+
ErrorCategory.VALIDATION: 2,
|
|
185
|
+
ErrorCategory.NOT_FOUND: 3,
|
|
186
|
+
ErrorCategory.CONFIGURATION: 4,
|
|
187
|
+
ErrorCategory.SYSTEM: 5,
|
|
188
|
+
ErrorCategory.GIT: 6,
|
|
189
|
+
ErrorCategory.DEPENDENCY: 7,
|
|
190
|
+
ErrorCategory.SECURITY: 8,
|
|
191
|
+
ErrorCategory.TIMEOUT: 9,
|
|
192
|
+
ErrorCategory.ALREADY_EXISTS: 10,
|
|
193
|
+
ErrorCategory.PERMISSION: 11,
|
|
194
|
+
}
|
|
195
|
+
return exit_codes.get(self.category, 1)
|
|
196
|
+
|
|
197
|
+
def __str__(self) -> str:
|
|
198
|
+
"""Format error for display"""
|
|
199
|
+
parts = [self.message]
|
|
200
|
+
if self.remediation:
|
|
201
|
+
parts.append(f"Remediation: {self.remediation}")
|
|
202
|
+
return "\n".join(parts)
|
|
203
|
+
|
|
204
|
+
def to_dict(self) -> dict[str, Any]:
|
|
205
|
+
"""Convert to dictionary for structured logging"""
|
|
206
|
+
return {
|
|
207
|
+
"message": self.message,
|
|
208
|
+
"code": self.code.value,
|
|
209
|
+
"code_name": self.code.name,
|
|
210
|
+
"category": self.category.value,
|
|
211
|
+
"context": self.context,
|
|
212
|
+
"remediation": self.remediation,
|
|
213
|
+
"cause": str(self.cause) if self.cause else None,
|
|
214
|
+
"exit_code": self.exit_code,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ============================================================================
|
|
219
|
+
# Validation Errors
|
|
220
|
+
# ============================================================================
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class ValidationError(SolokitError):
|
|
224
|
+
"""
|
|
225
|
+
Raised when validation fails.
|
|
226
|
+
|
|
227
|
+
Example:
|
|
228
|
+
>>> raise ValidationError(
|
|
229
|
+
... message="Invalid work item ID format",
|
|
230
|
+
... code=ErrorCode.INVALID_WORK_ITEM_ID,
|
|
231
|
+
... context={"work_item_id": "bad-id!"},
|
|
232
|
+
... remediation="Use alphanumeric characters and underscores only"
|
|
233
|
+
... )
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
def __init__(
|
|
237
|
+
self,
|
|
238
|
+
message: str,
|
|
239
|
+
code: ErrorCode = ErrorCode.MISSING_REQUIRED_FIELD,
|
|
240
|
+
context: dict[str, Any] | None = None,
|
|
241
|
+
remediation: str | None = None,
|
|
242
|
+
cause: Exception | None = None,
|
|
243
|
+
):
|
|
244
|
+
super().__init__(
|
|
245
|
+
message=message,
|
|
246
|
+
code=code,
|
|
247
|
+
category=ErrorCategory.VALIDATION,
|
|
248
|
+
context=context,
|
|
249
|
+
remediation=remediation,
|
|
250
|
+
cause=cause,
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class SpecValidationError(ValidationError):
|
|
255
|
+
"""
|
|
256
|
+
Raised when spec file validation fails.
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> raise SpecValidationError(
|
|
260
|
+
... work_item_id="my_feature",
|
|
261
|
+
... errors=["Missing Overview section", "Missing acceptance criteria"]
|
|
262
|
+
... )
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, work_item_id: str, errors: list[str], remediation: str | None = None):
|
|
266
|
+
message = f"Spec validation failed for '{work_item_id}'"
|
|
267
|
+
context = {
|
|
268
|
+
"work_item_id": work_item_id,
|
|
269
|
+
"validation_errors": errors,
|
|
270
|
+
"error_count": len(errors),
|
|
271
|
+
}
|
|
272
|
+
super().__init__(
|
|
273
|
+
message=message,
|
|
274
|
+
code=ErrorCode.SPEC_VALIDATION_FAILED,
|
|
275
|
+
context=context,
|
|
276
|
+
remediation=remediation
|
|
277
|
+
or f"Edit .session/specs/{work_item_id}.md to fix validation errors",
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# ============================================================================
|
|
282
|
+
# Not Found Errors
|
|
283
|
+
# ============================================================================
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class NotFoundError(SolokitError):
|
|
287
|
+
"""
|
|
288
|
+
Raised when a resource is not found.
|
|
289
|
+
|
|
290
|
+
Example:
|
|
291
|
+
>>> raise NotFoundError(
|
|
292
|
+
... message="Configuration file not found",
|
|
293
|
+
... code=ErrorCode.CONFIG_NOT_FOUND,
|
|
294
|
+
... context={"path": ".session/config.json"}
|
|
295
|
+
... )
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
def __init__(
|
|
299
|
+
self,
|
|
300
|
+
message: str,
|
|
301
|
+
code: ErrorCode,
|
|
302
|
+
context: dict[str, Any] | None = None,
|
|
303
|
+
remediation: str | None = None,
|
|
304
|
+
cause: Exception | None = None,
|
|
305
|
+
):
|
|
306
|
+
super().__init__(
|
|
307
|
+
message=message,
|
|
308
|
+
code=code,
|
|
309
|
+
category=ErrorCategory.NOT_FOUND,
|
|
310
|
+
context=context,
|
|
311
|
+
remediation=remediation,
|
|
312
|
+
cause=cause,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
class WorkItemNotFoundError(NotFoundError):
|
|
317
|
+
"""
|
|
318
|
+
Raised when work item doesn't exist.
|
|
319
|
+
|
|
320
|
+
Example:
|
|
321
|
+
>>> raise WorkItemNotFoundError("nonexistent_feature")
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
def __init__(self, work_item_id: str):
|
|
325
|
+
super().__init__(
|
|
326
|
+
message=f"Work item '{work_item_id}' not found",
|
|
327
|
+
code=ErrorCode.WORK_ITEM_NOT_FOUND,
|
|
328
|
+
context={"work_item_id": work_item_id},
|
|
329
|
+
remediation="Use 'sk work-list' to see available work items",
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
class FileNotFoundError(NotFoundError):
|
|
334
|
+
"""
|
|
335
|
+
Raised when a required file doesn't exist.
|
|
336
|
+
|
|
337
|
+
Note: This shadows the built-in FileNotFoundError, but provides
|
|
338
|
+
more structured error information.
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
>>> raise FileNotFoundError(
|
|
342
|
+
... file_path=".session/specs/my_feature.md",
|
|
343
|
+
... file_type="spec"
|
|
344
|
+
... )
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
def __init__(self, file_path: str, file_type: str | None = None):
|
|
348
|
+
context = {"file_path": file_path}
|
|
349
|
+
if file_type:
|
|
350
|
+
context["file_type"] = file_type
|
|
351
|
+
|
|
352
|
+
remediation_msg = None
|
|
353
|
+
if file_type:
|
|
354
|
+
remediation_msg = f"Create the missing {file_type} file: {file_path}"
|
|
355
|
+
|
|
356
|
+
super().__init__(
|
|
357
|
+
message=f"File not found: {file_path}",
|
|
358
|
+
code=ErrorCode.FILE_NOT_FOUND,
|
|
359
|
+
context=context,
|
|
360
|
+
remediation=remediation_msg,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class SessionNotFoundError(NotFoundError):
|
|
365
|
+
"""
|
|
366
|
+
Raised when no active session exists.
|
|
367
|
+
|
|
368
|
+
Example:
|
|
369
|
+
>>> raise SessionNotFoundError()
|
|
370
|
+
"""
|
|
371
|
+
|
|
372
|
+
def __init__(self) -> None:
|
|
373
|
+
super().__init__(
|
|
374
|
+
message="No active session found",
|
|
375
|
+
code=ErrorCode.SESSION_NOT_FOUND,
|
|
376
|
+
remediation="Start a session with 'sk start' or 'sk start <work_item_id>'",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
# ============================================================================
|
|
381
|
+
# Configuration Errors
|
|
382
|
+
# ============================================================================
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class ConfigurationError(SolokitError):
|
|
386
|
+
"""
|
|
387
|
+
Raised when configuration is invalid.
|
|
388
|
+
|
|
389
|
+
Example:
|
|
390
|
+
>>> raise ConfigurationError(
|
|
391
|
+
... message="Invalid configuration value",
|
|
392
|
+
... code=ErrorCode.INVALID_CONFIG_VALUE,
|
|
393
|
+
... context={"key": "test_command", "value": None}
|
|
394
|
+
... )
|
|
395
|
+
"""
|
|
396
|
+
|
|
397
|
+
def __init__(
|
|
398
|
+
self,
|
|
399
|
+
message: str,
|
|
400
|
+
code: ErrorCode = ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
401
|
+
context: dict[str, Any] | None = None,
|
|
402
|
+
remediation: str | None = None,
|
|
403
|
+
cause: Exception | None = None,
|
|
404
|
+
):
|
|
405
|
+
super().__init__(
|
|
406
|
+
message=message,
|
|
407
|
+
code=code,
|
|
408
|
+
category=ErrorCategory.CONFIGURATION,
|
|
409
|
+
context=context,
|
|
410
|
+
remediation=remediation,
|
|
411
|
+
cause=cause,
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
class ConfigValidationError(ConfigurationError):
|
|
416
|
+
"""
|
|
417
|
+
Raised when config fails schema validation.
|
|
418
|
+
|
|
419
|
+
Example:
|
|
420
|
+
>>> raise ConfigValidationError(
|
|
421
|
+
... config_path=".session/config.json",
|
|
422
|
+
... errors=["Missing 'project_name' field"]
|
|
423
|
+
... )
|
|
424
|
+
"""
|
|
425
|
+
|
|
426
|
+
def __init__(self, config_path: str, errors: list[str]):
|
|
427
|
+
message = f"Configuration validation failed: {config_path}"
|
|
428
|
+
context = {
|
|
429
|
+
"config_path": config_path,
|
|
430
|
+
"validation_errors": errors,
|
|
431
|
+
"error_count": len(errors),
|
|
432
|
+
}
|
|
433
|
+
super().__init__(
|
|
434
|
+
message=message,
|
|
435
|
+
code=ErrorCode.CONFIG_VALIDATION_FAILED,
|
|
436
|
+
context=context,
|
|
437
|
+
remediation="Check docs/guides/configuration.md for valid configuration options",
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
# ============================================================================
|
|
442
|
+
# Git Errors
|
|
443
|
+
# ============================================================================
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class GitError(SolokitError):
|
|
447
|
+
"""
|
|
448
|
+
Raised for git-related errors.
|
|
449
|
+
|
|
450
|
+
Example:
|
|
451
|
+
>>> raise GitError(
|
|
452
|
+
... message="Git command failed",
|
|
453
|
+
... code=ErrorCode.GIT_COMMAND_FAILED,
|
|
454
|
+
... context={"command": "git status"}
|
|
455
|
+
... )
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
def __init__(
|
|
459
|
+
self,
|
|
460
|
+
message: str,
|
|
461
|
+
code: ErrorCode,
|
|
462
|
+
context: dict[str, Any] | None = None,
|
|
463
|
+
remediation: str | None = None,
|
|
464
|
+
cause: Exception | None = None,
|
|
465
|
+
):
|
|
466
|
+
super().__init__(
|
|
467
|
+
message=message,
|
|
468
|
+
code=code,
|
|
469
|
+
category=ErrorCategory.GIT,
|
|
470
|
+
context=context,
|
|
471
|
+
remediation=remediation,
|
|
472
|
+
cause=cause,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
class NotAGitRepoError(GitError):
|
|
477
|
+
"""
|
|
478
|
+
Raised when operation requires git repo but not in one.
|
|
479
|
+
|
|
480
|
+
Example:
|
|
481
|
+
>>> raise NotAGitRepoError()
|
|
482
|
+
"""
|
|
483
|
+
|
|
484
|
+
def __init__(self, path: str | None = None):
|
|
485
|
+
context = {"path": path} if path else {}
|
|
486
|
+
super().__init__(
|
|
487
|
+
message="Not a git repository",
|
|
488
|
+
code=ErrorCode.NOT_A_GIT_REPO,
|
|
489
|
+
context=context,
|
|
490
|
+
remediation="Run 'git init' to initialize a repository",
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
class WorkingDirNotCleanError(GitError):
|
|
495
|
+
"""
|
|
496
|
+
Raised when working directory has uncommitted changes.
|
|
497
|
+
|
|
498
|
+
Example:
|
|
499
|
+
>>> raise WorkingDirNotCleanError(
|
|
500
|
+
... changes=["M src/file.py", "?? new_file.py"]
|
|
501
|
+
... )
|
|
502
|
+
"""
|
|
503
|
+
|
|
504
|
+
def __init__(self, changes: list[str] | None = None):
|
|
505
|
+
context = {"uncommitted_changes": changes} if changes else {}
|
|
506
|
+
super().__init__(
|
|
507
|
+
message="Working directory not clean (uncommitted changes)",
|
|
508
|
+
code=ErrorCode.WORKING_DIR_NOT_CLEAN,
|
|
509
|
+
context=context,
|
|
510
|
+
remediation="Commit or stash changes before proceeding",
|
|
511
|
+
)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
class BranchNotFoundError(GitError):
|
|
515
|
+
"""
|
|
516
|
+
Raised when git branch doesn't exist.
|
|
517
|
+
|
|
518
|
+
Example:
|
|
519
|
+
>>> raise BranchNotFoundError("feature-branch")
|
|
520
|
+
"""
|
|
521
|
+
|
|
522
|
+
def __init__(self, branch_name: str):
|
|
523
|
+
super().__init__(
|
|
524
|
+
message=f"Branch '{branch_name}' not found",
|
|
525
|
+
code=ErrorCode.BRANCH_NOT_FOUND,
|
|
526
|
+
context={"branch_name": branch_name},
|
|
527
|
+
remediation="Check branch name or create it with 'git checkout -b <branch>'",
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ============================================================================
|
|
532
|
+
# System Errors
|
|
533
|
+
# ============================================================================
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
class SystemError(SolokitError):
|
|
537
|
+
"""
|
|
538
|
+
Raised for system-level errors.
|
|
539
|
+
|
|
540
|
+
Example:
|
|
541
|
+
>>> raise SystemError(
|
|
542
|
+
... message="Failed to write file",
|
|
543
|
+
... code=ErrorCode.FILE_OPERATION_FAILED,
|
|
544
|
+
... context={"path": "/tmp/file.txt"}
|
|
545
|
+
... )
|
|
546
|
+
"""
|
|
547
|
+
|
|
548
|
+
def __init__(
|
|
549
|
+
self,
|
|
550
|
+
message: str,
|
|
551
|
+
code: ErrorCode,
|
|
552
|
+
context: dict[str, Any] | None = None,
|
|
553
|
+
remediation: str | None = None,
|
|
554
|
+
cause: Exception | None = None,
|
|
555
|
+
):
|
|
556
|
+
super().__init__(
|
|
557
|
+
message=message,
|
|
558
|
+
code=code,
|
|
559
|
+
category=ErrorCategory.SYSTEM,
|
|
560
|
+
context=context,
|
|
561
|
+
remediation=remediation,
|
|
562
|
+
cause=cause,
|
|
563
|
+
)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
class SubprocessError(SystemError):
|
|
567
|
+
"""
|
|
568
|
+
Raised when subprocess command fails.
|
|
569
|
+
|
|
570
|
+
Example:
|
|
571
|
+
>>> raise SubprocessError(
|
|
572
|
+
... command="pytest tests/",
|
|
573
|
+
... returncode=1,
|
|
574
|
+
... stderr="FAILED tests/test_foo.py"
|
|
575
|
+
... )
|
|
576
|
+
"""
|
|
577
|
+
|
|
578
|
+
def __init__(
|
|
579
|
+
self, command: str, returncode: int, stderr: str | None = None, stdout: str | None = None
|
|
580
|
+
):
|
|
581
|
+
context = {"command": command, "returncode": returncode, "stderr": stderr, "stdout": stdout}
|
|
582
|
+
super().__init__(
|
|
583
|
+
message=f"Command failed with exit code {returncode}: {command}",
|
|
584
|
+
code=ErrorCode.SUBPROCESS_FAILED,
|
|
585
|
+
context=context,
|
|
586
|
+
)
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
class TimeoutError(SystemError):
|
|
590
|
+
"""
|
|
591
|
+
Raised when operation times out.
|
|
592
|
+
|
|
593
|
+
Example:
|
|
594
|
+
>>> raise TimeoutError(
|
|
595
|
+
... operation="git fetch",
|
|
596
|
+
... timeout_seconds=30
|
|
597
|
+
... )
|
|
598
|
+
"""
|
|
599
|
+
|
|
600
|
+
def __init__(self, operation: str, timeout_seconds: int, context: dict[str, Any] | None = None):
|
|
601
|
+
ctx = context or {}
|
|
602
|
+
ctx.update({"operation": operation, "timeout_seconds": timeout_seconds})
|
|
603
|
+
super().__init__(
|
|
604
|
+
message=f"Operation timed out after {timeout_seconds}s: {operation}",
|
|
605
|
+
code=ErrorCode.OPERATION_TIMEOUT,
|
|
606
|
+
context=ctx,
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
class CommandExecutionError(SystemError):
|
|
611
|
+
"""
|
|
612
|
+
Raised when a command execution fails.
|
|
613
|
+
|
|
614
|
+
This wraps the CommandExecutionError from command_runner for consistency.
|
|
615
|
+
|
|
616
|
+
Example:
|
|
617
|
+
>>> raise CommandExecutionError(
|
|
618
|
+
... command="npm test",
|
|
619
|
+
... returncode=1,
|
|
620
|
+
... stderr="Test failed"
|
|
621
|
+
... )
|
|
622
|
+
"""
|
|
623
|
+
|
|
624
|
+
def __init__(
|
|
625
|
+
self,
|
|
626
|
+
command: str,
|
|
627
|
+
returncode: int | None = None,
|
|
628
|
+
stderr: str | None = None,
|
|
629
|
+
stdout: str | None = None,
|
|
630
|
+
exit_code: int | None = None,
|
|
631
|
+
context: dict[str, Any] | None = None,
|
|
632
|
+
):
|
|
633
|
+
# Support both returncode and exit_code for backwards compatibility
|
|
634
|
+
actual_code = exit_code if exit_code is not None else returncode
|
|
635
|
+
|
|
636
|
+
# Merge provided context with command details
|
|
637
|
+
ctx = context or {}
|
|
638
|
+
ctx.update(
|
|
639
|
+
{
|
|
640
|
+
"command": command,
|
|
641
|
+
"returncode": actual_code,
|
|
642
|
+
"stderr": stderr,
|
|
643
|
+
"stdout": stdout,
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
|
|
647
|
+
super().__init__(
|
|
648
|
+
message=f"Command execution failed: {command}",
|
|
649
|
+
code=ErrorCode.COMMAND_FAILED,
|
|
650
|
+
context=ctx,
|
|
651
|
+
)
|
|
652
|
+
# Store returncode for easy access (can't override exit_code which is a property)
|
|
653
|
+
self.returncode = actual_code
|
|
654
|
+
# Store stderr and stdout for easy access
|
|
655
|
+
self.stderr = stderr
|
|
656
|
+
self.stdout = stdout
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
# ============================================================================
|
|
660
|
+
# Dependency Errors
|
|
661
|
+
# ============================================================================
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
class DependencyError(SolokitError):
|
|
665
|
+
"""
|
|
666
|
+
Raised for dependency-related errors.
|
|
667
|
+
|
|
668
|
+
Example:
|
|
669
|
+
>>> raise DependencyError(
|
|
670
|
+
... message="Dependency not met",
|
|
671
|
+
... code=ErrorCode.UNMET_DEPENDENCY,
|
|
672
|
+
... context={"dependency": "feature_a"}
|
|
673
|
+
... )
|
|
674
|
+
"""
|
|
675
|
+
|
|
676
|
+
def __init__(
|
|
677
|
+
self,
|
|
678
|
+
message: str,
|
|
679
|
+
code: ErrorCode,
|
|
680
|
+
context: dict[str, Any] | None = None,
|
|
681
|
+
remediation: str | None = None,
|
|
682
|
+
cause: Exception | None = None,
|
|
683
|
+
):
|
|
684
|
+
super().__init__(
|
|
685
|
+
message=message,
|
|
686
|
+
code=code,
|
|
687
|
+
category=ErrorCategory.DEPENDENCY,
|
|
688
|
+
context=context,
|
|
689
|
+
remediation=remediation,
|
|
690
|
+
cause=cause,
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class CircularDependencyError(DependencyError):
|
|
695
|
+
"""
|
|
696
|
+
Raised when circular dependency detected.
|
|
697
|
+
|
|
698
|
+
Example:
|
|
699
|
+
>>> raise CircularDependencyError(["feature_a", "feature_b", "feature_a"])
|
|
700
|
+
"""
|
|
701
|
+
|
|
702
|
+
def __init__(self, cycle: list[str]):
|
|
703
|
+
cycle_str = " -> ".join(cycle)
|
|
704
|
+
super().__init__(
|
|
705
|
+
message=f"Circular dependency detected: {cycle_str}",
|
|
706
|
+
code=ErrorCode.CIRCULAR_DEPENDENCY,
|
|
707
|
+
context={"cycle": cycle},
|
|
708
|
+
remediation="Break the dependency cycle by reordering work items",
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class UnmetDependencyError(DependencyError):
|
|
713
|
+
"""
|
|
714
|
+
Raised when dependency not met.
|
|
715
|
+
|
|
716
|
+
Example:
|
|
717
|
+
>>> raise UnmetDependencyError("feature_b", "feature_a")
|
|
718
|
+
"""
|
|
719
|
+
|
|
720
|
+
def __init__(self, work_item_id: str, dependency_id: str):
|
|
721
|
+
super().__init__(
|
|
722
|
+
message=f"Cannot start '{work_item_id}': dependency '{dependency_id}' not completed",
|
|
723
|
+
code=ErrorCode.UNMET_DEPENDENCY,
|
|
724
|
+
context={"work_item_id": work_item_id, "dependency_id": dependency_id},
|
|
725
|
+
remediation=f"Complete '{dependency_id}' before starting '{work_item_id}'",
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
# ============================================================================
|
|
730
|
+
# Already Exists Errors
|
|
731
|
+
# ============================================================================
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
class AlreadyExistsError(SolokitError):
|
|
735
|
+
"""
|
|
736
|
+
Raised when resource already exists.
|
|
737
|
+
|
|
738
|
+
Example:
|
|
739
|
+
>>> raise AlreadyExistsError(
|
|
740
|
+
... message="Work item already exists",
|
|
741
|
+
... code=ErrorCode.WORK_ITEM_ALREADY_EXISTS,
|
|
742
|
+
... context={"work_item_id": "my_feature"}
|
|
743
|
+
... )
|
|
744
|
+
"""
|
|
745
|
+
|
|
746
|
+
def __init__(
|
|
747
|
+
self,
|
|
748
|
+
message: str,
|
|
749
|
+
code: ErrorCode,
|
|
750
|
+
context: dict[str, Any] | None = None,
|
|
751
|
+
remediation: str | None = None,
|
|
752
|
+
cause: Exception | None = None,
|
|
753
|
+
):
|
|
754
|
+
super().__init__(
|
|
755
|
+
message=message,
|
|
756
|
+
code=code,
|
|
757
|
+
category=ErrorCategory.ALREADY_EXISTS,
|
|
758
|
+
context=context,
|
|
759
|
+
remediation=remediation,
|
|
760
|
+
cause=cause,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
|
|
764
|
+
class SessionAlreadyActiveError(AlreadyExistsError):
|
|
765
|
+
"""
|
|
766
|
+
Raised when trying to start session while one is active.
|
|
767
|
+
|
|
768
|
+
Example:
|
|
769
|
+
>>> raise SessionAlreadyActiveError("current_feature")
|
|
770
|
+
"""
|
|
771
|
+
|
|
772
|
+
def __init__(self, current_work_item_id: str):
|
|
773
|
+
super().__init__(
|
|
774
|
+
message=f"Session already active for '{current_work_item_id}'",
|
|
775
|
+
code=ErrorCode.SESSION_ALREADY_ACTIVE,
|
|
776
|
+
context={"current_work_item_id": current_work_item_id},
|
|
777
|
+
remediation="Complete current session with 'sk end' before starting a new one",
|
|
778
|
+
)
|
|
779
|
+
|
|
780
|
+
|
|
781
|
+
class WorkItemAlreadyExistsError(AlreadyExistsError):
|
|
782
|
+
"""
|
|
783
|
+
Raised when trying to create work item that already exists.
|
|
784
|
+
|
|
785
|
+
Example:
|
|
786
|
+
>>> raise WorkItemAlreadyExistsError("my_feature")
|
|
787
|
+
"""
|
|
788
|
+
|
|
789
|
+
def __init__(self, work_item_id: str):
|
|
790
|
+
super().__init__(
|
|
791
|
+
message=f"Work item '{work_item_id}' already exists",
|
|
792
|
+
code=ErrorCode.WORK_ITEM_ALREADY_EXISTS,
|
|
793
|
+
context={"work_item_id": work_item_id},
|
|
794
|
+
remediation=f"Use 'sk work-show {work_item_id}' to view existing work item",
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
# ============================================================================
|
|
799
|
+
# Quality Gate Errors
|
|
800
|
+
# ============================================================================
|
|
801
|
+
|
|
802
|
+
|
|
803
|
+
class QualityGateError(SolokitError):
|
|
804
|
+
"""
|
|
805
|
+
Raised when quality gate fails.
|
|
806
|
+
|
|
807
|
+
Example:
|
|
808
|
+
>>> raise QualityGateError(
|
|
809
|
+
... message="Tests failed",
|
|
810
|
+
... code=ErrorCode.TEST_FAILED,
|
|
811
|
+
... context={"failed_tests": ["test_foo", "test_bar"]}
|
|
812
|
+
... )
|
|
813
|
+
"""
|
|
814
|
+
|
|
815
|
+
def __init__(
|
|
816
|
+
self,
|
|
817
|
+
message: str,
|
|
818
|
+
code: ErrorCode,
|
|
819
|
+
context: dict[str, Any] | None = None,
|
|
820
|
+
remediation: str | None = None,
|
|
821
|
+
cause: Exception | None = None,
|
|
822
|
+
):
|
|
823
|
+
super().__init__(
|
|
824
|
+
message=message,
|
|
825
|
+
code=code,
|
|
826
|
+
category=ErrorCategory.VALIDATION,
|
|
827
|
+
context=context,
|
|
828
|
+
remediation=remediation,
|
|
829
|
+
cause=cause,
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
|
|
833
|
+
class QualityTestFailedError(QualityGateError):
|
|
834
|
+
"""
|
|
835
|
+
Raised when quality tests fail.
|
|
836
|
+
|
|
837
|
+
Example:
|
|
838
|
+
>>> raise QualityTestFailedError(
|
|
839
|
+
... failed_count=2,
|
|
840
|
+
... total_count=10,
|
|
841
|
+
... details=["test_foo failed", "test_bar failed"]
|
|
842
|
+
... )
|
|
843
|
+
"""
|
|
844
|
+
|
|
845
|
+
def __init__(self, failed_count: int, total_count: int, details: list[str] | None = None):
|
|
846
|
+
message = f"{failed_count} of {total_count} tests failed"
|
|
847
|
+
context = {"failed_count": failed_count, "total_count": total_count, "details": details}
|
|
848
|
+
super().__init__(
|
|
849
|
+
message=message,
|
|
850
|
+
code=ErrorCode.TEST_FAILED,
|
|
851
|
+
context=context,
|
|
852
|
+
remediation="Fix failing tests before completing session",
|
|
853
|
+
)
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
# ============================================================================
|
|
857
|
+
# File Operation Errors
|
|
858
|
+
# ============================================================================
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class FileOperationError(SystemError):
|
|
862
|
+
"""
|
|
863
|
+
Raised when file operations fail (read, write, parse, etc.).
|
|
864
|
+
|
|
865
|
+
Example:
|
|
866
|
+
>>> raise FileOperationError(
|
|
867
|
+
... operation="write",
|
|
868
|
+
... file_path="/path/to/file.json",
|
|
869
|
+
... details="Permission denied"
|
|
870
|
+
... )
|
|
871
|
+
"""
|
|
872
|
+
|
|
873
|
+
def __init__(
|
|
874
|
+
self,
|
|
875
|
+
operation: str,
|
|
876
|
+
file_path: str,
|
|
877
|
+
details: str | None = None,
|
|
878
|
+
cause: Exception | None = None,
|
|
879
|
+
):
|
|
880
|
+
message = f"File {operation} operation failed: {file_path}"
|
|
881
|
+
if details:
|
|
882
|
+
message = f"{message} - {details}"
|
|
883
|
+
|
|
884
|
+
context = {"operation": operation, "file_path": file_path, "details": details}
|
|
885
|
+
super().__init__(
|
|
886
|
+
message=message, code=ErrorCode.FILE_OPERATION_FAILED, context=context, cause=cause
|
|
887
|
+
)
|
|
888
|
+
# Store as instance attributes for easy access
|
|
889
|
+
self.operation = operation
|
|
890
|
+
self.file_path = file_path
|
|
891
|
+
self.details = details
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
# ============================================================================
|
|
895
|
+
# Learning Errors
|
|
896
|
+
# ============================================================================
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
class LearningError(ValidationError):
|
|
900
|
+
"""
|
|
901
|
+
Raised when learning operations fail (validation, storage, curation).
|
|
902
|
+
|
|
903
|
+
Example:
|
|
904
|
+
>>> raise LearningError(
|
|
905
|
+
... message="Learning content cannot be empty",
|
|
906
|
+
... context={"learning_id": "abc123"}
|
|
907
|
+
... )
|
|
908
|
+
"""
|
|
909
|
+
|
|
910
|
+
def __init__(
|
|
911
|
+
self,
|
|
912
|
+
message: str,
|
|
913
|
+
context: dict[str, Any] | None = None,
|
|
914
|
+
remediation: str | None = None,
|
|
915
|
+
cause: Exception | None = None,
|
|
916
|
+
):
|
|
917
|
+
super().__init__(
|
|
918
|
+
message=message,
|
|
919
|
+
code=ErrorCode.MISSING_REQUIRED_FIELD,
|
|
920
|
+
context=context,
|
|
921
|
+
remediation=remediation or "Check learning content and structure",
|
|
922
|
+
cause=cause,
|
|
923
|
+
)
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
class LearningNotFoundError(NotFoundError):
|
|
927
|
+
"""
|
|
928
|
+
Raised when a learning doesn't exist.
|
|
929
|
+
|
|
930
|
+
Example:
|
|
931
|
+
>>> raise LearningNotFoundError("abc123")
|
|
932
|
+
"""
|
|
933
|
+
|
|
934
|
+
def __init__(self, learning_id: str):
|
|
935
|
+
super().__init__(
|
|
936
|
+
message=f"Learning '{learning_id}' not found",
|
|
937
|
+
code=ErrorCode.LEARNING_NOT_FOUND,
|
|
938
|
+
context={"learning_id": learning_id},
|
|
939
|
+
remediation="Use search or list commands to find available learnings",
|
|
940
|
+
)
|
|
941
|
+
|
|
942
|
+
|
|
943
|
+
# ============================================================================
|
|
944
|
+
# Deployment Errors
|
|
945
|
+
# ============================================================================
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
class DeploymentError(SolokitError):
|
|
949
|
+
"""
|
|
950
|
+
Raised when deployment operations fail.
|
|
951
|
+
|
|
952
|
+
Example:
|
|
953
|
+
>>> raise DeploymentError(
|
|
954
|
+
... message="Deployment failed",
|
|
955
|
+
... code=ErrorCode.DEPLOYMENT_FAILED,
|
|
956
|
+
... context={"work_item_id": "deploy-001"}
|
|
957
|
+
... )
|
|
958
|
+
"""
|
|
959
|
+
|
|
960
|
+
def __init__(
|
|
961
|
+
self,
|
|
962
|
+
message: str,
|
|
963
|
+
code: ErrorCode = ErrorCode.DEPLOYMENT_FAILED,
|
|
964
|
+
context: dict[str, Any] | None = None,
|
|
965
|
+
remediation: str | None = None,
|
|
966
|
+
cause: Exception | None = None,
|
|
967
|
+
):
|
|
968
|
+
super().__init__(
|
|
969
|
+
message=message,
|
|
970
|
+
code=code,
|
|
971
|
+
category=ErrorCategory.SYSTEM,
|
|
972
|
+
context=context,
|
|
973
|
+
remediation=remediation,
|
|
974
|
+
cause=cause,
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
class PreDeploymentCheckError(DeploymentError):
|
|
979
|
+
"""
|
|
980
|
+
Raised when pre-deployment validation checks fail.
|
|
981
|
+
|
|
982
|
+
Example:
|
|
983
|
+
>>> raise PreDeploymentCheckError(
|
|
984
|
+
... check_name="integration_tests",
|
|
985
|
+
... details="3 tests failed"
|
|
986
|
+
... )
|
|
987
|
+
"""
|
|
988
|
+
|
|
989
|
+
def __init__(
|
|
990
|
+
self, check_name: str, details: str | None = None, context: dict[str, Any] | None = None
|
|
991
|
+
):
|
|
992
|
+
message = f"Pre-deployment check '{check_name}' failed"
|
|
993
|
+
if details:
|
|
994
|
+
message = f"{message}: {details}"
|
|
995
|
+
|
|
996
|
+
ctx = context or {}
|
|
997
|
+
ctx.update({"check_name": check_name, "details": details})
|
|
998
|
+
|
|
999
|
+
super().__init__(
|
|
1000
|
+
message=message,
|
|
1001
|
+
code=ErrorCode.PRE_DEPLOYMENT_CHECK_FAILED,
|
|
1002
|
+
context=ctx,
|
|
1003
|
+
remediation=f"Fix {check_name} issues before proceeding with deployment",
|
|
1004
|
+
)
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
class SmokeTestError(DeploymentError):
|
|
1008
|
+
"""
|
|
1009
|
+
Raised when smoke tests fail after deployment.
|
|
1010
|
+
|
|
1011
|
+
Example:
|
|
1012
|
+
>>> raise SmokeTestError(
|
|
1013
|
+
... test_name="health_check",
|
|
1014
|
+
... details="Endpoint returned 500"
|
|
1015
|
+
... )
|
|
1016
|
+
"""
|
|
1017
|
+
|
|
1018
|
+
def __init__(
|
|
1019
|
+
self, test_name: str, details: str | None = None, context: dict[str, Any] | None = None
|
|
1020
|
+
):
|
|
1021
|
+
message = f"Smoke test '{test_name}' failed"
|
|
1022
|
+
if details:
|
|
1023
|
+
message = f"{message}: {details}"
|
|
1024
|
+
|
|
1025
|
+
ctx = context or {}
|
|
1026
|
+
ctx.update({"test_name": test_name, "details": details})
|
|
1027
|
+
|
|
1028
|
+
super().__init__(
|
|
1029
|
+
message=message,
|
|
1030
|
+
code=ErrorCode.SMOKE_TEST_FAILED,
|
|
1031
|
+
context=ctx,
|
|
1032
|
+
remediation="Check deployment logs and verify service health",
|
|
1033
|
+
)
|
|
1034
|
+
|
|
1035
|
+
|
|
1036
|
+
class RollbackError(DeploymentError):
|
|
1037
|
+
"""
|
|
1038
|
+
Raised when rollback operation fails.
|
|
1039
|
+
|
|
1040
|
+
Example:
|
|
1041
|
+
>>> raise RollbackError(
|
|
1042
|
+
... step="restore_database",
|
|
1043
|
+
... details="Backup file not found"
|
|
1044
|
+
... )
|
|
1045
|
+
"""
|
|
1046
|
+
|
|
1047
|
+
def __init__(
|
|
1048
|
+
self,
|
|
1049
|
+
step: str | None = None,
|
|
1050
|
+
details: str | None = None,
|
|
1051
|
+
context: dict[str, Any] | None = None,
|
|
1052
|
+
):
|
|
1053
|
+
message = "Rollback failed"
|
|
1054
|
+
if step:
|
|
1055
|
+
message = f"Rollback failed at step '{step}'"
|
|
1056
|
+
if details:
|
|
1057
|
+
message = f"{message}: {details}"
|
|
1058
|
+
|
|
1059
|
+
ctx = context or {}
|
|
1060
|
+
if step:
|
|
1061
|
+
ctx["failed_step"] = step
|
|
1062
|
+
if details:
|
|
1063
|
+
ctx["details"] = details
|
|
1064
|
+
|
|
1065
|
+
super().__init__(
|
|
1066
|
+
message=message,
|
|
1067
|
+
code=ErrorCode.ROLLBACK_FAILED,
|
|
1068
|
+
context=ctx,
|
|
1069
|
+
remediation="Manual intervention may be required to restore system state",
|
|
1070
|
+
)
|
|
1071
|
+
|
|
1072
|
+
|
|
1073
|
+
class DeploymentStepError(DeploymentError):
|
|
1074
|
+
"""
|
|
1075
|
+
Raised when a deployment step fails.
|
|
1076
|
+
|
|
1077
|
+
Example:
|
|
1078
|
+
>>> raise DeploymentStepError(
|
|
1079
|
+
... step_number=2,
|
|
1080
|
+
... step_description="Build application",
|
|
1081
|
+
... details="Compilation failed"
|
|
1082
|
+
... )
|
|
1083
|
+
"""
|
|
1084
|
+
|
|
1085
|
+
def __init__(
|
|
1086
|
+
self,
|
|
1087
|
+
step_number: int,
|
|
1088
|
+
step_description: str,
|
|
1089
|
+
details: str | None = None,
|
|
1090
|
+
context: dict[str, Any] | None = None,
|
|
1091
|
+
):
|
|
1092
|
+
message = f"Deployment step {step_number} failed: {step_description}"
|
|
1093
|
+
if details:
|
|
1094
|
+
message = f"{message} - {details}"
|
|
1095
|
+
|
|
1096
|
+
ctx = context or {}
|
|
1097
|
+
ctx.update(
|
|
1098
|
+
{"step_number": step_number, "step_description": step_description, "details": details}
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
super().__init__(
|
|
1102
|
+
message=message,
|
|
1103
|
+
code=ErrorCode.DEPLOYMENT_STEP_FAILED,
|
|
1104
|
+
context=ctx,
|
|
1105
|
+
remediation="Review deployment logs and fix the failing step",
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
# ============================================================================
|
|
1110
|
+
# Integration Test Errors
|
|
1111
|
+
# ============================================================================
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
class IntegrationTestError(SolokitError):
|
|
1115
|
+
"""
|
|
1116
|
+
Base exception for integration test failures.
|
|
1117
|
+
|
|
1118
|
+
Example:
|
|
1119
|
+
>>> raise IntegrationTestError(
|
|
1120
|
+
... message="Integration test failed",
|
|
1121
|
+
... context={"test_name": "order_processing"}
|
|
1122
|
+
... )
|
|
1123
|
+
"""
|
|
1124
|
+
|
|
1125
|
+
def __init__(
|
|
1126
|
+
self,
|
|
1127
|
+
message: str,
|
|
1128
|
+
code: ErrorCode = ErrorCode.TEST_FAILED,
|
|
1129
|
+
context: dict[str, Any] | None = None,
|
|
1130
|
+
remediation: str | None = None,
|
|
1131
|
+
cause: Exception | None = None,
|
|
1132
|
+
):
|
|
1133
|
+
super().__init__(
|
|
1134
|
+
message=message,
|
|
1135
|
+
code=code,
|
|
1136
|
+
category=ErrorCategory.SYSTEM,
|
|
1137
|
+
context=context,
|
|
1138
|
+
remediation=remediation,
|
|
1139
|
+
cause=cause,
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
class EnvironmentSetupError(IntegrationTestError):
|
|
1144
|
+
"""
|
|
1145
|
+
Raised when integration test environment setup fails.
|
|
1146
|
+
|
|
1147
|
+
Example:
|
|
1148
|
+
>>> raise EnvironmentSetupError(
|
|
1149
|
+
... component="docker-compose",
|
|
1150
|
+
... details="Failed to start PostgreSQL service"
|
|
1151
|
+
... )
|
|
1152
|
+
"""
|
|
1153
|
+
|
|
1154
|
+
def __init__(
|
|
1155
|
+
self, component: str, details: str | None = None, context: dict[str, Any] | None = None
|
|
1156
|
+
):
|
|
1157
|
+
message = f"Environment setup failed: {component}"
|
|
1158
|
+
if details:
|
|
1159
|
+
message = f"{message} - {details}"
|
|
1160
|
+
|
|
1161
|
+
ctx = context or {}
|
|
1162
|
+
ctx.update({"component": component, "details": details})
|
|
1163
|
+
|
|
1164
|
+
super().__init__(
|
|
1165
|
+
message=message,
|
|
1166
|
+
code=ErrorCode.COMMAND_FAILED,
|
|
1167
|
+
context=ctx,
|
|
1168
|
+
remediation="Check Docker/docker-compose installation and service configurations",
|
|
1169
|
+
)
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
class IntegrationExecutionError(IntegrationTestError):
|
|
1173
|
+
"""
|
|
1174
|
+
Raised when test execution fails.
|
|
1175
|
+
|
|
1176
|
+
Example:
|
|
1177
|
+
>>> raise IntegrationExecutionError(
|
|
1178
|
+
... test_framework="pytest",
|
|
1179
|
+
... details="3 tests failed"
|
|
1180
|
+
... )
|
|
1181
|
+
"""
|
|
1182
|
+
|
|
1183
|
+
def __init__(
|
|
1184
|
+
self, test_framework: str, details: str | None = None, context: dict[str, Any] | None = None
|
|
1185
|
+
):
|
|
1186
|
+
message = f"Test execution failed: {test_framework}"
|
|
1187
|
+
if details:
|
|
1188
|
+
message = f"{message} - {details}"
|
|
1189
|
+
|
|
1190
|
+
ctx = context or {}
|
|
1191
|
+
ctx.update({"test_framework": test_framework, "details": details})
|
|
1192
|
+
|
|
1193
|
+
super().__init__(
|
|
1194
|
+
message=message,
|
|
1195
|
+
code=ErrorCode.TEST_FAILED,
|
|
1196
|
+
context=ctx,
|
|
1197
|
+
remediation="Review test output and fix failing tests",
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
# ============================================================================
|
|
1202
|
+
# API Validation Errors
|
|
1203
|
+
# ============================================================================
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
class APIValidationError(ValidationError):
|
|
1207
|
+
"""
|
|
1208
|
+
Base exception for API validation errors.
|
|
1209
|
+
|
|
1210
|
+
Example:
|
|
1211
|
+
>>> raise APIValidationError(
|
|
1212
|
+
... message="API validation failed",
|
|
1213
|
+
... context={"endpoint": "/api/users"}
|
|
1214
|
+
... )
|
|
1215
|
+
"""
|
|
1216
|
+
|
|
1217
|
+
def __init__(
|
|
1218
|
+
self,
|
|
1219
|
+
message: str,
|
|
1220
|
+
code: ErrorCode = ErrorCode.API_VALIDATION_FAILED,
|
|
1221
|
+
context: dict[str, Any] | None = None,
|
|
1222
|
+
remediation: str | None = None,
|
|
1223
|
+
cause: Exception | None = None,
|
|
1224
|
+
):
|
|
1225
|
+
super().__init__(
|
|
1226
|
+
message=message, code=code, context=context, remediation=remediation, cause=cause
|
|
1227
|
+
)
|
|
1228
|
+
|
|
1229
|
+
|
|
1230
|
+
class SchemaValidationError(APIValidationError):
|
|
1231
|
+
"""
|
|
1232
|
+
Raised when OpenAPI/Swagger schema validation fails.
|
|
1233
|
+
|
|
1234
|
+
Example:
|
|
1235
|
+
>>> raise SchemaValidationError(
|
|
1236
|
+
... contract_file="api/openapi.yaml",
|
|
1237
|
+
... details="Missing 'paths' field"
|
|
1238
|
+
... )
|
|
1239
|
+
"""
|
|
1240
|
+
|
|
1241
|
+
def __init__(self, contract_file: str, details: str, context: dict[str, Any] | None = None):
|
|
1242
|
+
message = f"Schema validation failed for '{contract_file}': {details}"
|
|
1243
|
+
ctx = context or {}
|
|
1244
|
+
ctx.update({"contract_file": contract_file, "details": details})
|
|
1245
|
+
|
|
1246
|
+
super().__init__(
|
|
1247
|
+
message=message,
|
|
1248
|
+
code=ErrorCode.SCHEMA_VALIDATION_FAILED,
|
|
1249
|
+
context=ctx,
|
|
1250
|
+
remediation=f"Fix schema validation errors in {contract_file}",
|
|
1251
|
+
)
|
|
1252
|
+
|
|
1253
|
+
|
|
1254
|
+
class ContractViolationError(APIValidationError):
|
|
1255
|
+
"""
|
|
1256
|
+
Raised when API contract is violated.
|
|
1257
|
+
|
|
1258
|
+
Example:
|
|
1259
|
+
>>> raise ContractViolationError(
|
|
1260
|
+
... path="/api/users",
|
|
1261
|
+
... method="POST",
|
|
1262
|
+
... violation_type="removed_required_parameter",
|
|
1263
|
+
... details="Parameter 'email' is required"
|
|
1264
|
+
... )
|
|
1265
|
+
"""
|
|
1266
|
+
|
|
1267
|
+
def __init__(
|
|
1268
|
+
self,
|
|
1269
|
+
path: str,
|
|
1270
|
+
method: str,
|
|
1271
|
+
violation_type: str,
|
|
1272
|
+
details: str,
|
|
1273
|
+
severity: str = "high",
|
|
1274
|
+
context: dict[str, Any] | None = None,
|
|
1275
|
+
):
|
|
1276
|
+
message = f"Contract violation in {method} {path}: {details}"
|
|
1277
|
+
ctx = context or {}
|
|
1278
|
+
ctx.update(
|
|
1279
|
+
{
|
|
1280
|
+
"path": path,
|
|
1281
|
+
"method": method,
|
|
1282
|
+
"violation_type": violation_type,
|
|
1283
|
+
"details": details,
|
|
1284
|
+
"severity": severity,
|
|
1285
|
+
}
|
|
1286
|
+
)
|
|
1287
|
+
|
|
1288
|
+
super().__init__(
|
|
1289
|
+
message=message,
|
|
1290
|
+
code=ErrorCode.CONTRACT_VIOLATION,
|
|
1291
|
+
context=ctx,
|
|
1292
|
+
remediation="Review API contract changes and update implementation",
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
|
|
1296
|
+
class BreakingChangeError(APIValidationError):
|
|
1297
|
+
"""
|
|
1298
|
+
Raised when breaking changes are detected in API contracts.
|
|
1299
|
+
|
|
1300
|
+
Example:
|
|
1301
|
+
>>> raise BreakingChangeError(
|
|
1302
|
+
... breaking_changes=[
|
|
1303
|
+
... {"type": "removed_endpoint", "path": "/api/old"},
|
|
1304
|
+
... {"type": "removed_method", "path": "/api/users", "method": "DELETE"}
|
|
1305
|
+
... ],
|
|
1306
|
+
... allow_breaking_changes=False
|
|
1307
|
+
... )
|
|
1308
|
+
"""
|
|
1309
|
+
|
|
1310
|
+
def __init__(
|
|
1311
|
+
self,
|
|
1312
|
+
breaking_changes: list[dict],
|
|
1313
|
+
allow_breaking_changes: bool = False,
|
|
1314
|
+
context: dict[str, Any] | None = None,
|
|
1315
|
+
):
|
|
1316
|
+
change_count = len(breaking_changes)
|
|
1317
|
+
message = f"{change_count} breaking change{'s' if change_count != 1 else ''} detected"
|
|
1318
|
+
if not allow_breaking_changes:
|
|
1319
|
+
message = f"{message} (not allowed)"
|
|
1320
|
+
|
|
1321
|
+
ctx = context or {}
|
|
1322
|
+
ctx.update(
|
|
1323
|
+
{
|
|
1324
|
+
"breaking_changes": breaking_changes,
|
|
1325
|
+
"breaking_change_count": change_count,
|
|
1326
|
+
"allow_breaking_changes": allow_breaking_changes,
|
|
1327
|
+
}
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
super().__init__(
|
|
1331
|
+
message=message,
|
|
1332
|
+
code=ErrorCode.BREAKING_CHANGE_DETECTED,
|
|
1333
|
+
context=ctx,
|
|
1334
|
+
remediation=(
|
|
1335
|
+
"Review breaking changes and either: "
|
|
1336
|
+
"1) Fix them to maintain backward compatibility, or "
|
|
1337
|
+
"2) Set 'allow_breaking_changes: true' if intentional"
|
|
1338
|
+
),
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1341
|
+
|
|
1342
|
+
class InvalidOpenAPISpecError(APIValidationError):
|
|
1343
|
+
"""
|
|
1344
|
+
Raised when OpenAPI/Swagger specification is invalid.
|
|
1345
|
+
|
|
1346
|
+
Example:
|
|
1347
|
+
>>> raise InvalidOpenAPISpecError(
|
|
1348
|
+
... contract_file="api/openapi.yaml",
|
|
1349
|
+
... details="Not a valid OpenAPI/Swagger specification"
|
|
1350
|
+
... )
|
|
1351
|
+
"""
|
|
1352
|
+
|
|
1353
|
+
def __init__(self, contract_file: str, details: str, context: dict[str, Any] | None = None):
|
|
1354
|
+
message = f"Invalid OpenAPI/Swagger spec: {contract_file}"
|
|
1355
|
+
ctx = context or {}
|
|
1356
|
+
ctx.update({"contract_file": contract_file, "details": details})
|
|
1357
|
+
|
|
1358
|
+
super().__init__(
|
|
1359
|
+
message=message,
|
|
1360
|
+
code=ErrorCode.INVALID_OPENAPI_SPEC,
|
|
1361
|
+
context=ctx,
|
|
1362
|
+
remediation=f"Ensure {contract_file} is a valid OpenAPI/Swagger specification with 'openapi' or 'swagger' field",
|
|
1363
|
+
)
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
# ============================================================================
|
|
1367
|
+
# Performance Testing Errors
|
|
1368
|
+
# ============================================================================
|
|
1369
|
+
|
|
1370
|
+
|
|
1371
|
+
class PerformanceTestError(SolokitError):
|
|
1372
|
+
"""
|
|
1373
|
+
Base class for performance testing errors.
|
|
1374
|
+
|
|
1375
|
+
Example:
|
|
1376
|
+
>>> raise PerformanceTestError(
|
|
1377
|
+
... message="Performance test failed",
|
|
1378
|
+
... code=ErrorCode.PERFORMANCE_TEST_FAILED,
|
|
1379
|
+
... context={"work_item_id": "perf-001"}
|
|
1380
|
+
... )
|
|
1381
|
+
"""
|
|
1382
|
+
|
|
1383
|
+
def __init__(
|
|
1384
|
+
self,
|
|
1385
|
+
message: str,
|
|
1386
|
+
code: ErrorCode = ErrorCode.PERFORMANCE_TEST_FAILED,
|
|
1387
|
+
context: dict[str, Any] | None = None,
|
|
1388
|
+
remediation: str | None = None,
|
|
1389
|
+
cause: Exception | None = None,
|
|
1390
|
+
):
|
|
1391
|
+
super().__init__(
|
|
1392
|
+
message=message,
|
|
1393
|
+
code=code,
|
|
1394
|
+
category=ErrorCategory.VALIDATION,
|
|
1395
|
+
context=context,
|
|
1396
|
+
remediation=remediation,
|
|
1397
|
+
cause=cause,
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
|
|
1401
|
+
class BenchmarkFailedError(PerformanceTestError):
|
|
1402
|
+
"""
|
|
1403
|
+
Raised when a performance benchmark fails to meet requirements.
|
|
1404
|
+
|
|
1405
|
+
Example:
|
|
1406
|
+
>>> raise BenchmarkFailedError(
|
|
1407
|
+
... metric="p95_latency",
|
|
1408
|
+
... actual=150.5,
|
|
1409
|
+
... expected=100.0,
|
|
1410
|
+
... unit="ms"
|
|
1411
|
+
... )
|
|
1412
|
+
"""
|
|
1413
|
+
|
|
1414
|
+
def __init__(self, metric: str, actual: float, expected: float, unit: str = "ms"):
|
|
1415
|
+
message = f"Benchmark failed: {metric} {actual}{unit} exceeds requirement {expected}{unit}"
|
|
1416
|
+
context = {
|
|
1417
|
+
"metric": metric,
|
|
1418
|
+
"actual_value": actual,
|
|
1419
|
+
"expected_value": expected,
|
|
1420
|
+
"unit": unit,
|
|
1421
|
+
"delta": actual - expected,
|
|
1422
|
+
"percentage_over": ((actual / expected - 1) * 100) if expected > 0 else 0,
|
|
1423
|
+
}
|
|
1424
|
+
super().__init__(
|
|
1425
|
+
message=message,
|
|
1426
|
+
code=ErrorCode.BENCHMARK_FAILED,
|
|
1427
|
+
context=context,
|
|
1428
|
+
remediation=f"Optimize performance to meet {metric} requirement of {expected}{unit}",
|
|
1429
|
+
)
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class PerformanceRegressionError(PerformanceTestError):
|
|
1433
|
+
"""
|
|
1434
|
+
Raised when performance regression is detected against baseline.
|
|
1435
|
+
|
|
1436
|
+
Example:
|
|
1437
|
+
>>> raise PerformanceRegressionError(
|
|
1438
|
+
... metric="p50_latency",
|
|
1439
|
+
... current=55.0,
|
|
1440
|
+
... baseline=50.0,
|
|
1441
|
+
... threshold_percent=10.0
|
|
1442
|
+
... )
|
|
1443
|
+
"""
|
|
1444
|
+
|
|
1445
|
+
def __init__(
|
|
1446
|
+
self, metric: str, current: float, baseline: float, threshold_percent: float = 10.0
|
|
1447
|
+
):
|
|
1448
|
+
regression_percent = ((current / baseline - 1) * 100) if baseline > 0 else 0
|
|
1449
|
+
message = (
|
|
1450
|
+
f"Performance regression detected: {metric} increased from "
|
|
1451
|
+
f"{baseline}ms to {current}ms ({regression_percent:.1f}% slower, "
|
|
1452
|
+
f"threshold: {threshold_percent}%)"
|
|
1453
|
+
)
|
|
1454
|
+
context = {
|
|
1455
|
+
"metric": metric,
|
|
1456
|
+
"current_value": current,
|
|
1457
|
+
"baseline_value": baseline,
|
|
1458
|
+
"regression_percent": regression_percent,
|
|
1459
|
+
"threshold_percent": threshold_percent,
|
|
1460
|
+
}
|
|
1461
|
+
super().__init__(
|
|
1462
|
+
message=message,
|
|
1463
|
+
code=ErrorCode.PERFORMANCE_REGRESSION,
|
|
1464
|
+
context=context,
|
|
1465
|
+
remediation="Investigate recent changes that may have caused performance degradation",
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
|
|
1469
|
+
class LoadTestFailedError(PerformanceTestError):
|
|
1470
|
+
"""
|
|
1471
|
+
Raised when load test execution fails.
|
|
1472
|
+
|
|
1473
|
+
Example:
|
|
1474
|
+
>>> raise LoadTestFailedError(
|
|
1475
|
+
... endpoint="http://localhost:8000/api",
|
|
1476
|
+
... details="Connection refused"
|
|
1477
|
+
... )
|
|
1478
|
+
"""
|
|
1479
|
+
|
|
1480
|
+
def __init__(
|
|
1481
|
+
self, endpoint: str, details: str | None = None, context: dict[str, Any] | None = None
|
|
1482
|
+
):
|
|
1483
|
+
message = f"Load test failed for endpoint: {endpoint}"
|
|
1484
|
+
if details:
|
|
1485
|
+
message = f"{message} - {details}"
|
|
1486
|
+
|
|
1487
|
+
ctx = context or {}
|
|
1488
|
+
ctx.update({"endpoint": endpoint, "details": details})
|
|
1489
|
+
|
|
1490
|
+
super().__init__(
|
|
1491
|
+
message=message,
|
|
1492
|
+
code=ErrorCode.LOAD_TEST_FAILED,
|
|
1493
|
+
context=ctx,
|
|
1494
|
+
remediation="Verify the endpoint is accessible and the service is running",
|
|
1495
|
+
)
|
|
1496
|
+
|
|
1497
|
+
|
|
1498
|
+
# ============================================================================
|
|
1499
|
+
# Project Initialization Errors
|
|
1500
|
+
# ============================================================================
|
|
1501
|
+
|
|
1502
|
+
|
|
1503
|
+
class ProjectInitializationError(SolokitError):
|
|
1504
|
+
"""
|
|
1505
|
+
Base exception for project initialization errors.
|
|
1506
|
+
|
|
1507
|
+
Example:
|
|
1508
|
+
>>> raise ProjectInitializationError(
|
|
1509
|
+
... message="Project initialization failed",
|
|
1510
|
+
... context={"reason": "Missing template files"}
|
|
1511
|
+
... )
|
|
1512
|
+
"""
|
|
1513
|
+
|
|
1514
|
+
def __init__(
|
|
1515
|
+
self,
|
|
1516
|
+
message: str,
|
|
1517
|
+
code: ErrorCode = ErrorCode.FILE_OPERATION_FAILED,
|
|
1518
|
+
context: dict[str, Any] | None = None,
|
|
1519
|
+
remediation: str | None = None,
|
|
1520
|
+
cause: Exception | None = None,
|
|
1521
|
+
):
|
|
1522
|
+
super().__init__(
|
|
1523
|
+
message=message,
|
|
1524
|
+
code=code,
|
|
1525
|
+
category=ErrorCategory.SYSTEM,
|
|
1526
|
+
context=context,
|
|
1527
|
+
remediation=remediation,
|
|
1528
|
+
cause=cause,
|
|
1529
|
+
)
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
class DirectoryNotEmptyError(AlreadyExistsError):
|
|
1533
|
+
"""
|
|
1534
|
+
Raised when attempting to initialize in a directory that already has Solokit structure.
|
|
1535
|
+
|
|
1536
|
+
Example:
|
|
1537
|
+
>>> raise DirectoryNotEmptyError(".session")
|
|
1538
|
+
"""
|
|
1539
|
+
|
|
1540
|
+
def __init__(self, directory: str):
|
|
1541
|
+
super().__init__(
|
|
1542
|
+
message=f"Directory '{directory}' already exists",
|
|
1543
|
+
code=ErrorCode.FILE_ALREADY_EXISTS,
|
|
1544
|
+
context={"directory": directory},
|
|
1545
|
+
remediation=f"Remove '{directory}' directory or run initialization in a different location",
|
|
1546
|
+
)
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
class TemplateNotFoundError(FileNotFoundError):
|
|
1550
|
+
"""
|
|
1551
|
+
Raised when a required template file is not found.
|
|
1552
|
+
|
|
1553
|
+
Example:
|
|
1554
|
+
>>> raise TemplateNotFoundError(
|
|
1555
|
+
... template_name="package.json.template",
|
|
1556
|
+
... template_path="/path/to/templates"
|
|
1557
|
+
... )
|
|
1558
|
+
"""
|
|
1559
|
+
|
|
1560
|
+
def __init__(self, template_name: str, template_path: str):
|
|
1561
|
+
super().__init__(file_path=f"{template_path}/{template_name}", file_type="template")
|
|
1562
|
+
self.context["template_name"] = template_name
|
|
1563
|
+
self.template_name = template_name
|
|
1564
|
+
self.template_path = template_path
|
|
1565
|
+
self.remediation = (
|
|
1566
|
+
f"Ensure Solokit is properly installed and template file exists: {template_name}"
|
|
1567
|
+
)
|