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,261 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Security vulnerability scanner.
|
|
4
|
+
|
|
5
|
+
Runs security scans using Bandit (Python) and Safety (Python dependencies),
|
|
6
|
+
or npm audit (JavaScript/TypeScript).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import tempfile
|
|
14
|
+
import time
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from solokit.core.command_runner import CommandRunner
|
|
19
|
+
from solokit.core.constants import QUALITY_CHECK_LONG_TIMEOUT
|
|
20
|
+
from solokit.core.logging_config import get_logger
|
|
21
|
+
from solokit.quality.checkers.base import CheckResult, QualityChecker
|
|
22
|
+
|
|
23
|
+
logger = get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class SecurityChecker(QualityChecker):
|
|
27
|
+
"""Security vulnerability scanning for Python and JavaScript/TypeScript."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
config: dict[str, Any],
|
|
32
|
+
project_root: Path | None = None,
|
|
33
|
+
language: str | None = None,
|
|
34
|
+
runner: CommandRunner | None = None,
|
|
35
|
+
):
|
|
36
|
+
"""Initialize security checker.
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
config: Security configuration
|
|
40
|
+
project_root: Project root directory
|
|
41
|
+
language: Programming language (python, javascript, typescript)
|
|
42
|
+
runner: Optional CommandRunner instance (for testing)
|
|
43
|
+
"""
|
|
44
|
+
super().__init__(config, project_root)
|
|
45
|
+
self.runner = (
|
|
46
|
+
runner
|
|
47
|
+
if runner is not None
|
|
48
|
+
else CommandRunner(default_timeout=QUALITY_CHECK_LONG_TIMEOUT)
|
|
49
|
+
)
|
|
50
|
+
self.language = language or self._detect_language()
|
|
51
|
+
|
|
52
|
+
def name(self) -> str:
|
|
53
|
+
"""Return checker name."""
|
|
54
|
+
return "security"
|
|
55
|
+
|
|
56
|
+
def is_enabled(self) -> bool:
|
|
57
|
+
"""Check if security scanning is enabled."""
|
|
58
|
+
return bool(self.config.get("enabled", True))
|
|
59
|
+
|
|
60
|
+
def _detect_language(self) -> str:
|
|
61
|
+
"""Detect primary project language."""
|
|
62
|
+
if (self.project_root / "pyproject.toml").exists() or (
|
|
63
|
+
self.project_root / "setup.py"
|
|
64
|
+
).exists():
|
|
65
|
+
return "python"
|
|
66
|
+
elif (self.project_root / "package.json").exists():
|
|
67
|
+
if (self.project_root / "tsconfig.json").exists():
|
|
68
|
+
return "typescript"
|
|
69
|
+
return "javascript"
|
|
70
|
+
return "python" # default
|
|
71
|
+
|
|
72
|
+
def run(self) -> CheckResult:
|
|
73
|
+
"""Run security vulnerability scan."""
|
|
74
|
+
start_time = time.time()
|
|
75
|
+
|
|
76
|
+
if not self.is_enabled():
|
|
77
|
+
return self._create_skipped_result()
|
|
78
|
+
|
|
79
|
+
logger.info(f"Running security scan for {self.language}")
|
|
80
|
+
|
|
81
|
+
if self.language == "python":
|
|
82
|
+
results = self._scan_python()
|
|
83
|
+
elif self.language in ["javascript", "typescript"]:
|
|
84
|
+
results = self._scan_javascript()
|
|
85
|
+
else:
|
|
86
|
+
return self._create_skipped_result(reason=f"unsupported language: {self.language}")
|
|
87
|
+
|
|
88
|
+
# Check if passed based on fail_on threshold
|
|
89
|
+
fail_on = self.config.get("fail_on", "high").upper()
|
|
90
|
+
severity_levels = ["LOW", "MEDIUM", "HIGH", "CRITICAL"]
|
|
91
|
+
|
|
92
|
+
if fail_on not in severity_levels:
|
|
93
|
+
fail_on = "HIGH"
|
|
94
|
+
|
|
95
|
+
fail_threshold = severity_levels.index(fail_on)
|
|
96
|
+
|
|
97
|
+
passed = True
|
|
98
|
+
for severity, count in results.get("by_severity", {}).items():
|
|
99
|
+
if (
|
|
100
|
+
severity in severity_levels
|
|
101
|
+
and severity_levels.index(severity) >= fail_threshold
|
|
102
|
+
and count > 0
|
|
103
|
+
):
|
|
104
|
+
passed = False
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
execution_time = time.time() - start_time
|
|
108
|
+
|
|
109
|
+
return CheckResult(
|
|
110
|
+
checker_name=self.name(),
|
|
111
|
+
passed=passed,
|
|
112
|
+
status="passed" if passed else "failed",
|
|
113
|
+
errors=results.get("vulnerabilities", []),
|
|
114
|
+
warnings=[],
|
|
115
|
+
info={
|
|
116
|
+
"by_severity": results.get("by_severity", {}),
|
|
117
|
+
"fail_threshold": fail_on,
|
|
118
|
+
"language": self.language,
|
|
119
|
+
},
|
|
120
|
+
execution_time=execution_time,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
def _scan_python(self) -> dict[str, Any]:
|
|
124
|
+
"""Run Python security scans (Bandit + Safety)."""
|
|
125
|
+
results: dict[str, Any] = {"vulnerabilities": [], "by_severity": {}}
|
|
126
|
+
|
|
127
|
+
# Run Bandit
|
|
128
|
+
bandit_results = self._run_bandit()
|
|
129
|
+
if bandit_results:
|
|
130
|
+
results["bandit"] = bandit_results
|
|
131
|
+
# Count by severity
|
|
132
|
+
for issue in bandit_results.get("results", []):
|
|
133
|
+
severity = issue.get("issue_severity", "LOW")
|
|
134
|
+
results["by_severity"][severity] = results["by_severity"].get(severity, 0) + 1
|
|
135
|
+
# Add to vulnerabilities list
|
|
136
|
+
results["vulnerabilities"].append(
|
|
137
|
+
{
|
|
138
|
+
"source": "bandit",
|
|
139
|
+
"file": issue.get("filename", ""),
|
|
140
|
+
"line": issue.get("line_number", 0),
|
|
141
|
+
"issue": issue.get("issue_text", ""),
|
|
142
|
+
"severity": severity,
|
|
143
|
+
"confidence": issue.get("issue_confidence", ""),
|
|
144
|
+
}
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Run Safety
|
|
148
|
+
safety_results = self._run_safety()
|
|
149
|
+
if safety_results:
|
|
150
|
+
results["safety"] = safety_results
|
|
151
|
+
results["vulnerabilities"].extend(safety_results)
|
|
152
|
+
|
|
153
|
+
return results
|
|
154
|
+
|
|
155
|
+
def _run_bandit(self) -> dict[str, Any] | None:
|
|
156
|
+
"""Run Bandit security scanner."""
|
|
157
|
+
try:
|
|
158
|
+
# Create temporary file for report
|
|
159
|
+
fd, bandit_report_path = tempfile.mkstemp(suffix=".json")
|
|
160
|
+
os.close(fd)
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
src_dir = self.project_root / "src"
|
|
164
|
+
if not src_dir.exists():
|
|
165
|
+
logger.debug("No src/ directory found, skipping Bandit")
|
|
166
|
+
return None
|
|
167
|
+
|
|
168
|
+
self.runner.run(
|
|
169
|
+
[
|
|
170
|
+
"bandit",
|
|
171
|
+
"-r",
|
|
172
|
+
str(src_dir),
|
|
173
|
+
"-f",
|
|
174
|
+
"json",
|
|
175
|
+
"-o",
|
|
176
|
+
bandit_report_path,
|
|
177
|
+
],
|
|
178
|
+
timeout=QUALITY_CHECK_LONG_TIMEOUT,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
if Path(bandit_report_path).exists():
|
|
182
|
+
try:
|
|
183
|
+
with open(bandit_report_path) as f:
|
|
184
|
+
content = f.read().strip()
|
|
185
|
+
if content:
|
|
186
|
+
return json.loads(content) # type: ignore[no-any-return]
|
|
187
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
188
|
+
logger.warning(f"Failed to parse bandit report: {e}")
|
|
189
|
+
except OSError as e:
|
|
190
|
+
logger.warning(f"Failed to read bandit report: {e}")
|
|
191
|
+
finally:
|
|
192
|
+
# Clean up temporary file
|
|
193
|
+
try:
|
|
194
|
+
Path(bandit_report_path).unlink()
|
|
195
|
+
except OSError:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
except (ImportError, OSError) as e:
|
|
199
|
+
logger.debug(f"Bandit not available: {e}")
|
|
200
|
+
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
def _run_safety(self) -> list[dict[str, Any]]:
|
|
204
|
+
"""Run Safety dependency scanner."""
|
|
205
|
+
requirements_file = self.project_root / "requirements.txt"
|
|
206
|
+
if not requirements_file.exists():
|
|
207
|
+
logger.debug("No requirements.txt found, skipping Safety")
|
|
208
|
+
return []
|
|
209
|
+
|
|
210
|
+
result = self.runner.run(
|
|
211
|
+
["safety", "check", "--file", str(requirements_file), "--json"],
|
|
212
|
+
timeout=QUALITY_CHECK_LONG_TIMEOUT,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
if result.success and result.stdout:
|
|
216
|
+
try:
|
|
217
|
+
# Safety may prefix JSON with deprecation warnings, so find the first JSON marker
|
|
218
|
+
json_start = min(
|
|
219
|
+
(
|
|
220
|
+
pos
|
|
221
|
+
for pos in [result.stdout.find("{"), result.stdout.find("[")]
|
|
222
|
+
if pos != -1
|
|
223
|
+
),
|
|
224
|
+
default=-1,
|
|
225
|
+
)
|
|
226
|
+
if json_start != -1:
|
|
227
|
+
return json.loads(result.stdout[json_start:]) # type: ignore[no-any-return]
|
|
228
|
+
return json.loads(result.stdout) # type: ignore[no-any-return]
|
|
229
|
+
except json.JSONDecodeError as e:
|
|
230
|
+
logger.warning(f"Failed to parse safety output: {e}")
|
|
231
|
+
logger.debug(f"Safety output: {result.stdout[:200]}")
|
|
232
|
+
|
|
233
|
+
return []
|
|
234
|
+
|
|
235
|
+
def _scan_javascript(self) -> dict[str, Any]:
|
|
236
|
+
"""Run JavaScript/TypeScript security scans (npm audit)."""
|
|
237
|
+
results: dict[str, Any] = {"vulnerabilities": [], "by_severity": {}}
|
|
238
|
+
|
|
239
|
+
package_json = self.project_root / "package.json"
|
|
240
|
+
if not package_json.exists():
|
|
241
|
+
logger.debug("No package.json found, skipping npm audit")
|
|
242
|
+
return results
|
|
243
|
+
|
|
244
|
+
audit_result = self.runner.run(
|
|
245
|
+
["npm", "audit", "--json"], timeout=QUALITY_CHECK_LONG_TIMEOUT
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if audit_result.success and audit_result.stdout:
|
|
249
|
+
try:
|
|
250
|
+
audit_data = json.loads(audit_result.stdout)
|
|
251
|
+
results["npm_audit"] = audit_data
|
|
252
|
+
|
|
253
|
+
# Count by severity
|
|
254
|
+
for vuln in audit_data.get("vulnerabilities", {}).values():
|
|
255
|
+
severity = vuln.get("severity", "low").upper()
|
|
256
|
+
results["by_severity"][severity] = results["by_severity"].get(severity, 0) + 1
|
|
257
|
+
|
|
258
|
+
except json.JSONDecodeError:
|
|
259
|
+
logger.warning("Failed to parse npm audit output")
|
|
260
|
+
|
|
261
|
+
return results
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Specification completeness checker.
|
|
4
|
+
|
|
5
|
+
Validates that work item specification files are complete and properly formatted.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import time
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Union, cast
|
|
13
|
+
|
|
14
|
+
from solokit.core.exceptions import (
|
|
15
|
+
FileNotFoundError as SolokitFileNotFoundError,
|
|
16
|
+
)
|
|
17
|
+
from solokit.core.exceptions import SpecValidationError
|
|
18
|
+
from solokit.core.logging_config import get_logger
|
|
19
|
+
from solokit.quality.checkers.base import CheckResult, QualityChecker
|
|
20
|
+
from solokit.work_items.spec_validator import validate_spec_file
|
|
21
|
+
|
|
22
|
+
logger = get_logger(__name__)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SpecCompletenessChecker(QualityChecker):
|
|
26
|
+
"""Specification file completeness validation."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
config: dict[str, Any],
|
|
31
|
+
project_root: Path | None = None,
|
|
32
|
+
work_item: dict[str, Any] | None = None,
|
|
33
|
+
):
|
|
34
|
+
"""Initialize spec completeness checker.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
config: Spec completeness configuration
|
|
38
|
+
project_root: Project root directory
|
|
39
|
+
work_item: Work item dictionary with 'id' and 'type' fields
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(config, project_root)
|
|
42
|
+
self.work_item = work_item or {}
|
|
43
|
+
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
"""Return checker name."""
|
|
46
|
+
return "spec_completeness"
|
|
47
|
+
|
|
48
|
+
def is_enabled(self) -> bool:
|
|
49
|
+
"""Check if spec completeness validation is enabled."""
|
|
50
|
+
return bool(self.config.get("enabled", True))
|
|
51
|
+
|
|
52
|
+
def run(self) -> CheckResult:
|
|
53
|
+
"""Run spec completeness validation."""
|
|
54
|
+
start_time = time.time()
|
|
55
|
+
|
|
56
|
+
if not self.is_enabled():
|
|
57
|
+
return self._create_skipped_result()
|
|
58
|
+
|
|
59
|
+
work_item_id = self.work_item.get("id")
|
|
60
|
+
work_item_type = self.work_item.get("type")
|
|
61
|
+
|
|
62
|
+
if not work_item_id or not work_item_type:
|
|
63
|
+
execution_time = time.time() - start_time
|
|
64
|
+
return CheckResult(
|
|
65
|
+
checker_name=self.name(),
|
|
66
|
+
passed=False,
|
|
67
|
+
status="failed",
|
|
68
|
+
errors=[{"message": "Work item missing 'id' or 'type' field"}],
|
|
69
|
+
warnings=[],
|
|
70
|
+
info={},
|
|
71
|
+
execution_time=execution_time,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
logger.info(f"Validating spec file for work item: {work_item_id}")
|
|
75
|
+
|
|
76
|
+
# Validate spec file
|
|
77
|
+
try:
|
|
78
|
+
validate_spec_file(work_item_id, work_item_type)
|
|
79
|
+
execution_time = time.time() - start_time
|
|
80
|
+
return CheckResult(
|
|
81
|
+
checker_name=self.name(),
|
|
82
|
+
passed=True,
|
|
83
|
+
status="passed",
|
|
84
|
+
errors=[],
|
|
85
|
+
warnings=[],
|
|
86
|
+
info={"message": f"Spec file for '{work_item_id}' is complete"},
|
|
87
|
+
execution_time=execution_time,
|
|
88
|
+
)
|
|
89
|
+
except SpecValidationError as e:
|
|
90
|
+
execution_time = time.time() - start_time
|
|
91
|
+
validation_errors = e.context.get("validation_errors", [])
|
|
92
|
+
errors = [{"message": error} for error in validation_errors]
|
|
93
|
+
return CheckResult(
|
|
94
|
+
checker_name=self.name(),
|
|
95
|
+
passed=False,
|
|
96
|
+
status="failed",
|
|
97
|
+
errors=cast(list[Union[dict[str, Any], str]], errors),
|
|
98
|
+
warnings=[],
|
|
99
|
+
info={
|
|
100
|
+
"message": f"Spec file for '{work_item_id}' is incomplete",
|
|
101
|
+
"suggestion": e.remediation
|
|
102
|
+
or f"Edit .session/specs/{work_item_id}.md to add missing sections",
|
|
103
|
+
},
|
|
104
|
+
execution_time=execution_time,
|
|
105
|
+
)
|
|
106
|
+
except SolokitFileNotFoundError as e:
|
|
107
|
+
execution_time = time.time() - start_time
|
|
108
|
+
return CheckResult(
|
|
109
|
+
checker_name=self.name(),
|
|
110
|
+
passed=False,
|
|
111
|
+
status="failed",
|
|
112
|
+
errors=[{"message": e.message}],
|
|
113
|
+
warnings=[],
|
|
114
|
+
info={"suggestion": e.remediation},
|
|
115
|
+
execution_time=execution_time,
|
|
116
|
+
)
|
|
117
|
+
except (OSError, ValueError) as e:
|
|
118
|
+
execution_time = time.time() - start_time
|
|
119
|
+
return CheckResult(
|
|
120
|
+
checker_name=self.name(),
|
|
121
|
+
passed=False,
|
|
122
|
+
status="failed",
|
|
123
|
+
errors=[{"message": f"Error validating spec file: {str(e)}"}],
|
|
124
|
+
warnings=[],
|
|
125
|
+
info={"suggestion": "Check spec file format and validator configuration"},
|
|
126
|
+
execution_time=execution_time,
|
|
127
|
+
)
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Test execution and coverage checker.
|
|
4
|
+
|
|
5
|
+
Runs tests using pytest, Jest, or other test frameworks and validates coverage.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import time
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Union, cast
|
|
14
|
+
|
|
15
|
+
from solokit.core.command_runner import CommandRunner
|
|
16
|
+
from solokit.core.constants import TEST_RUNNER_TIMEOUT
|
|
17
|
+
from solokit.core.logging_config import get_logger
|
|
18
|
+
from solokit.quality.checkers.base import CheckResult, QualityChecker
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ExecutionChecker(QualityChecker):
|
|
24
|
+
"""Test execution and coverage validation."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
config: dict[str, Any],
|
|
29
|
+
project_root: Path | None = None,
|
|
30
|
+
language: str | None = None,
|
|
31
|
+
runner: CommandRunner | None = None,
|
|
32
|
+
):
|
|
33
|
+
"""Initialize test runner.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
config: Test execution configuration
|
|
37
|
+
project_root: Project root directory
|
|
38
|
+
language: Programming language (python, javascript, typescript)
|
|
39
|
+
runner: Optional CommandRunner instance (for testing)
|
|
40
|
+
"""
|
|
41
|
+
super().__init__(config, project_root)
|
|
42
|
+
self.runner = (
|
|
43
|
+
runner if runner is not None else CommandRunner(default_timeout=TEST_RUNNER_TIMEOUT)
|
|
44
|
+
)
|
|
45
|
+
self.language = language or self._detect_language()
|
|
46
|
+
|
|
47
|
+
def name(self) -> str:
|
|
48
|
+
"""Return checker name."""
|
|
49
|
+
return "tests"
|
|
50
|
+
|
|
51
|
+
def is_enabled(self) -> bool:
|
|
52
|
+
"""Check if test execution is enabled."""
|
|
53
|
+
return bool(self.config.get("enabled", True))
|
|
54
|
+
|
|
55
|
+
def _detect_language(self) -> str:
|
|
56
|
+
"""Detect primary project language."""
|
|
57
|
+
if (self.project_root / "pyproject.toml").exists() or (
|
|
58
|
+
self.project_root / "setup.py"
|
|
59
|
+
).exists():
|
|
60
|
+
return "python"
|
|
61
|
+
elif (self.project_root / "package.json").exists():
|
|
62
|
+
if (self.project_root / "tsconfig.json").exists():
|
|
63
|
+
return "typescript"
|
|
64
|
+
return "javascript"
|
|
65
|
+
return "python" # default
|
|
66
|
+
|
|
67
|
+
def run(self) -> CheckResult:
|
|
68
|
+
"""Run test suite with coverage."""
|
|
69
|
+
start_time = time.time()
|
|
70
|
+
|
|
71
|
+
if not self.is_enabled():
|
|
72
|
+
return self._create_skipped_result()
|
|
73
|
+
|
|
74
|
+
logger.info(f"Running tests for {self.language}")
|
|
75
|
+
|
|
76
|
+
# Get test command for language
|
|
77
|
+
commands = self.config.get("commands", {})
|
|
78
|
+
command = commands.get(self.language)
|
|
79
|
+
if not command:
|
|
80
|
+
logger.warning(f"No test command configured for language: {self.language}")
|
|
81
|
+
return self._create_skipped_result(reason=f"no command for {self.language}")
|
|
82
|
+
|
|
83
|
+
# Run tests
|
|
84
|
+
result = self.runner.run(command.split(), timeout=TEST_RUNNER_TIMEOUT)
|
|
85
|
+
|
|
86
|
+
# pytest exit codes:
|
|
87
|
+
# 0 = all tests passed
|
|
88
|
+
# 1 = tests were collected and run but some failed
|
|
89
|
+
# 2 = test execution was interrupted
|
|
90
|
+
# 3 = internal error
|
|
91
|
+
# 4 = pytest command line usage error
|
|
92
|
+
# 5 = no tests were collected
|
|
93
|
+
|
|
94
|
+
if result.timed_out:
|
|
95
|
+
execution_time = time.time() - start_time
|
|
96
|
+
return CheckResult(
|
|
97
|
+
checker_name=self.name(),
|
|
98
|
+
passed=False,
|
|
99
|
+
status="failed",
|
|
100
|
+
errors=[{"message": "Test execution timed out"}],
|
|
101
|
+
warnings=[],
|
|
102
|
+
info={"reason": "timeout"},
|
|
103
|
+
execution_time=execution_time,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
# Command not found (test tool not available)
|
|
107
|
+
if result.returncode == -1 and "not found" in result.stderr.lower():
|
|
108
|
+
return self._create_skipped_result(reason=f"{command.split()[0]} not available")
|
|
109
|
+
|
|
110
|
+
# Treat "no tests collected" (exit code 5) as skipped, not failed
|
|
111
|
+
if result.returncode == 5:
|
|
112
|
+
execution_time = time.time() - start_time
|
|
113
|
+
return CheckResult(
|
|
114
|
+
checker_name=self.name(),
|
|
115
|
+
passed=True,
|
|
116
|
+
status="skipped",
|
|
117
|
+
errors=[],
|
|
118
|
+
warnings=[],
|
|
119
|
+
info={"reason": "no tests collected", "returncode": result.returncode},
|
|
120
|
+
execution_time=execution_time,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# Parse coverage
|
|
124
|
+
coverage = self._parse_coverage()
|
|
125
|
+
passed = result.returncode == 0
|
|
126
|
+
|
|
127
|
+
# Check coverage threshold
|
|
128
|
+
threshold = self.config.get("coverage_threshold", 80)
|
|
129
|
+
if coverage is not None and coverage < threshold:
|
|
130
|
+
passed = False
|
|
131
|
+
|
|
132
|
+
execution_time = time.time() - start_time
|
|
133
|
+
|
|
134
|
+
errors = []
|
|
135
|
+
if result.returncode != 0:
|
|
136
|
+
errors.append(
|
|
137
|
+
{
|
|
138
|
+
"message": f"Tests failed with exit code {result.returncode}",
|
|
139
|
+
"output": (result.stderr[:500] if result.stderr else ""), # Limit output
|
|
140
|
+
}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
if coverage is not None and coverage < threshold:
|
|
144
|
+
errors.append({"message": f"Coverage {coverage}% below threshold {threshold}%"})
|
|
145
|
+
|
|
146
|
+
return CheckResult(
|
|
147
|
+
checker_name=self.name(),
|
|
148
|
+
passed=passed,
|
|
149
|
+
status="passed" if passed else "failed",
|
|
150
|
+
errors=cast(list[Union[dict[str, Any], str]], errors),
|
|
151
|
+
warnings=[],
|
|
152
|
+
info={
|
|
153
|
+
"coverage": coverage,
|
|
154
|
+
"threshold": threshold,
|
|
155
|
+
"returncode": result.returncode,
|
|
156
|
+
"output": result.stdout[:1000] if result.stdout else "", # Limit output
|
|
157
|
+
},
|
|
158
|
+
execution_time=execution_time,
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
def _parse_coverage(self) -> float | None:
|
|
162
|
+
"""Parse coverage from test results."""
|
|
163
|
+
try:
|
|
164
|
+
if self.language == "python":
|
|
165
|
+
coverage_file = self.project_root / "coverage.json"
|
|
166
|
+
if coverage_file.exists():
|
|
167
|
+
with open(coverage_file) as f:
|
|
168
|
+
data = json.load(f)
|
|
169
|
+
return data.get("totals", {}).get("percent_covered", 0) # type: ignore[no-any-return]
|
|
170
|
+
|
|
171
|
+
elif self.language in ["javascript", "typescript"]:
|
|
172
|
+
coverage_file = self.project_root / "coverage" / "coverage-summary.json"
|
|
173
|
+
if coverage_file.exists():
|
|
174
|
+
with open(coverage_file) as f:
|
|
175
|
+
data = json.load(f)
|
|
176
|
+
return data.get("total", {}).get("lines", {}).get("pct", 0) # type: ignore[no-any-return]
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
except OSError as e:
|
|
180
|
+
logger.debug(f"Failed to read coverage file: {e}")
|
|
181
|
+
return None
|
|
182
|
+
except json.JSONDecodeError as e:
|
|
183
|
+
logger.debug(f"Failed to parse coverage file: {e}")
|
|
184
|
+
return None
|