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,838 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Spec Markdown Parsing Module
|
|
4
|
+
|
|
5
|
+
Parses work item specification files from .session/specs/ directory.
|
|
6
|
+
Extracts structured data from markdown for use by validators, runners, and quality gates.
|
|
7
|
+
|
|
8
|
+
Part of Phase 5.7.2: Spec File First Architecture
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from solokit.core.error_handlers import log_errors
|
|
20
|
+
from solokit.core.exceptions import (
|
|
21
|
+
ErrorCode,
|
|
22
|
+
ValidationError,
|
|
23
|
+
)
|
|
24
|
+
from solokit.core.exceptions import (
|
|
25
|
+
FileNotFoundError as SolokitFileNotFoundError,
|
|
26
|
+
)
|
|
27
|
+
from solokit.core.logging_config import get_logger
|
|
28
|
+
from solokit.core.output import get_output
|
|
29
|
+
from solokit.core.types import WorkItemType
|
|
30
|
+
|
|
31
|
+
logger = get_logger(__name__)
|
|
32
|
+
output = get_output()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def strip_html_comments(content: str) -> str:
|
|
36
|
+
"""
|
|
37
|
+
Remove all HTML comments from content.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
content: Markdown content with possible HTML comments
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Content with all <!-- ... --> comments removed
|
|
44
|
+
"""
|
|
45
|
+
# Remove HTML comments (including multiline)
|
|
46
|
+
return re.sub(r"<!--.*?-->", "", content, flags=re.DOTALL)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_section(content: str, section_name: str) -> str | None:
|
|
50
|
+
"""
|
|
51
|
+
Extract content between '## SectionName' and next '##' heading.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
content: Full markdown content (should have HTML comments stripped first)
|
|
55
|
+
section_name: Name of section to extract (case-insensitive)
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Section content (excluding heading) or None if not found
|
|
59
|
+
"""
|
|
60
|
+
lines = content.split("\n")
|
|
61
|
+
in_section = False
|
|
62
|
+
section_content = []
|
|
63
|
+
|
|
64
|
+
for line in lines:
|
|
65
|
+
# Check if this is an H2 heading
|
|
66
|
+
if line.startswith("## "):
|
|
67
|
+
heading = line[3:].strip()
|
|
68
|
+
|
|
69
|
+
# Found our target section
|
|
70
|
+
if heading.lower() == section_name.lower():
|
|
71
|
+
in_section = True
|
|
72
|
+
continue
|
|
73
|
+
# Found next section, stop collecting
|
|
74
|
+
elif in_section:
|
|
75
|
+
break
|
|
76
|
+
|
|
77
|
+
# Collect lines while in target section
|
|
78
|
+
if in_section:
|
|
79
|
+
section_content.append(line)
|
|
80
|
+
|
|
81
|
+
# Return None if section not found
|
|
82
|
+
if not section_content:
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
# Return trimmed content
|
|
86
|
+
return "\n".join(section_content).strip()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def extract_subsection(section_content: str, subsection_name: str) -> str | None:
|
|
90
|
+
"""
|
|
91
|
+
Extract content under '### SubsectionName' within a section.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
section_content: Content of a section (from parse_section)
|
|
95
|
+
subsection_name: Name of subsection to extract (case-insensitive)
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
Subsection content (excluding heading) or None if not found
|
|
99
|
+
"""
|
|
100
|
+
if not section_content:
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
lines = section_content.split("\n")
|
|
104
|
+
in_subsection = False
|
|
105
|
+
subsection_content = []
|
|
106
|
+
|
|
107
|
+
for line in lines:
|
|
108
|
+
# Check if this is an H3 heading
|
|
109
|
+
if line.startswith("### "):
|
|
110
|
+
heading = line[4:].strip()
|
|
111
|
+
|
|
112
|
+
# Found our target subsection
|
|
113
|
+
if heading.lower() == subsection_name.lower():
|
|
114
|
+
in_subsection = True
|
|
115
|
+
continue
|
|
116
|
+
# Found next subsection, stop collecting
|
|
117
|
+
elif in_subsection:
|
|
118
|
+
break
|
|
119
|
+
# H2 heading means we've left the parent section
|
|
120
|
+
elif line.startswith("## ") and in_subsection:
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
# Collect lines while in target subsection
|
|
124
|
+
if in_subsection:
|
|
125
|
+
subsection_content.append(line)
|
|
126
|
+
|
|
127
|
+
# Return None if subsection not found
|
|
128
|
+
if not subsection_content:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
# Return trimmed content
|
|
132
|
+
return "\n".join(subsection_content).strip()
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def extract_checklist(content: str) -> list[dict[str, Any]]:
|
|
136
|
+
"""
|
|
137
|
+
Extract checklist items from markdown.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
content: Markdown content containing checklist items
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
List of dicts with 'text' and 'checked' keys:
|
|
144
|
+
[
|
|
145
|
+
{"text": "Item text", "checked": False},
|
|
146
|
+
{"text": "Checked item", "checked": True},
|
|
147
|
+
...
|
|
148
|
+
]
|
|
149
|
+
"""
|
|
150
|
+
if not content:
|
|
151
|
+
return []
|
|
152
|
+
|
|
153
|
+
checklist = []
|
|
154
|
+
for line in content.split("\n"):
|
|
155
|
+
# Match checklist pattern: - [ ] or - [x]
|
|
156
|
+
match = re.match(r"-\s+\[([ xX])\]\s+(.+)", line.strip())
|
|
157
|
+
if match:
|
|
158
|
+
checked = match.group(1).lower() == "x"
|
|
159
|
+
text = match.group(2).strip()
|
|
160
|
+
checklist.append({"text": text, "checked": checked})
|
|
161
|
+
|
|
162
|
+
return checklist
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def extract_code_blocks(content: str) -> list[dict[str, str]]:
|
|
166
|
+
"""
|
|
167
|
+
Extract all code blocks from content.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
content: Markdown content with code blocks
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
List of dicts with 'language' and 'code' keys:
|
|
174
|
+
[
|
|
175
|
+
{"language": "typescript", "code": "..."},
|
|
176
|
+
{"language": "bash", "code": "..."},
|
|
177
|
+
...
|
|
178
|
+
]
|
|
179
|
+
"""
|
|
180
|
+
if not content:
|
|
181
|
+
return []
|
|
182
|
+
|
|
183
|
+
code_blocks = []
|
|
184
|
+
|
|
185
|
+
# Pattern to match ```language\n...\n```
|
|
186
|
+
pattern = r"```(\w+)?\n(.*?)```"
|
|
187
|
+
matches = re.finditer(pattern, content, flags=re.DOTALL)
|
|
188
|
+
|
|
189
|
+
for match in matches:
|
|
190
|
+
language = match.group(1) or "text" # Default to 'text' if no language specified
|
|
191
|
+
code = match.group(2).strip()
|
|
192
|
+
code_blocks.append({"language": language, "code": code})
|
|
193
|
+
|
|
194
|
+
return code_blocks
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def extract_list_items(content: str) -> list[str]:
|
|
198
|
+
"""
|
|
199
|
+
Extract bullet point or numbered list items from content.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
content: Markdown content with list items
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
List of item text (without bullets/numbers)
|
|
206
|
+
"""
|
|
207
|
+
if not content:
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
items = []
|
|
211
|
+
for line in content.split("\n"):
|
|
212
|
+
# Match bullet points (-, *, +) or numbered lists (1., 2., etc.)
|
|
213
|
+
match = re.match(r"^[\s]*(?:[-*+]|\d+\.)\s+(.+)", line)
|
|
214
|
+
if match:
|
|
215
|
+
items.append(match.group(1).strip())
|
|
216
|
+
|
|
217
|
+
return items
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ============================================================================
|
|
221
|
+
# Work Item Type-Specific Parsers
|
|
222
|
+
# ============================================================================
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def parse_feature_spec(content: str) -> dict[str, Any]:
|
|
226
|
+
"""
|
|
227
|
+
Parse feature specification.
|
|
228
|
+
|
|
229
|
+
Sections:
|
|
230
|
+
- Overview
|
|
231
|
+
- User Story
|
|
232
|
+
- Rationale
|
|
233
|
+
- Acceptance Criteria
|
|
234
|
+
- Implementation Details (with subsections: Approach, Components Affected, API Changes, Database Changes)
|
|
235
|
+
- Testing Strategy
|
|
236
|
+
- Documentation Updates
|
|
237
|
+
- Dependencies
|
|
238
|
+
- Estimated Effort
|
|
239
|
+
"""
|
|
240
|
+
# Strip HTML comments first
|
|
241
|
+
content = strip_html_comments(content)
|
|
242
|
+
|
|
243
|
+
result: dict[str, Any] = {}
|
|
244
|
+
|
|
245
|
+
# Extract main sections
|
|
246
|
+
result["overview"] = parse_section(content, "Overview")
|
|
247
|
+
result["user_story"] = parse_section(content, "User Story")
|
|
248
|
+
result["rationale"] = parse_section(content, "Rationale")
|
|
249
|
+
|
|
250
|
+
# Acceptance Criteria - extract as checklist
|
|
251
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
252
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
253
|
+
|
|
254
|
+
# Implementation Details with subsections
|
|
255
|
+
impl_section = parse_section(content, "Implementation Details")
|
|
256
|
+
if impl_section:
|
|
257
|
+
result["implementation_details"] = {
|
|
258
|
+
"approach": extract_subsection(impl_section, "Approach"),
|
|
259
|
+
"llm_processing_config": extract_subsection(
|
|
260
|
+
impl_section, "LLM/Processing Configuration"
|
|
261
|
+
),
|
|
262
|
+
"components_affected": extract_subsection(impl_section, "Components Affected"),
|
|
263
|
+
"api_changes": extract_subsection(impl_section, "API Changes"),
|
|
264
|
+
"database_changes": extract_subsection(impl_section, "Database Changes"),
|
|
265
|
+
"code_blocks": extract_code_blocks(impl_section),
|
|
266
|
+
}
|
|
267
|
+
else:
|
|
268
|
+
result["implementation_details"] = None
|
|
269
|
+
|
|
270
|
+
# Testing Strategy
|
|
271
|
+
result["testing_strategy"] = parse_section(content, "Testing Strategy")
|
|
272
|
+
|
|
273
|
+
# Documentation Updates - extract as checklist
|
|
274
|
+
doc_section = parse_section(content, "Documentation Updates")
|
|
275
|
+
result["documentation_updates"] = extract_checklist(doc_section) if doc_section else []
|
|
276
|
+
|
|
277
|
+
# Dependencies
|
|
278
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
279
|
+
|
|
280
|
+
# Estimated Effort
|
|
281
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
282
|
+
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def parse_bug_spec(content: str) -> dict[str, Any]:
|
|
287
|
+
"""
|
|
288
|
+
Parse bug specification.
|
|
289
|
+
|
|
290
|
+
Sections:
|
|
291
|
+
- Description
|
|
292
|
+
- Steps to Reproduce
|
|
293
|
+
- Expected Behavior
|
|
294
|
+
- Actual Behavior
|
|
295
|
+
- Impact
|
|
296
|
+
- Root Cause Analysis (with subsections: Investigation, Root Cause, Why It Happened)
|
|
297
|
+
- Fix Approach
|
|
298
|
+
- Prevention
|
|
299
|
+
- Testing Strategy
|
|
300
|
+
- Dependencies
|
|
301
|
+
- Estimated Effort
|
|
302
|
+
"""
|
|
303
|
+
# Strip HTML comments first
|
|
304
|
+
content = strip_html_comments(content)
|
|
305
|
+
|
|
306
|
+
result: dict[str, Any] = {}
|
|
307
|
+
|
|
308
|
+
# Extract main sections
|
|
309
|
+
result["description"] = parse_section(content, "Description")
|
|
310
|
+
result["steps_to_reproduce"] = parse_section(content, "Steps to Reproduce")
|
|
311
|
+
result["expected_behavior"] = parse_section(content, "Expected Behavior")
|
|
312
|
+
result["actual_behavior"] = parse_section(content, "Actual Behavior")
|
|
313
|
+
result["impact"] = parse_section(content, "Impact")
|
|
314
|
+
|
|
315
|
+
# Root Cause Analysis with subsections
|
|
316
|
+
rca_section = parse_section(content, "Root Cause Analysis")
|
|
317
|
+
if rca_section:
|
|
318
|
+
result["root_cause_analysis"] = {
|
|
319
|
+
"investigation": extract_subsection(rca_section, "Investigation"),
|
|
320
|
+
"root_cause": extract_subsection(rca_section, "Root Cause"),
|
|
321
|
+
"why_it_happened": extract_subsection(rca_section, "Why It Happened"),
|
|
322
|
+
"code_blocks": extract_code_blocks(rca_section),
|
|
323
|
+
}
|
|
324
|
+
else:
|
|
325
|
+
result["root_cause_analysis"] = None
|
|
326
|
+
|
|
327
|
+
# Fix Approach
|
|
328
|
+
result["fix_approach"] = parse_section(content, "Fix Approach")
|
|
329
|
+
|
|
330
|
+
# Prevention
|
|
331
|
+
result["prevention"] = parse_section(content, "Prevention")
|
|
332
|
+
|
|
333
|
+
# Testing Strategy
|
|
334
|
+
result["testing_strategy"] = parse_section(content, "Testing Strategy")
|
|
335
|
+
|
|
336
|
+
# Acceptance Criteria - extract as checklist
|
|
337
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
338
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
339
|
+
|
|
340
|
+
# Dependencies
|
|
341
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
342
|
+
|
|
343
|
+
# Estimated Effort
|
|
344
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
345
|
+
|
|
346
|
+
return result
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def parse_refactor_spec(content: str) -> dict[str, Any]:
|
|
350
|
+
"""
|
|
351
|
+
Parse refactor specification.
|
|
352
|
+
|
|
353
|
+
Sections:
|
|
354
|
+
- Overview
|
|
355
|
+
- Current State
|
|
356
|
+
- Problems with Current Approach
|
|
357
|
+
- Proposed Refactor (with subsections: New Approach, Benefits, Trade-offs)
|
|
358
|
+
- Implementation Plan
|
|
359
|
+
- Scope (with subsections: In Scope, Out of Scope)
|
|
360
|
+
- Risk Assessment
|
|
361
|
+
- Acceptance Criteria
|
|
362
|
+
- Testing Strategy
|
|
363
|
+
- Dependencies
|
|
364
|
+
- Estimated Effort
|
|
365
|
+
"""
|
|
366
|
+
# Strip HTML comments first
|
|
367
|
+
content = strip_html_comments(content)
|
|
368
|
+
|
|
369
|
+
result: dict[str, Any] = {}
|
|
370
|
+
|
|
371
|
+
# Extract main sections
|
|
372
|
+
result["overview"] = parse_section(content, "Overview")
|
|
373
|
+
result["current_state"] = parse_section(content, "Current State")
|
|
374
|
+
result["problems"] = parse_section(content, "Problems with Current Approach")
|
|
375
|
+
|
|
376
|
+
# Proposed Refactor with subsections
|
|
377
|
+
refactor_section = parse_section(content, "Proposed Refactor")
|
|
378
|
+
if refactor_section:
|
|
379
|
+
result["proposed_refactor"] = {
|
|
380
|
+
"new_approach": extract_subsection(refactor_section, "New Approach"),
|
|
381
|
+
"benefits": extract_subsection(refactor_section, "Benefits"),
|
|
382
|
+
"trade_offs": extract_subsection(refactor_section, "Trade-offs"),
|
|
383
|
+
"code_blocks": extract_code_blocks(refactor_section),
|
|
384
|
+
}
|
|
385
|
+
else:
|
|
386
|
+
result["proposed_refactor"] = None
|
|
387
|
+
|
|
388
|
+
# Implementation Plan
|
|
389
|
+
result["implementation_plan"] = parse_section(content, "Implementation Plan")
|
|
390
|
+
|
|
391
|
+
# Scope with subsections
|
|
392
|
+
scope_section = parse_section(content, "Scope")
|
|
393
|
+
if scope_section:
|
|
394
|
+
result["scope"] = {
|
|
395
|
+
"in_scope": extract_subsection(scope_section, "In Scope"),
|
|
396
|
+
"out_of_scope": extract_subsection(scope_section, "Out of Scope"),
|
|
397
|
+
}
|
|
398
|
+
else:
|
|
399
|
+
result["scope"] = None
|
|
400
|
+
|
|
401
|
+
# Risk Assessment
|
|
402
|
+
result["risk_assessment"] = parse_section(content, "Risk Assessment")
|
|
403
|
+
|
|
404
|
+
# Acceptance Criteria - extract as checklist
|
|
405
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
406
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
407
|
+
|
|
408
|
+
# Testing Strategy
|
|
409
|
+
result["testing_strategy"] = parse_section(content, "Testing Strategy")
|
|
410
|
+
|
|
411
|
+
# Dependencies
|
|
412
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
413
|
+
|
|
414
|
+
# Estimated Effort
|
|
415
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
416
|
+
|
|
417
|
+
return result
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def parse_security_spec(content: str) -> dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Parse security specification.
|
|
423
|
+
|
|
424
|
+
Sections:
|
|
425
|
+
- Security Issue
|
|
426
|
+
- Severity
|
|
427
|
+
- Affected Components
|
|
428
|
+
- Threat Model (with subsections: Assets at Risk, Threat Actors, Attack Scenarios)
|
|
429
|
+
- Attack Vector
|
|
430
|
+
- Mitigation Strategy
|
|
431
|
+
- Security Testing (with subsections: Automated Security Testing, Manual Security Testing, Test Cases)
|
|
432
|
+
- Compliance
|
|
433
|
+
- Acceptance Criteria
|
|
434
|
+
- Post-Deployment
|
|
435
|
+
- Dependencies
|
|
436
|
+
- Estimated Effort
|
|
437
|
+
"""
|
|
438
|
+
# Strip HTML comments first
|
|
439
|
+
content = strip_html_comments(content)
|
|
440
|
+
|
|
441
|
+
result: dict[str, Any] = {}
|
|
442
|
+
|
|
443
|
+
# Extract main sections
|
|
444
|
+
result["security_issue"] = parse_section(content, "Security Issue")
|
|
445
|
+
result["severity"] = parse_section(content, "Severity")
|
|
446
|
+
result["affected_components"] = parse_section(content, "Affected Components")
|
|
447
|
+
|
|
448
|
+
# Threat Model with subsections
|
|
449
|
+
threat_section = parse_section(content, "Threat Model")
|
|
450
|
+
if threat_section:
|
|
451
|
+
result["threat_model"] = {
|
|
452
|
+
"assets_at_risk": extract_subsection(threat_section, "Assets at Risk"),
|
|
453
|
+
"threat_actors": extract_subsection(threat_section, "Threat Actors"),
|
|
454
|
+
"attack_scenarios": extract_subsection(threat_section, "Attack Scenarios"),
|
|
455
|
+
"code_blocks": extract_code_blocks(threat_section),
|
|
456
|
+
}
|
|
457
|
+
else:
|
|
458
|
+
result["threat_model"] = None
|
|
459
|
+
|
|
460
|
+
# Attack Vector
|
|
461
|
+
result["attack_vector"] = parse_section(content, "Attack Vector")
|
|
462
|
+
|
|
463
|
+
# Mitigation Strategy
|
|
464
|
+
result["mitigation_strategy"] = parse_section(content, "Mitigation Strategy")
|
|
465
|
+
|
|
466
|
+
# Security Testing with subsections
|
|
467
|
+
testing_section = parse_section(content, "Security Testing")
|
|
468
|
+
if testing_section:
|
|
469
|
+
result["security_testing"] = {
|
|
470
|
+
"automated": extract_subsection(testing_section, "Automated Security Testing"),
|
|
471
|
+
"manual": extract_subsection(testing_section, "Manual Security Testing"),
|
|
472
|
+
"test_cases": extract_subsection(testing_section, "Test Cases"),
|
|
473
|
+
"checklist": extract_checklist(testing_section),
|
|
474
|
+
}
|
|
475
|
+
else:
|
|
476
|
+
result["security_testing"] = None
|
|
477
|
+
|
|
478
|
+
# Compliance - extract as checklist
|
|
479
|
+
compliance_section = parse_section(content, "Compliance")
|
|
480
|
+
result["compliance"] = extract_checklist(compliance_section) if compliance_section else []
|
|
481
|
+
|
|
482
|
+
# Acceptance Criteria - extract as checklist
|
|
483
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
484
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
485
|
+
|
|
486
|
+
# Post-Deployment
|
|
487
|
+
post_section = parse_section(content, "Post-Deployment")
|
|
488
|
+
result["post_deployment"] = extract_checklist(post_section) if post_section else []
|
|
489
|
+
|
|
490
|
+
# Dependencies
|
|
491
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
492
|
+
|
|
493
|
+
# Estimated Effort
|
|
494
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
495
|
+
|
|
496
|
+
return result
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def parse_integration_test_spec(content: str) -> dict[str, Any]:
|
|
500
|
+
"""
|
|
501
|
+
Parse integration test specification.
|
|
502
|
+
|
|
503
|
+
Sections:
|
|
504
|
+
- Scope
|
|
505
|
+
- Test Scenarios (multiple subsections: Scenario 1, Scenario 2, etc.)
|
|
506
|
+
- Performance Benchmarks
|
|
507
|
+
- API Contracts
|
|
508
|
+
- Environment Requirements
|
|
509
|
+
- Acceptance Criteria
|
|
510
|
+
- Dependencies
|
|
511
|
+
- Estimated Effort
|
|
512
|
+
"""
|
|
513
|
+
# Strip HTML comments first
|
|
514
|
+
content = strip_html_comments(content)
|
|
515
|
+
|
|
516
|
+
result: dict[str, Any] = {}
|
|
517
|
+
|
|
518
|
+
# Extract main sections
|
|
519
|
+
result["scope"] = parse_section(content, "Scope")
|
|
520
|
+
|
|
521
|
+
# Test Scenarios - extract all scenarios
|
|
522
|
+
scenarios_section = parse_section(content, "Test Scenarios")
|
|
523
|
+
if scenarios_section:
|
|
524
|
+
# Find all subsections that start with "Scenario"
|
|
525
|
+
scenarios = []
|
|
526
|
+
lines = scenarios_section.split("\n")
|
|
527
|
+
current_scenario: str | None = None
|
|
528
|
+
current_content: list[str] = []
|
|
529
|
+
|
|
530
|
+
for line in lines:
|
|
531
|
+
if line.startswith("### Scenario"):
|
|
532
|
+
# Save previous scenario if exists
|
|
533
|
+
if current_scenario is not None:
|
|
534
|
+
scenarios.append(
|
|
535
|
+
{
|
|
536
|
+
"name": current_scenario,
|
|
537
|
+
"content": "\n".join(current_content).strip(),
|
|
538
|
+
}
|
|
539
|
+
)
|
|
540
|
+
# Start new scenario
|
|
541
|
+
current_scenario = line[4:].strip() # Remove '### '
|
|
542
|
+
current_content = []
|
|
543
|
+
elif current_scenario:
|
|
544
|
+
current_content.append(line)
|
|
545
|
+
|
|
546
|
+
# Save last scenario
|
|
547
|
+
if current_scenario is not None:
|
|
548
|
+
scenarios.append(
|
|
549
|
+
{
|
|
550
|
+
"name": current_scenario,
|
|
551
|
+
"content": "\n".join(current_content).strip(),
|
|
552
|
+
}
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
result["test_scenarios"] = scenarios
|
|
556
|
+
else:
|
|
557
|
+
result["test_scenarios"] = []
|
|
558
|
+
|
|
559
|
+
# Performance Benchmarks
|
|
560
|
+
result["performance_benchmarks"] = parse_section(content, "Performance Benchmarks")
|
|
561
|
+
|
|
562
|
+
# API Contracts
|
|
563
|
+
result["api_contracts"] = parse_section(content, "API Contracts")
|
|
564
|
+
|
|
565
|
+
# Environment Requirements
|
|
566
|
+
result["environment_requirements"] = parse_section(content, "Environment Requirements")
|
|
567
|
+
|
|
568
|
+
# Acceptance Criteria - extract as checklist
|
|
569
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
570
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
571
|
+
|
|
572
|
+
# Dependencies
|
|
573
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
574
|
+
|
|
575
|
+
# Estimated Effort
|
|
576
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
577
|
+
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def parse_deployment_spec(content: str) -> dict[str, Any]:
|
|
582
|
+
"""
|
|
583
|
+
Parse deployment specification.
|
|
584
|
+
|
|
585
|
+
Sections:
|
|
586
|
+
- Deployment Scope
|
|
587
|
+
- Deployment Procedure (with subsections: Pre-Deployment Checklist, Deployment Steps, Post-Deployment Steps)
|
|
588
|
+
- Environment Configuration
|
|
589
|
+
- Rollback Procedure (with subsections: Rollback Triggers, Rollback Steps)
|
|
590
|
+
- Smoke Tests (multiple subsections: Test 1, Test 2, etc.)
|
|
591
|
+
- Monitoring & Alerting
|
|
592
|
+
- Post-Deployment Monitoring Period
|
|
593
|
+
- Acceptance Criteria
|
|
594
|
+
- Dependencies
|
|
595
|
+
- Estimated Effort
|
|
596
|
+
"""
|
|
597
|
+
# Strip HTML comments first
|
|
598
|
+
content = strip_html_comments(content)
|
|
599
|
+
|
|
600
|
+
result: dict[str, Any] = {}
|
|
601
|
+
|
|
602
|
+
# Extract main sections
|
|
603
|
+
result["deployment_scope"] = parse_section(content, "Deployment Scope")
|
|
604
|
+
|
|
605
|
+
# Deployment Procedure with subsections
|
|
606
|
+
procedure_section = parse_section(content, "Deployment Procedure")
|
|
607
|
+
if procedure_section:
|
|
608
|
+
result["deployment_procedure"] = {
|
|
609
|
+
"pre_deployment": extract_subsection(procedure_section, "Pre-Deployment Checklist"),
|
|
610
|
+
"deployment_steps": extract_subsection(procedure_section, "Deployment Steps"),
|
|
611
|
+
"post_deployment": extract_subsection(procedure_section, "Post-Deployment Steps"),
|
|
612
|
+
"code_blocks": extract_code_blocks(procedure_section),
|
|
613
|
+
"checklist": extract_checklist(procedure_section),
|
|
614
|
+
}
|
|
615
|
+
else:
|
|
616
|
+
result["deployment_procedure"] = None
|
|
617
|
+
|
|
618
|
+
# Environment Configuration
|
|
619
|
+
result["environment_configuration"] = parse_section(content, "Environment Configuration")
|
|
620
|
+
|
|
621
|
+
# Rollback Procedure with subsections
|
|
622
|
+
rollback_section = parse_section(content, "Rollback Procedure")
|
|
623
|
+
if rollback_section:
|
|
624
|
+
result["rollback_procedure"] = {
|
|
625
|
+
"triggers": extract_subsection(rollback_section, "Rollback Triggers"),
|
|
626
|
+
"steps": extract_subsection(rollback_section, "Rollback Steps"),
|
|
627
|
+
"code_blocks": extract_code_blocks(rollback_section),
|
|
628
|
+
}
|
|
629
|
+
else:
|
|
630
|
+
result["rollback_procedure"] = None
|
|
631
|
+
|
|
632
|
+
# Smoke Tests - extract all tests
|
|
633
|
+
smoke_section = parse_section(content, "Smoke Tests")
|
|
634
|
+
if smoke_section:
|
|
635
|
+
# Find all subsections that start with "Test"
|
|
636
|
+
tests = []
|
|
637
|
+
lines = smoke_section.split("\n")
|
|
638
|
+
current_test: str | None = None
|
|
639
|
+
current_content: list[str] = []
|
|
640
|
+
|
|
641
|
+
for line in lines:
|
|
642
|
+
if line.startswith("### Test"):
|
|
643
|
+
# Save previous test if exists
|
|
644
|
+
if current_test is not None:
|
|
645
|
+
tests.append(
|
|
646
|
+
{
|
|
647
|
+
"name": current_test,
|
|
648
|
+
"content": "\n".join(current_content).strip(),
|
|
649
|
+
}
|
|
650
|
+
)
|
|
651
|
+
# Start new test
|
|
652
|
+
current_test = line[4:].strip() # Remove '### '
|
|
653
|
+
current_content = []
|
|
654
|
+
elif current_test:
|
|
655
|
+
current_content.append(line)
|
|
656
|
+
|
|
657
|
+
# Save last test
|
|
658
|
+
if current_test is not None:
|
|
659
|
+
tests.append({"name": current_test, "content": "\n".join(current_content).strip()})
|
|
660
|
+
|
|
661
|
+
result["smoke_tests"] = tests
|
|
662
|
+
else:
|
|
663
|
+
result["smoke_tests"] = []
|
|
664
|
+
|
|
665
|
+
# Monitoring & Alerting
|
|
666
|
+
result["monitoring"] = parse_section(content, "Monitoring & Alerting")
|
|
667
|
+
|
|
668
|
+
# Post-Deployment Monitoring Period
|
|
669
|
+
result["monitoring_period"] = parse_section(content, "Post-Deployment Monitoring Period")
|
|
670
|
+
|
|
671
|
+
# Acceptance Criteria - extract as checklist
|
|
672
|
+
ac_section = parse_section(content, "Acceptance Criteria")
|
|
673
|
+
result["acceptance_criteria"] = extract_checklist(ac_section) if ac_section else []
|
|
674
|
+
|
|
675
|
+
# Dependencies
|
|
676
|
+
result["dependencies"] = parse_section(content, "Dependencies")
|
|
677
|
+
|
|
678
|
+
# Estimated Effort
|
|
679
|
+
result["estimated_effort"] = parse_section(content, "Estimated Effort")
|
|
680
|
+
|
|
681
|
+
return result
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
# ============================================================================
|
|
685
|
+
# Main Entry Point
|
|
686
|
+
# ============================================================================
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
@log_errors()
|
|
690
|
+
def parse_spec_file(work_item: str | dict[str, Any]) -> dict[str, Any]:
|
|
691
|
+
"""
|
|
692
|
+
Parse a work item specification file.
|
|
693
|
+
|
|
694
|
+
Args:
|
|
695
|
+
work_item: Either a work item dict with 'spec_file' and 'id' fields,
|
|
696
|
+
or a string work_item_id (for backwards compatibility)
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Parsed specification as structured dict
|
|
700
|
+
|
|
701
|
+
Raises:
|
|
702
|
+
FileNotFoundError: If spec file doesn't exist
|
|
703
|
+
ValidationError: If work item type cannot be determined or spec format is invalid
|
|
704
|
+
SpecValidationError: If spec file structure is invalid
|
|
705
|
+
"""
|
|
706
|
+
# Handle backwards compatibility: accept both dict and string
|
|
707
|
+
if isinstance(work_item, str):
|
|
708
|
+
# Legacy call with just work_item_id string
|
|
709
|
+
work_item_id: str | Any | None = work_item
|
|
710
|
+
spec_path = Path(f".session/specs/{work_item_id}.md")
|
|
711
|
+
logger.debug("Parsing spec file for work item (legacy): %s", work_item_id)
|
|
712
|
+
else:
|
|
713
|
+
# New call with work item dict
|
|
714
|
+
work_item_id = work_item.get("id")
|
|
715
|
+
# Use spec_file from work item if available, otherwise fallback to ID-based pattern
|
|
716
|
+
spec_file_path = work_item.get("spec_file")
|
|
717
|
+
if spec_file_path:
|
|
718
|
+
spec_path = Path(spec_file_path)
|
|
719
|
+
logger.debug("Parsing spec file from work_item.spec_file: %s", spec_path)
|
|
720
|
+
else:
|
|
721
|
+
# Fallback to legacy pattern for backwards compatibility
|
|
722
|
+
spec_path = Path(f".session/specs/{work_item_id}.md")
|
|
723
|
+
logger.debug("Parsing spec file (fallback to ID pattern): %s", spec_path)
|
|
724
|
+
|
|
725
|
+
if not spec_path.exists():
|
|
726
|
+
logger.error("Spec file not found: %s", spec_path)
|
|
727
|
+
raise SolokitFileNotFoundError(file_path=str(spec_path), file_type="spec")
|
|
728
|
+
|
|
729
|
+
try:
|
|
730
|
+
with open(spec_path, encoding="utf-8") as f:
|
|
731
|
+
content = f.read()
|
|
732
|
+
except OSError as e:
|
|
733
|
+
logger.error("Failed to read spec file: %s", spec_path)
|
|
734
|
+
raise ValidationError(
|
|
735
|
+
message=f"Failed to read spec file: {spec_path}",
|
|
736
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
737
|
+
context={"file_path": str(spec_path)},
|
|
738
|
+
remediation="Check file permissions and try again",
|
|
739
|
+
cause=e,
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Determine work item type from first line (H1 heading)
|
|
743
|
+
first_line = content.split("\n")[0].strip()
|
|
744
|
+
if not first_line.startswith("# "):
|
|
745
|
+
logger.error("Invalid spec file: Missing H1 heading in %s", spec_path)
|
|
746
|
+
raise ValidationError(
|
|
747
|
+
message=f"Invalid spec file: Missing H1 heading in {spec_path}",
|
|
748
|
+
code=ErrorCode.SPEC_VALIDATION_FAILED,
|
|
749
|
+
context={"file_path": str(spec_path), "first_line": first_line},
|
|
750
|
+
remediation="Spec file must start with '# Type: Name' heading",
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
# Extract type from "# Type: Name" pattern
|
|
754
|
+
heading_match = re.match(r"#\s*(\w+):\s*(.+)", first_line)
|
|
755
|
+
if not heading_match:
|
|
756
|
+
logger.error("Invalid spec file: H1 heading doesn't match pattern in %s", spec_path)
|
|
757
|
+
raise ValidationError(
|
|
758
|
+
message=f"Invalid spec file: H1 heading doesn't match 'Type: Name' pattern in {spec_path}",
|
|
759
|
+
code=ErrorCode.SPEC_VALIDATION_FAILED,
|
|
760
|
+
context={"file_path": str(spec_path), "heading": first_line},
|
|
761
|
+
remediation="Use format: '# Type: Name' (e.g., '# Feature: My Feature')",
|
|
762
|
+
)
|
|
763
|
+
|
|
764
|
+
work_type = heading_match.group(1).lower()
|
|
765
|
+
work_name = heading_match.group(2).strip()
|
|
766
|
+
logger.debug("Detected work type: %s, name: %s", work_type, work_name)
|
|
767
|
+
|
|
768
|
+
# Parse based on work item type
|
|
769
|
+
parsers = {
|
|
770
|
+
WorkItemType.FEATURE.value: parse_feature_spec,
|
|
771
|
+
WorkItemType.BUG.value: parse_bug_spec,
|
|
772
|
+
WorkItemType.REFACTOR.value: parse_refactor_spec,
|
|
773
|
+
WorkItemType.SECURITY.value: parse_security_spec,
|
|
774
|
+
WorkItemType.INTEGRATION_TEST.value: parse_integration_test_spec,
|
|
775
|
+
WorkItemType.DEPLOYMENT.value: parse_deployment_spec,
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
parser = parsers.get(work_type)
|
|
779
|
+
if not parser:
|
|
780
|
+
valid_types = ", ".join(parsers.keys())
|
|
781
|
+
raise ValidationError(
|
|
782
|
+
message=f"Unknown work item type: {work_type}",
|
|
783
|
+
code=ErrorCode.INVALID_WORK_ITEM_TYPE,
|
|
784
|
+
context={
|
|
785
|
+
"work_type": work_type,
|
|
786
|
+
"valid_types": list(parsers.keys()),
|
|
787
|
+
"file_path": str(spec_path),
|
|
788
|
+
},
|
|
789
|
+
remediation=f"Use one of: {valid_types}",
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
# Parse the spec
|
|
793
|
+
try:
|
|
794
|
+
parsed = parser(content)
|
|
795
|
+
parsed["_meta"] = {
|
|
796
|
+
"work_item_id": work_item_id,
|
|
797
|
+
"work_type": work_type,
|
|
798
|
+
"name": work_name,
|
|
799
|
+
"spec_path": str(spec_path),
|
|
800
|
+
}
|
|
801
|
+
return parsed
|
|
802
|
+
except ValidationError:
|
|
803
|
+
# Re-raise ValidationError as-is
|
|
804
|
+
raise
|
|
805
|
+
except Exception as e:
|
|
806
|
+
# Wrap unexpected errors in ValidationError
|
|
807
|
+
logger.error("Error parsing spec file %s: %s", spec_path, str(e))
|
|
808
|
+
raise ValidationError(
|
|
809
|
+
message=f"Error parsing spec file {spec_path}",
|
|
810
|
+
code=ErrorCode.SPEC_VALIDATION_FAILED,
|
|
811
|
+
context={"file_path": str(spec_path), "work_type": work_type, "error": str(e)},
|
|
812
|
+
remediation="Check spec file format and content",
|
|
813
|
+
cause=e,
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
|
|
817
|
+
# ============================================================================
|
|
818
|
+
# CLI Interface for Testing
|
|
819
|
+
# ============================================================================
|
|
820
|
+
|
|
821
|
+
if __name__ == "__main__":
|
|
822
|
+
import sys
|
|
823
|
+
|
|
824
|
+
if len(sys.argv) < 2:
|
|
825
|
+
output.info("Usage: spec_parser.py <work_item_id>")
|
|
826
|
+
output.info("Example: spec_parser.py feature_001")
|
|
827
|
+
sys.exit(1)
|
|
828
|
+
|
|
829
|
+
work_item_id = sys.argv[1]
|
|
830
|
+
|
|
831
|
+
try:
|
|
832
|
+
logger.info("Parsing spec file for work item: %s", work_item_id)
|
|
833
|
+
result = parse_spec_file(work_item_id)
|
|
834
|
+
output.info(json.dumps(result, indent=2))
|
|
835
|
+
except Exception as e:
|
|
836
|
+
logger.error("Failed to parse spec file", exc_info=True)
|
|
837
|
+
output.error(f"Error: {e}")
|
|
838
|
+
sys.exit(1)
|