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,493 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spec File Validation Module
|
|
4
|
+
|
|
5
|
+
Validates work item specification files for completeness and correctness.
|
|
6
|
+
Ensures specs have all required sections and meet quality standards.
|
|
7
|
+
|
|
8
|
+
Part of Phase 5.7.5: Spec File Validation System
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Optional
|
|
14
|
+
|
|
15
|
+
from solokit.core.error_handlers import log_errors
|
|
16
|
+
from solokit.core.exceptions import (
|
|
17
|
+
FileNotFoundError,
|
|
18
|
+
FileOperationError,
|
|
19
|
+
SpecValidationError,
|
|
20
|
+
)
|
|
21
|
+
from solokit.core.logging_config import get_logger
|
|
22
|
+
from solokit.core.output import get_output
|
|
23
|
+
from solokit.core.types import WorkItemType
|
|
24
|
+
from solokit.work_items.spec_parser import (
|
|
25
|
+
extract_checklist,
|
|
26
|
+
extract_subsection,
|
|
27
|
+
parse_section,
|
|
28
|
+
strip_html_comments,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
output = get_output()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_validation_rules(work_item_type: str) -> dict[str, Any]:
|
|
36
|
+
"""
|
|
37
|
+
Get validation rules for a specific work item type.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
work_item_type: Type of work item (feature, bug, refactor, security, integration_test, deployment)
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Dictionary with required_sections, optional_sections, and special_requirements
|
|
44
|
+
"""
|
|
45
|
+
rules = {
|
|
46
|
+
WorkItemType.FEATURE.value: {
|
|
47
|
+
"required_sections": [
|
|
48
|
+
"Overview",
|
|
49
|
+
"Rationale",
|
|
50
|
+
"Acceptance Criteria",
|
|
51
|
+
"Implementation Details",
|
|
52
|
+
"Testing Strategy",
|
|
53
|
+
],
|
|
54
|
+
"optional_sections": [
|
|
55
|
+
"User Story",
|
|
56
|
+
"Documentation Updates",
|
|
57
|
+
"Dependencies",
|
|
58
|
+
"Estimated Effort",
|
|
59
|
+
],
|
|
60
|
+
"special_requirements": {"acceptance_criteria_min_items": 3},
|
|
61
|
+
},
|
|
62
|
+
WorkItemType.BUG.value: {
|
|
63
|
+
"required_sections": [
|
|
64
|
+
"Description",
|
|
65
|
+
"Steps to Reproduce",
|
|
66
|
+
"Root Cause Analysis",
|
|
67
|
+
"Fix Approach",
|
|
68
|
+
],
|
|
69
|
+
"optional_sections": [
|
|
70
|
+
"Expected Behavior",
|
|
71
|
+
"Actual Behavior",
|
|
72
|
+
"Impact",
|
|
73
|
+
"Testing Strategy",
|
|
74
|
+
"Prevention",
|
|
75
|
+
"Dependencies",
|
|
76
|
+
"Estimated Effort",
|
|
77
|
+
],
|
|
78
|
+
"special_requirements": {},
|
|
79
|
+
},
|
|
80
|
+
WorkItemType.REFACTOR.value: {
|
|
81
|
+
"required_sections": [
|
|
82
|
+
"Overview",
|
|
83
|
+
"Current State",
|
|
84
|
+
"Proposed Refactor",
|
|
85
|
+
"Scope",
|
|
86
|
+
],
|
|
87
|
+
"optional_sections": [
|
|
88
|
+
"Problems with Current Approach",
|
|
89
|
+
"Implementation Plan",
|
|
90
|
+
"Risk Assessment",
|
|
91
|
+
"Success Criteria",
|
|
92
|
+
"Testing Strategy",
|
|
93
|
+
"Dependencies",
|
|
94
|
+
"Estimated Effort",
|
|
95
|
+
],
|
|
96
|
+
"special_requirements": {},
|
|
97
|
+
},
|
|
98
|
+
WorkItemType.SECURITY.value: {
|
|
99
|
+
"required_sections": [
|
|
100
|
+
"Security Issue",
|
|
101
|
+
"Threat Model",
|
|
102
|
+
"Attack Vector",
|
|
103
|
+
"Mitigation Strategy",
|
|
104
|
+
"Compliance",
|
|
105
|
+
],
|
|
106
|
+
"optional_sections": [
|
|
107
|
+
"Severity",
|
|
108
|
+
"Affected Components",
|
|
109
|
+
"Security Testing",
|
|
110
|
+
"Post-Deployment",
|
|
111
|
+
"Testing Strategy",
|
|
112
|
+
"Acceptance Criteria",
|
|
113
|
+
"Dependencies",
|
|
114
|
+
"Estimated Effort",
|
|
115
|
+
],
|
|
116
|
+
"special_requirements": {},
|
|
117
|
+
},
|
|
118
|
+
WorkItemType.INTEGRATION_TEST.value: {
|
|
119
|
+
"required_sections": [
|
|
120
|
+
"Scope",
|
|
121
|
+
"Test Scenarios",
|
|
122
|
+
"Performance Benchmarks",
|
|
123
|
+
"Environment Requirements",
|
|
124
|
+
"Acceptance Criteria",
|
|
125
|
+
],
|
|
126
|
+
"optional_sections": ["API Contracts", "Dependencies", "Estimated Effort"],
|
|
127
|
+
"special_requirements": {
|
|
128
|
+
"test_scenarios_min": 1,
|
|
129
|
+
"acceptance_criteria_min_items": 3,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
WorkItemType.DEPLOYMENT.value: {
|
|
133
|
+
"required_sections": [
|
|
134
|
+
"Deployment Scope",
|
|
135
|
+
"Deployment Procedure",
|
|
136
|
+
"Rollback Procedure",
|
|
137
|
+
"Smoke Tests",
|
|
138
|
+
"Acceptance Criteria",
|
|
139
|
+
],
|
|
140
|
+
"optional_sections": [
|
|
141
|
+
"Environment Configuration",
|
|
142
|
+
"Monitoring & Alerting",
|
|
143
|
+
"Post-Deployment Monitoring Period",
|
|
144
|
+
"Dependencies",
|
|
145
|
+
"Estimated Effort",
|
|
146
|
+
],
|
|
147
|
+
"special_requirements": {
|
|
148
|
+
"deployment_procedure_subsections": [
|
|
149
|
+
"Pre-Deployment Checklist",
|
|
150
|
+
"Deployment Steps",
|
|
151
|
+
"Post-Deployment Steps",
|
|
152
|
+
],
|
|
153
|
+
"rollback_procedure_subsections": [
|
|
154
|
+
"Rollback Triggers",
|
|
155
|
+
"Rollback Steps",
|
|
156
|
+
],
|
|
157
|
+
"smoke_tests_min": 1,
|
|
158
|
+
"acceptance_criteria_min_items": 3,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return rules.get(
|
|
164
|
+
work_item_type,
|
|
165
|
+
{"required_sections": [], "optional_sections": [], "special_requirements": {}},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def check_required_sections(spec_content: str, work_item_type: str) -> list[str]:
|
|
170
|
+
"""
|
|
171
|
+
Check if all required sections are present and non-empty.
|
|
172
|
+
|
|
173
|
+
Args:
|
|
174
|
+
spec_content: Full spec file content
|
|
175
|
+
work_item_type: Type of work item
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
List of error messages (empty if all checks pass)
|
|
179
|
+
"""
|
|
180
|
+
errors = []
|
|
181
|
+
rules = get_validation_rules(work_item_type)
|
|
182
|
+
required_sections = rules.get("required_sections", [])
|
|
183
|
+
|
|
184
|
+
# Strip HTML comments before parsing
|
|
185
|
+
clean_content = strip_html_comments(spec_content)
|
|
186
|
+
|
|
187
|
+
for section_name in required_sections:
|
|
188
|
+
section_content = parse_section(clean_content, section_name)
|
|
189
|
+
|
|
190
|
+
if section_content is None:
|
|
191
|
+
errors.append(f"Missing required section: '{section_name}'")
|
|
192
|
+
elif not section_content.strip():
|
|
193
|
+
errors.append(f"Required section '{section_name}' is empty")
|
|
194
|
+
|
|
195
|
+
return errors
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def check_acceptance_criteria(spec_content: str, min_items: int = 3) -> Optional[str]:
|
|
199
|
+
"""
|
|
200
|
+
Check if Acceptance Criteria section has enough items.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
spec_content: Full spec file content
|
|
204
|
+
min_items: Minimum number of acceptance criteria items required (default: 3)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Error message if validation fails, None otherwise
|
|
208
|
+
"""
|
|
209
|
+
clean_content = strip_html_comments(spec_content)
|
|
210
|
+
ac_section = parse_section(clean_content, "Acceptance Criteria")
|
|
211
|
+
|
|
212
|
+
if ac_section is None:
|
|
213
|
+
return None # Section doesn't exist, will be caught by check_required_sections
|
|
214
|
+
|
|
215
|
+
checklist = extract_checklist(ac_section)
|
|
216
|
+
|
|
217
|
+
if len(checklist) < min_items:
|
|
218
|
+
return f"Acceptance Criteria must have at least {min_items} items (found {len(checklist)})"
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_test_scenarios(spec_content: str, min_scenarios: int = 1) -> Optional[str]:
|
|
224
|
+
"""
|
|
225
|
+
Check if Test Scenarios section has enough scenarios.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
spec_content: Full spec file content
|
|
229
|
+
min_scenarios: Minimum number of test scenarios required (default: 1)
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Error message if validation fails, None otherwise
|
|
233
|
+
"""
|
|
234
|
+
clean_content = strip_html_comments(spec_content)
|
|
235
|
+
scenarios_section = parse_section(clean_content, "Test Scenarios")
|
|
236
|
+
|
|
237
|
+
if scenarios_section is None:
|
|
238
|
+
return None # Will be caught by check_required_sections
|
|
239
|
+
|
|
240
|
+
# Count H3 headings that match "Scenario N:" pattern
|
|
241
|
+
scenario_count = len(re.findall(r"###\s+Scenario\s+\d+:", scenarios_section, re.IGNORECASE))
|
|
242
|
+
|
|
243
|
+
if scenario_count < min_scenarios:
|
|
244
|
+
return f"Test Scenarios must have at least {min_scenarios} scenario(s) (found {scenario_count})"
|
|
245
|
+
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def check_smoke_tests(spec_content: str, min_tests: int = 1) -> Optional[str]:
|
|
250
|
+
"""
|
|
251
|
+
Check if Smoke Tests section has enough test cases.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
spec_content: Full spec file content
|
|
255
|
+
min_tests: Minimum number of smoke tests required (default: 1)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
Error message if validation fails, None otherwise
|
|
259
|
+
"""
|
|
260
|
+
clean_content = strip_html_comments(spec_content)
|
|
261
|
+
smoke_tests_section = parse_section(clean_content, "Smoke Tests")
|
|
262
|
+
|
|
263
|
+
if smoke_tests_section is None:
|
|
264
|
+
return None # Will be caught by check_required_sections
|
|
265
|
+
|
|
266
|
+
# Count H3 headings that match "Test N:" pattern
|
|
267
|
+
test_count = len(re.findall(r"###\s+Test\s+\d+:", smoke_tests_section, re.IGNORECASE))
|
|
268
|
+
|
|
269
|
+
if test_count < min_tests:
|
|
270
|
+
return f"Smoke Tests must have at least {min_tests} test(s) (found {test_count})"
|
|
271
|
+
|
|
272
|
+
return None
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def check_deployment_subsections(spec_content: str) -> list[str]:
|
|
276
|
+
"""
|
|
277
|
+
Check if Deployment Procedure has all required subsections.
|
|
278
|
+
|
|
279
|
+
Args:
|
|
280
|
+
spec_content: Full spec file content
|
|
281
|
+
|
|
282
|
+
Returns:
|
|
283
|
+
List of error messages (empty if all checks pass)
|
|
284
|
+
"""
|
|
285
|
+
errors = []
|
|
286
|
+
clean_content = strip_html_comments(spec_content)
|
|
287
|
+
deployment_section = parse_section(clean_content, "Deployment Procedure")
|
|
288
|
+
|
|
289
|
+
if deployment_section is None:
|
|
290
|
+
return [] # Will be caught by check_required_sections
|
|
291
|
+
|
|
292
|
+
required_subsections = [
|
|
293
|
+
"Pre-Deployment Checklist",
|
|
294
|
+
"Deployment Steps",
|
|
295
|
+
"Post-Deployment Steps",
|
|
296
|
+
]
|
|
297
|
+
|
|
298
|
+
for subsection_name in required_subsections:
|
|
299
|
+
subsection_content = extract_subsection(deployment_section, subsection_name)
|
|
300
|
+
if subsection_content is None:
|
|
301
|
+
errors.append(f"Deployment Procedure missing required subsection: '{subsection_name}'")
|
|
302
|
+
elif not subsection_content.strip():
|
|
303
|
+
errors.append(f"Deployment Procedure subsection '{subsection_name}' is empty")
|
|
304
|
+
|
|
305
|
+
return errors
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def check_rollback_subsections(spec_content: str) -> list[str]:
|
|
309
|
+
"""
|
|
310
|
+
Check if Rollback Procedure has all required subsections.
|
|
311
|
+
|
|
312
|
+
Args:
|
|
313
|
+
spec_content: Full spec file content
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
List of error messages (empty if all checks pass)
|
|
317
|
+
"""
|
|
318
|
+
errors = []
|
|
319
|
+
clean_content = strip_html_comments(spec_content)
|
|
320
|
+
rollback_section = parse_section(clean_content, "Rollback Procedure")
|
|
321
|
+
|
|
322
|
+
if rollback_section is None:
|
|
323
|
+
return [] # Will be caught by check_required_sections
|
|
324
|
+
|
|
325
|
+
required_subsections = ["Rollback Triggers", "Rollback Steps"]
|
|
326
|
+
|
|
327
|
+
for subsection_name in required_subsections:
|
|
328
|
+
subsection_content = extract_subsection(rollback_section, subsection_name)
|
|
329
|
+
if subsection_content is None:
|
|
330
|
+
errors.append(f"Rollback Procedure missing required subsection: '{subsection_name}'")
|
|
331
|
+
elif not subsection_content.strip():
|
|
332
|
+
errors.append(f"Rollback Procedure subsection '{subsection_name}' is empty")
|
|
333
|
+
|
|
334
|
+
return errors
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
@log_errors()
|
|
338
|
+
def validate_spec_file(work_item_id: str, work_item_type: str) -> None:
|
|
339
|
+
"""
|
|
340
|
+
Validate a work item specification file for completeness and correctness.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
work_item_id: ID of the work item
|
|
344
|
+
work_item_type: Type of work item (feature, bug, refactor, security, integration_test, deployment)
|
|
345
|
+
|
|
346
|
+
Raises:
|
|
347
|
+
FileNotFoundError: If spec file doesn't exist
|
|
348
|
+
FileOperationError: If spec file cannot be read
|
|
349
|
+
SpecValidationError: If spec validation fails (contains list of validation errors)
|
|
350
|
+
"""
|
|
351
|
+
# Try to load work items to get spec_file path
|
|
352
|
+
# If work_items.json doesn't exist, fallback to default pattern (for backwards compatibility/tests)
|
|
353
|
+
import json
|
|
354
|
+
|
|
355
|
+
work_items_file = Path(".session/tracking/work_items.json")
|
|
356
|
+
spec_file_path = None
|
|
357
|
+
|
|
358
|
+
if work_items_file.exists():
|
|
359
|
+
# Load from work_items.json (preferred method)
|
|
360
|
+
try:
|
|
361
|
+
with open(work_items_file) as f:
|
|
362
|
+
work_items_data = json.load(f)
|
|
363
|
+
|
|
364
|
+
if work_item_id in work_items_data.get("work_items", {}):
|
|
365
|
+
work_item = work_items_data["work_items"][work_item_id]
|
|
366
|
+
spec_file_path = work_item.get("spec_file")
|
|
367
|
+
except (OSError, json.JSONDecodeError):
|
|
368
|
+
# If loading fails, fallback to default pattern
|
|
369
|
+
pass
|
|
370
|
+
|
|
371
|
+
# Fallback to default pattern if not found in work_items.json
|
|
372
|
+
if not spec_file_path:
|
|
373
|
+
spec_file_path = f".session/specs/{work_item_id}.md"
|
|
374
|
+
|
|
375
|
+
spec_path = Path(spec_file_path)
|
|
376
|
+
|
|
377
|
+
if not spec_path.exists():
|
|
378
|
+
raise FileNotFoundError(file_path=str(spec_path), file_type="spec")
|
|
379
|
+
|
|
380
|
+
# Read spec content
|
|
381
|
+
try:
|
|
382
|
+
spec_content = spec_path.read_text(encoding="utf-8")
|
|
383
|
+
except OSError as e:
|
|
384
|
+
raise FileOperationError(
|
|
385
|
+
operation="read", file_path=str(spec_path), details=str(e), cause=e
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
# Collect all errors
|
|
389
|
+
errors = []
|
|
390
|
+
|
|
391
|
+
# Check required sections
|
|
392
|
+
errors.extend(check_required_sections(spec_content, work_item_type))
|
|
393
|
+
|
|
394
|
+
# Get special requirements for this work item type
|
|
395
|
+
rules = get_validation_rules(work_item_type)
|
|
396
|
+
special_requirements = rules.get("special_requirements", {})
|
|
397
|
+
|
|
398
|
+
# Check acceptance criteria (if required)
|
|
399
|
+
if "acceptance_criteria_min_items" in special_requirements:
|
|
400
|
+
min_items = special_requirements["acceptance_criteria_min_items"]
|
|
401
|
+
ac_error = check_acceptance_criteria(spec_content, min_items)
|
|
402
|
+
if ac_error:
|
|
403
|
+
errors.append(ac_error)
|
|
404
|
+
|
|
405
|
+
# Check test scenarios (for integration_test)
|
|
406
|
+
if "test_scenarios_min" in special_requirements:
|
|
407
|
+
min_scenarios = special_requirements["test_scenarios_min"]
|
|
408
|
+
scenarios_error = check_test_scenarios(spec_content, min_scenarios)
|
|
409
|
+
if scenarios_error:
|
|
410
|
+
errors.append(scenarios_error)
|
|
411
|
+
|
|
412
|
+
# Check smoke tests (for deployment)
|
|
413
|
+
if "smoke_tests_min" in special_requirements:
|
|
414
|
+
min_tests = special_requirements["smoke_tests_min"]
|
|
415
|
+
smoke_error = check_smoke_tests(spec_content, min_tests)
|
|
416
|
+
if smoke_error:
|
|
417
|
+
errors.append(smoke_error)
|
|
418
|
+
|
|
419
|
+
# Check deployment subsections (for deployment)
|
|
420
|
+
if "deployment_procedure_subsections" in special_requirements:
|
|
421
|
+
errors.extend(check_deployment_subsections(spec_content))
|
|
422
|
+
|
|
423
|
+
# Check rollback subsections (for deployment)
|
|
424
|
+
if "rollback_procedure_subsections" in special_requirements:
|
|
425
|
+
errors.extend(check_rollback_subsections(spec_content))
|
|
426
|
+
|
|
427
|
+
# Raise SpecValidationError if any validation errors found
|
|
428
|
+
if errors:
|
|
429
|
+
raise SpecValidationError(work_item_id=work_item_id, errors=errors)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def format_validation_report(
|
|
433
|
+
work_item_id: str, work_item_type: str, validation_error: Optional[SpecValidationError] = None
|
|
434
|
+
) -> str:
|
|
435
|
+
"""
|
|
436
|
+
Format validation errors into a human-readable report.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
work_item_id: ID of the work item
|
|
440
|
+
work_item_type: Type of work item
|
|
441
|
+
validation_error: SpecValidationError containing validation errors, None if valid
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
Formatted validation report
|
|
445
|
+
"""
|
|
446
|
+
if not validation_error:
|
|
447
|
+
return f"✅ Spec file for '{work_item_id}' ({work_item_type}) is valid"
|
|
448
|
+
|
|
449
|
+
errors = validation_error.context.get("validation_errors", [])
|
|
450
|
+
|
|
451
|
+
report = f"❌ Spec file for '{work_item_id}' ({work_item_type}) has validation errors:\n\n"
|
|
452
|
+
|
|
453
|
+
for i, error in enumerate(errors, 1):
|
|
454
|
+
report += f"{i}. {error}\n"
|
|
455
|
+
|
|
456
|
+
report += "\n💡 Suggestions:\n"
|
|
457
|
+
report += f"- Review the template at templates/{work_item_type}_spec.md\n"
|
|
458
|
+
report += "- Check docs/spec-template-structure.md for section requirements\n"
|
|
459
|
+
report += f"- Edit .session/specs/{work_item_id}.md to add missing sections\n"
|
|
460
|
+
|
|
461
|
+
return report
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
# CLI interface for testing
|
|
465
|
+
if __name__ == "__main__":
|
|
466
|
+
import sys
|
|
467
|
+
|
|
468
|
+
if len(sys.argv) < 3:
|
|
469
|
+
output.info("Usage: python3 spec_validator.py <work_item_id> <work_item_type>")
|
|
470
|
+
output.info("Example: python3 spec_validator.py feature_websocket_notifications feature")
|
|
471
|
+
sys.exit(1)
|
|
472
|
+
|
|
473
|
+
work_item_id = sys.argv[1]
|
|
474
|
+
work_item_type = sys.argv[2]
|
|
475
|
+
|
|
476
|
+
try:
|
|
477
|
+
logger.info("Validating spec file for %s (%s)", work_item_id, work_item_type)
|
|
478
|
+
validate_spec_file(work_item_id, work_item_type)
|
|
479
|
+
report = format_validation_report(work_item_id, work_item_type)
|
|
480
|
+
output.info(report)
|
|
481
|
+
logger.info("Spec validation successful")
|
|
482
|
+
sys.exit(0)
|
|
483
|
+
except SpecValidationError as e:
|
|
484
|
+
logger.warning("Spec validation failed: %s", e.message)
|
|
485
|
+
report = format_validation_report(work_item_id, work_item_type, e)
|
|
486
|
+
output.info(report)
|
|
487
|
+
sys.exit(e.exit_code)
|
|
488
|
+
except (FileNotFoundError, FileOperationError) as e:
|
|
489
|
+
logger.error("File operation error during validation", exc_info=True)
|
|
490
|
+
output.error(f"Error: {e.message}")
|
|
491
|
+
if e.remediation:
|
|
492
|
+
output.info(f"Remediation: {e.remediation}")
|
|
493
|
+
sys.exit(e.exit_code)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Work Item Updater - Update operations for work items.
|
|
4
|
+
|
|
5
|
+
Handles field updates with validation, both interactive and non-interactive.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
from solokit.core.error_handlers import log_errors
|
|
14
|
+
from solokit.core.exceptions import (
|
|
15
|
+
ErrorCode,
|
|
16
|
+
FileOperationError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
WorkItemNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
from solokit.core.logging_config import get_logger
|
|
21
|
+
from solokit.core.types import Priority, WorkItemStatus
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .repository import WorkItemRepository
|
|
25
|
+
from .validator import WorkItemValidator
|
|
26
|
+
from solokit.core.output import get_output
|
|
27
|
+
|
|
28
|
+
logger = get_logger(__name__)
|
|
29
|
+
output = get_output()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class WorkItemUpdater:
|
|
33
|
+
"""Handles work item update operations"""
|
|
34
|
+
|
|
35
|
+
PRIORITIES = Priority.values()
|
|
36
|
+
|
|
37
|
+
def __init__(self, repository: WorkItemRepository, validator: WorkItemValidator | None = None):
|
|
38
|
+
"""Initialize updater with repository and optional validator
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
repository: WorkItemRepository instance for data access
|
|
42
|
+
validator: Optional WorkItemValidator for validation
|
|
43
|
+
"""
|
|
44
|
+
self.repository = repository
|
|
45
|
+
self.validator = validator
|
|
46
|
+
|
|
47
|
+
@log_errors()
|
|
48
|
+
def update(self, work_id: str, **updates: Any) -> None:
|
|
49
|
+
"""Update work item fields
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
work_id: ID of the work item to update
|
|
53
|
+
**updates: Field updates (status, priority, milestone, add_dependency, remove_dependency)
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
FileOperationError: If work_items.json doesn't exist
|
|
57
|
+
WorkItemNotFoundError: If work item doesn't exist
|
|
58
|
+
ValidationError: If invalid status or priority provided
|
|
59
|
+
"""
|
|
60
|
+
items = self.repository.get_all_work_items()
|
|
61
|
+
|
|
62
|
+
if not items:
|
|
63
|
+
raise FileOperationError(
|
|
64
|
+
operation="read",
|
|
65
|
+
file_path=str(self.repository.work_items_file),
|
|
66
|
+
details="No work items found",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if work_id not in items:
|
|
70
|
+
raise WorkItemNotFoundError(work_id)
|
|
71
|
+
|
|
72
|
+
item = items[work_id]
|
|
73
|
+
changes = []
|
|
74
|
+
|
|
75
|
+
# Apply updates
|
|
76
|
+
for field, value in updates.items():
|
|
77
|
+
if field == "status":
|
|
78
|
+
if value not in WorkItemStatus.values():
|
|
79
|
+
logger.warning("Invalid status: %s", value)
|
|
80
|
+
raise ValidationError(
|
|
81
|
+
message=f"Invalid status: {value}",
|
|
82
|
+
code=ErrorCode.INVALID_STATUS,
|
|
83
|
+
context={"status": value, "valid_statuses": WorkItemStatus.values()},
|
|
84
|
+
remediation=f"Valid statuses: {', '.join(WorkItemStatus.values())}",
|
|
85
|
+
)
|
|
86
|
+
old_value = item["status"]
|
|
87
|
+
item["status"] = value
|
|
88
|
+
changes.append(f" status: {old_value} → {value}")
|
|
89
|
+
|
|
90
|
+
elif field == "priority":
|
|
91
|
+
if value not in self.PRIORITIES:
|
|
92
|
+
logger.warning("Invalid priority: %s", value)
|
|
93
|
+
raise ValidationError(
|
|
94
|
+
message=f"Invalid priority: {value}",
|
|
95
|
+
code=ErrorCode.INVALID_PRIORITY,
|
|
96
|
+
context={"priority": value, "valid_priorities": self.PRIORITIES},
|
|
97
|
+
remediation=f"Valid priorities: {', '.join(self.PRIORITIES)}",
|
|
98
|
+
)
|
|
99
|
+
old_value = item["priority"]
|
|
100
|
+
item["priority"] = value
|
|
101
|
+
changes.append(f" priority: {old_value} → {value}")
|
|
102
|
+
|
|
103
|
+
elif field == "milestone":
|
|
104
|
+
old_value = item.get("milestone", "(none)")
|
|
105
|
+
item["milestone"] = value
|
|
106
|
+
changes.append(f" milestone: {old_value} → {value}")
|
|
107
|
+
|
|
108
|
+
elif field == "add_dependency":
|
|
109
|
+
# Support comma-separated list of dependencies
|
|
110
|
+
deps = item.get("dependencies", [])
|
|
111
|
+
dep_ids = [d.strip() for d in value.split(",") if d.strip()]
|
|
112
|
+
|
|
113
|
+
for dep_id in dep_ids:
|
|
114
|
+
if dep_id not in deps:
|
|
115
|
+
if self.repository.work_item_exists(dep_id):
|
|
116
|
+
deps.append(dep_id)
|
|
117
|
+
changes.append(f" added dependency: {dep_id}")
|
|
118
|
+
else:
|
|
119
|
+
logger.warning("Dependency '%s' not found", dep_id)
|
|
120
|
+
raise WorkItemNotFoundError(dep_id)
|
|
121
|
+
|
|
122
|
+
item["dependencies"] = deps
|
|
123
|
+
|
|
124
|
+
elif field == "remove_dependency":
|
|
125
|
+
# Support comma-separated list of dependencies
|
|
126
|
+
deps = item.get("dependencies", [])
|
|
127
|
+
dep_ids = [d.strip() for d in value.split(",") if d.strip()]
|
|
128
|
+
|
|
129
|
+
for dep_id in dep_ids:
|
|
130
|
+
if dep_id in deps:
|
|
131
|
+
deps.remove(dep_id)
|
|
132
|
+
changes.append(f" removed dependency: {dep_id}")
|
|
133
|
+
|
|
134
|
+
item["dependencies"] = deps
|
|
135
|
+
|
|
136
|
+
if not changes:
|
|
137
|
+
logger.info("No changes made to %s", work_id)
|
|
138
|
+
raise ValidationError(
|
|
139
|
+
message="No changes made",
|
|
140
|
+
code=ErrorCode.MISSING_REQUIRED_FIELD,
|
|
141
|
+
context={"work_item_id": work_id},
|
|
142
|
+
remediation="Provide valid field updates",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# Record update
|
|
146
|
+
item.setdefault("update_history", []).append(
|
|
147
|
+
{"timestamp": datetime.now().isoformat(), "changes": changes}
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Save - pass the entire updated item as a dict of updates
|
|
151
|
+
self.repository.update_work_item(work_id, item)
|
|
152
|
+
|
|
153
|
+
# Success - user-facing output
|
|
154
|
+
output.info(f"\nUpdated {work_id}:")
|
|
155
|
+
for change in changes:
|
|
156
|
+
output.info(change)
|
|
157
|
+
output.info("")
|