devcouncil 0.3.1 → 0.4.0
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.
- package/README.md +46 -30
- package/package.json +6 -2
- package/packages/codeintel-grammars/hatch_build.py +43 -0
- package/packages/codeintel-grammars/pyproject.toml +16 -0
- package/packages/codeintel-grammars/src/devcouncil_codeintel_grammars/__init__.py +93 -0
- package/pyproject.toml +99 -4
- package/src/devcouncil/app/config.py +512 -20
- package/src/devcouncil/app/events.py +4 -23
- package/src/devcouncil/app/orchestrator.py +5 -0
- package/src/devcouncil/app/run_context.py +3 -3
- package/src/devcouncil/assets/__init__.py +4 -1
- package/src/devcouncil/assets/vendor/force-graph.min.js +5 -0
- package/src/devcouncil/campaign/__init__.py +71 -0
- package/src/devcouncil/campaign/bloom.py +137 -0
- package/src/devcouncil/campaign/dashboard.py +123 -0
- package/src/devcouncil/campaign/mailbox.py +305 -0
- package/src/devcouncil/campaign/notify.py +91 -0
- package/src/devcouncil/campaign/orchestrator.py +592 -0
- package/src/devcouncil/campaign/prompts/coordinator.md +29 -0
- package/src/devcouncil/campaign/prompts/director.md +21 -0
- package/src/devcouncil/campaign/prompts/protocol.md +46 -0
- package/src/devcouncil/campaign/prompts/reviewer.md +24 -0
- package/src/devcouncil/campaign/prompts/worker.md +24 -0
- package/src/devcouncil/campaign/roles.py +202 -0
- package/src/devcouncil/campaign/watcher.py +153 -0
- package/src/devcouncil/cli/commands/agents.py +24 -17
- package/src/devcouncil/cli/commands/artifacts.py +36 -27
- package/src/devcouncil/cli/commands/ast.py +12 -3
- package/src/devcouncil/cli/commands/baseline.py +21 -12
- package/src/devcouncil/cli/commands/boot.py +218 -0
- package/src/devcouncil/cli/commands/campaign.py +302 -0
- package/src/devcouncil/cli/commands/check.py +225 -12
- package/src/devcouncil/cli/commands/config.py +221 -74
- package/src/devcouncil/cli/commands/cost.py +137 -28
- package/src/devcouncil/cli/commands/dashboard.py +12 -4
- package/src/devcouncil/cli/commands/debug_cmd.py +249 -0
- package/src/devcouncil/cli/commands/design.py +27 -17
- package/src/devcouncil/cli/commands/doctor.py +790 -8
- package/src/devcouncil/cli/commands/evidence.py +41 -20
- package/src/devcouncil/cli/commands/export.py +73 -0
- package/src/devcouncil/cli/commands/gaps.py +175 -0
- package/src/devcouncil/cli/commands/gated_write.py +76 -0
- package/src/devcouncil/cli/commands/go.py +220 -68
- package/src/devcouncil/cli/commands/graph_cmd.py +1192 -0
- package/src/devcouncil/cli/commands/handoff.py +45 -34
- package/src/devcouncil/cli/commands/hook.py +630 -85
- package/src/devcouncil/cli/commands/init.py +89 -30
- package/src/devcouncil/cli/commands/integrate.py +296 -1385
- package/src/devcouncil/cli/commands/lease.py +120 -0
- package/src/devcouncil/cli/commands/logs.py +12 -5
- package/src/devcouncil/cli/commands/lsp.py +40 -5
- package/src/devcouncil/cli/commands/map.py +317 -74
- package/src/devcouncil/cli/commands/mcp_server.py +12 -2
- package/src/devcouncil/cli/commands/okf.py +44 -6
- package/src/devcouncil/cli/commands/plan.py +184 -69
- package/src/devcouncil/cli/commands/prompt.py +26 -17
- package/src/devcouncil/cli/commands/provenance.py +79 -0
- package/src/devcouncil/cli/commands/repair.py +60 -49
- package/src/devcouncil/cli/commands/report.py +148 -40
- package/src/devcouncil/cli/commands/requirements.py +104 -0
- package/src/devcouncil/cli/commands/reset_demo_state.py +13 -4
- package/src/devcouncil/cli/commands/rollback.py +46 -35
- package/src/devcouncil/cli/commands/run.py +173 -8
- package/src/devcouncil/cli/commands/runs.py +298 -68
- package/src/devcouncil/cli/commands/scaffold.py +33 -12
- package/src/devcouncil/cli/commands/semantic.py +29 -14
- package/src/devcouncil/cli/commands/setup.py +103 -93
- package/src/devcouncil/cli/commands/shell.py +51 -42
- package/src/devcouncil/cli/commands/show.py +56 -42
- package/src/devcouncil/cli/commands/skills.py +29 -20
- package/src/devcouncil/cli/commands/status.py +80 -67
- package/src/devcouncil/cli/commands/task_gate.py +295 -0
- package/src/devcouncil/cli/commands/tasks.py +248 -19
- package/src/devcouncil/cli/commands/trace.py +14 -8
- package/src/devcouncil/cli/commands/verify.py +33 -8
- package/src/devcouncil/cli/commands/version.py +14 -6
- package/src/devcouncil/cli/commands/watch.py +49 -30
- package/src/devcouncil/cli/commands/watch_fs.py +30 -19
- package/src/devcouncil/cli/commands/wiki.py +278 -0
- package/src/devcouncil/cli/main.py +58 -1
- package/src/devcouncil/codeintel/__init__.py +16 -0
- package/src/devcouncil/codeintel/build_control.py +429 -0
- package/src/devcouncil/codeintel/build_worker.py +78 -0
- package/src/devcouncil/codeintel/debug/__init__.py +17 -0
- package/src/devcouncil/codeintel/debug/broker.py +114 -0
- package/src/devcouncil/codeintel/debug/broker_client.py +61 -0
- package/src/devcouncil/codeintel/debug/consent.py +36 -0
- package/src/devcouncil/codeintel/debug/discovery.py +132 -0
- package/src/devcouncil/codeintel/debug/fingerprint.py +85 -0
- package/src/devcouncil/codeintel/debug/protocol.py +259 -0
- package/src/devcouncil/codeintel/debug/python_trace_runner.py +81 -0
- package/src/devcouncil/codeintel/debug/session.py +238 -0
- package/src/devcouncil/codeintel/debug/tracing.py +201 -0
- package/src/devcouncil/codeintel/languages/__init__.py +17 -0
- package/src/devcouncil/codeintel/languages/generic_extractor.py +236 -0
- package/src/devcouncil/codeintel/languages/registry.py +149 -0
- package/src/devcouncil/codeintel/languages/workers.py +245 -0
- package/src/devcouncil/codeintel/query/__init__.py +5 -0
- package/src/devcouncil/codeintel/query/engine.py +289 -0
- package/src/devcouncil/codeintel/resolution/__init__.py +6 -0
- package/src/devcouncil/codeintel/resolution/abstract_state.py +301 -0
- package/src/devcouncil/codeintel/resolution/frameworks/__init__.py +33 -0
- package/src/devcouncil/codeintel/resolution/frameworks/base.py +46 -0
- package/src/devcouncil/codeintel/resolution/frameworks/di.py +56 -0
- package/src/devcouncil/codeintel/resolution/frameworks/events.py +45 -0
- package/src/devcouncil/codeintel/resolution/frameworks/routes.py +88 -0
- package/src/devcouncil/codeintel/resolution/semantic.py +887 -0
- package/src/devcouncil/codeintel/service.py +104 -0
- package/src/devcouncil/codeintel/store/__init__.py +15 -0
- package/src/devcouncil/codeintel/store/sqlite.py +1565 -0
- package/src/devcouncil/codeintel/sync/__init__.py +19 -0
- package/src/devcouncil/codeintel/sync/coordinator.py +430 -0
- package/src/devcouncil/codeintel/sync/incremental.py +484 -0
- package/src/devcouncil/codeintel/sync/lease.py +96 -0
- package/src/devcouncil/codeintel/sync/scope.py +98 -0
- package/src/devcouncil/council/__init__.py +4 -0
- package/src/devcouncil/council/prompts/__init__.py +4 -0
- package/src/devcouncil/domain/checkpoint_refs.py +17 -0
- package/src/devcouncil/domain/evidence.py +1 -0
- package/src/devcouncil/domain/gap.py +10 -0
- package/src/devcouncil/domain/requirement.py +5 -1
- package/src/devcouncil/domain/task.py +43 -2
- package/src/devcouncil/execution/checkpoints.py +25 -31
- package/src/devcouncil/execution/context_builder.py +15 -44
- package/src/devcouncil/execution/fs_watcher.py +64 -0
- package/src/devcouncil/execution/gated_write.py +203 -0
- package/src/devcouncil/execution/handoff.py +2 -1
- package/src/devcouncil/execution/hook_policy.py +19 -5
- package/src/devcouncil/execution/lease_ops.py +177 -0
- package/src/devcouncil/execution/lease_validation.py +71 -0
- package/src/devcouncil/execution/patch.py +3 -0
- package/src/devcouncil/execution/permissions.py +1 -0
- package/src/devcouncil/execution/policy_engine.py +205 -10
- package/src/devcouncil/execution/prompt_builder.py +278 -33
- package/src/devcouncil/execution/run_trace.py +356 -0
- package/src/devcouncil/execution/shell_session.py +46 -5
- package/src/devcouncil/execution/stop_gate.py +746 -0
- package/src/devcouncil/execution/stop_gate_history.py +113 -0
- package/src/devcouncil/execution/stop_gate_state.py +54 -0
- package/src/devcouncil/execution/stop_gate_verify_cache.py +69 -0
- package/src/devcouncil/execution/task_gate_ops.py +590 -0
- package/src/devcouncil/execution/task_runner.py +19 -0
- package/src/devcouncil/executors/advisor_tool.py +315 -0
- package/src/devcouncil/executors/agent_registry.py +125 -17
- package/src/devcouncil/executors/claude_sdk.py +376 -0
- package/src/devcouncil/executors/coding_cli.py +724 -25
- package/src/devcouncil/executors/mini_swe.py +50 -8
- package/src/devcouncil/executors/native/agent.py +224 -19
- package/src/devcouncil/executors/openhands.py +50 -8
- package/src/devcouncil/executors/transient_retry.py +99 -0
- package/src/devcouncil/gating/checks/clean_git.py +5 -2
- package/src/devcouncil/gating/checks/planned_files_check.py +38 -11
- package/src/devcouncil/gating/checks/secret_scan_check.py +2 -2
- package/src/devcouncil/gating/policy.py +46 -2
- package/src/devcouncil/indexing/ast_matcher.py +41 -4
- package/src/devcouncil/indexing/graph/__init__.py +78 -0
- package/src/devcouncil/indexing/graph/api_routes.py +522 -0
- package/src/devcouncil/indexing/graph/build.py +862 -0
- package/src/devcouncil/indexing/graph/cache.py +329 -0
- package/src/devcouncil/indexing/graph/communities.py +28 -0
- package/src/devcouncil/indexing/graph/cypher.py +107 -0
- package/src/devcouncil/indexing/graph/embeddings.py +194 -0
- package/src/devcouncil/indexing/graph/export.py +381 -0
- package/src/devcouncil/indexing/graph/export_links.py +81 -0
- package/src/devcouncil/indexing/graph/extract_python.py +307 -0
- package/src/devcouncil/indexing/graph/extract_ts.py +1205 -0
- package/src/devcouncil/indexing/graph/intel.py +668 -0
- package/src/devcouncil/indexing/graph/liveness.py +992 -0
- package/src/devcouncil/indexing/graph/okf_export.py +65 -0
- package/src/devcouncil/indexing/graph/pdg/__init__.py +67 -0
- package/src/devcouncil/indexing/graph/pdg/build.py +11 -0
- package/src/devcouncil/indexing/graph/pdg/cdg.py +41 -0
- package/src/devcouncil/indexing/graph/pdg/cfg.py +199 -0
- package/src/devcouncil/indexing/graph/pdg/query.py +21 -0
- package/src/devcouncil/indexing/graph/pdg/reaching_def.py +126 -0
- package/src/devcouncil/indexing/graph/pdg/schema.py +253 -0
- package/src/devcouncil/indexing/graph/pdg/taint.py +154 -0
- package/src/devcouncil/indexing/graph/query.py +302 -0
- package/src/devcouncil/indexing/graph/resolve.py +1020 -0
- package/src/devcouncil/indexing/graph/schema.py +103 -0
- package/src/devcouncil/indexing/graph_index.py +20 -29
- package/src/devcouncil/indexing/lsp.py +57 -25
- package/src/devcouncil/indexing/lsp_client.py +577 -0
- package/src/devcouncil/indexing/map_artifacts.py +355 -0
- package/src/devcouncil/indexing/map_refresh.py +141 -0
- package/src/devcouncil/indexing/repo_mapper.py +1509 -138
- package/src/devcouncil/indexing/semantic_index.py +12 -6
- package/src/devcouncil/indexing/subsystem_map.py +163 -0
- package/src/devcouncil/indexing/ts_imports.py +343 -0
- package/src/devcouncil/indexing/viz.py +960 -0
- package/src/devcouncil/indexing/walk.py +52 -0
- package/src/devcouncil/indexing/wiring.py +1776 -0
- package/src/devcouncil/integrations/actions.py +27 -4
- package/src/devcouncil/integrations/check.py +211 -16
- package/src/devcouncil/integrations/claude_assets.py +209 -12
- package/src/devcouncil/integrations/clients/__init__.py +1 -0
- package/src/devcouncil/integrations/clients/aider.py +52 -0
- package/src/devcouncil/integrations/clients/antigravity.py +87 -0
- package/src/devcouncil/integrations/clients/claude.py +339 -0
- package/src/devcouncil/integrations/clients/codex.py +39 -0
- package/src/devcouncil/integrations/clients/common.py +332 -0
- package/src/devcouncil/integrations/clients/cursor.py +164 -0
- package/src/devcouncil/integrations/clients/gemini.py +49 -0
- package/src/devcouncil/integrations/clients/grok.py +105 -0
- package/src/devcouncil/integrations/clients/hooks.py +500 -0
- package/src/devcouncil/integrations/clients/opencode.py +96 -0
- package/src/devcouncil/integrations/clients/warp.py +75 -0
- package/src/devcouncil/integrations/code_review_graph.py +2 -2
- package/src/devcouncil/integrations/github.py +73 -7
- package/src/devcouncil/integrations/integration_cli.py +197 -0
- package/src/devcouncil/integrations/mcp/handlers/__init__.py +1 -0
- package/src/devcouncil/integrations/mcp/handlers/ast_lsp.py +77 -0
- package/src/devcouncil/integrations/mcp/handlers/checkout.py +50 -0
- package/src/devcouncil/integrations/mcp/handlers/cli_gate.py +43 -0
- package/src/devcouncil/integrations/mcp/handlers/codeintel.py +182 -0
- package/src/devcouncil/integrations/mcp/handlers/debug.py +236 -0
- package/src/devcouncil/integrations/mcp/handlers/evidence.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/git.py +99 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +36 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +30 -0
- package/src/devcouncil/integrations/mcp/handlers/lease.py +70 -0
- package/src/devcouncil/integrations/mcp/handlers/live.py +108 -0
- package/src/devcouncil/integrations/mcp/handlers/map.py +676 -0
- package/src/devcouncil/integrations/mcp/handlers/next_task.py +35 -0
- package/src/devcouncil/integrations/mcp/handlers/policy.py +80 -0
- package/src/devcouncil/integrations/mcp/handlers/prompts.py +168 -0
- package/src/devcouncil/integrations/mcp/handlers/provenance.py +101 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +74 -0
- package/src/devcouncil/integrations/mcp/handlers/router_cache.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/run.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/runs.py +84 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +114 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +100 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +904 -0
- package/src/devcouncil/integrations/mcp/handlers/trace.py +65 -0
- package/src/devcouncil/integrations/mcp/handlers/verify.py +45 -0
- package/src/devcouncil/integrations/mcp/handlers/wiki.py +60 -0
- package/src/devcouncil/integrations/mcp/handlers/write.py +69 -0
- package/src/devcouncil/integrations/mcp/server.py +265 -2391
- package/src/devcouncil/integrations/mcp/util.py +303 -0
- package/src/devcouncil/integrations/setup.py +152 -0
- package/src/devcouncil/knowledge/fetch.py +4 -0
- package/src/devcouncil/knowledge/knowledge_select.py +38 -0
- package/src/devcouncil/knowledge/okf.py +2 -1
- package/src/devcouncil/knowledge/resource_discovery.py +40 -0
- package/src/devcouncil/knowledge/wiki.py +643 -0
- package/src/devcouncil/knowledge/wiki_read.py +87 -0
- package/src/devcouncil/live/cards.py +7 -7
- package/src/devcouncil/live/models.py +4 -1
- package/src/devcouncil/live/reviewer.py +90 -11
- package/src/devcouncil/live/signals.py +4 -2
- package/src/devcouncil/live/tasks.py +12 -3
- package/src/devcouncil/live/transcripts.py +69 -2
- package/src/devcouncil/llm/cache.py +5 -6
- package/src/devcouncil/llm/model_defaults.yaml +10 -10
- package/src/devcouncil/llm/provider.py +647 -73
- package/src/devcouncil/llm/router.py +271 -46
- package/src/devcouncil/llm/semantic_bridge.py +614 -0
- package/src/devcouncil/optimization/gepa_agent.py +6 -4
- package/src/devcouncil/optimization/skillopt.py +9 -5
- package/src/devcouncil/planning/arbiter_service.py +12 -3
- package/src/devcouncil/planning/correction_manifest.py +107 -10
- package/src/devcouncil/planning/plan_difficulty.py +69 -0
- package/src/devcouncil/planning/plan_service.py +5 -2
- package/src/devcouncil/planning/planned_files_reconcile.py +191 -0
- package/src/devcouncil/planning/prompt_enhancer_service.py +6 -5
- package/src/devcouncil/planning/question_conversion.py +56 -0
- package/src/devcouncil/planning/spec_service.py +9 -3
- package/src/devcouncil/repo/ci_scaffold.py +197 -1
- package/src/devcouncil/repo/gitignore.py +1 -2
- package/src/devcouncil/reporting/evidence_export.py +124 -0
- package/src/devcouncil/reporting/evidence_html.py +210 -0
- package/src/devcouncil/reporting/json_report.py +16 -12
- package/src/devcouncil/reporting/markdown_report.py +38 -9
- package/src/devcouncil/reporting/mcp_resources.py +142 -0
- package/src/devcouncil/reporting/report_builder.py +40 -4
- package/src/devcouncil/reporting/task_provenance.py +42 -0
- package/src/devcouncil/reporting/verdict.py +75 -0
- package/src/devcouncil/skills/library/README.md +1 -0
- package/src/devcouncil/skills/library/devcouncil-hero-loop.md +109 -0
- package/src/devcouncil/skills/library/devcouncil-verification.md +109 -0
- package/src/devcouncil/skills/library/devcouncil.md +93 -0
- package/src/devcouncil/skills/registry.py +43 -12
- package/src/devcouncil/storage/db.py +57 -11
- package/src/devcouncil/storage/models.py +6 -0
- package/src/devcouncil/storage/native.py +5 -3
- package/src/devcouncil/storage/repositories.py +50 -18
- package/src/devcouncil/telemetry/context.py +28 -0
- package/src/devcouncil/telemetry/cost.py +4 -5
- package/src/devcouncil/telemetry/logging_setup.py +78 -11
- package/src/devcouncil/telemetry/model_pricing.yaml +7 -0
- package/src/devcouncil/telemetry/stages.py +27 -2
- package/src/devcouncil/telemetry/tracker.py +50 -13
- package/src/devcouncil/ui/dashboard.py +120 -8
- package/src/devcouncil/utils/fsio.py +58 -0
- package/src/devcouncil/utils/git_snapshot.py +112 -0
- package/src/devcouncil/utils/json_persist.py +53 -0
- package/src/devcouncil/utils/proc.py +89 -0
- package/src/devcouncil/verification/acceptance_compiler.py +36 -13
- package/src/devcouncil/verification/ad_hoc_check.py +95 -3
- package/src/devcouncil/verification/checks/__init__.py +41 -0
- package/src/devcouncil/verification/checks/acceptance.py +39 -0
- package/src/devcouncil/verification/checks/acceptance_corpus.py +194 -0
- package/src/devcouncil/verification/checks/acceptance_evidence.py +239 -0
- package/src/devcouncil/verification/checks/command_evidence.py +148 -0
- package/src/devcouncil/verification/checks/compiled_acceptance.py +179 -0
- package/src/devcouncil/verification/checks/corpus_stale.py +124 -0
- package/src/devcouncil/verification/checks/corpus_verification.py +9 -0
- package/src/devcouncil/verification/checks/dead_symbols.py +360 -0
- package/src/devcouncil/verification/checks/diff_coverage_gate.py +101 -0
- package/src/devcouncil/verification/checks/doc_code_ref.py +79 -0
- package/src/devcouncil/verification/checks/liveness_ratchet.py +336 -0
- package/src/devcouncil/verification/checks/orphan_diff.py +104 -0
- package/src/devcouncil/verification/checks/planned_files.py +98 -0
- package/src/devcouncil/verification/checks/semantic_diff.py +241 -0
- package/src/devcouncil/verification/checks/stale_map.py +80 -0
- package/src/devcouncil/verification/checks/stub_scan.py +71 -0
- package/src/devcouncil/verification/checks/subsystem_boundary.py +103 -0
- package/src/devcouncil/verification/checks/wiring.py +216 -0
- package/src/devcouncil/verification/claims/__init__.py +23 -0
- package/src/devcouncil/verification/claims/checks.py +395 -0
- package/src/devcouncil/verification/claims/mapper.py +168 -0
- package/src/devcouncil/verification/claims/models.py +39 -0
- package/src/devcouncil/verification/claims/transcript.py +92 -0
- package/src/devcouncil/verification/claims/verdict.py +88 -0
- package/src/devcouncil/verification/command_evidence.py +170 -0
- package/src/devcouncil/verification/command_malformation.py +147 -0
- package/src/devcouncil/verification/command_runner.py +164 -0
- package/src/devcouncil/verification/coverage_measurement.py +292 -0
- package/src/devcouncil/verification/diff_coverage.py +151 -0
- package/src/devcouncil/verification/difficulty.py +296 -0
- package/src/devcouncil/verification/effort_heuristics.py +178 -0
- package/src/devcouncil/verification/gap_ids.py +63 -0
- package/src/devcouncil/verification/gate_cache.py +194 -0
- package/src/devcouncil/verification/gate_selector.py +344 -0
- package/src/devcouncil/verification/git_diff_fallback.py +272 -0
- package/src/devcouncil/verification/implementation_reviewer.py +13 -0
- package/src/devcouncil/verification/incremental_check.py +241 -0
- package/src/devcouncil/verification/next_actions.py +60 -1
- package/src/devcouncil/verification/rigor_analytics.py +130 -0
- package/src/devcouncil/verification/sandbox.py +38 -11
- package/src/devcouncil/verification/stub_detector.py +369 -0
- package/src/devcouncil/verification/test_resolver.py +67 -1
- package/src/devcouncil/verification/verifier.py +137 -1666
- package/src/devcouncil/verification/verify_orchestration.py +610 -0
- package/src/devcouncil/verification/verify_setup.py +176 -0
- package/src/devcouncil/verification/wiki_refresh.py +208 -0
- package/src/semantic_layer/__init__.py +58 -0
- package/src/semantic_layer/benchmark.py +75 -0
- package/src/semantic_layer/cache.py +290 -0
- package/src/semantic_layer/compressor.py +137 -0
- package/src/semantic_layer/config.py +75 -0
- package/src/semantic_layer/embeddings.py +69 -0
- package/src/semantic_layer/llm_backends.py +99 -0
- package/src/semantic_layer/pipeline.py +111 -0
- package/src/semantic_layer/router.py +128 -0
- package/src/semantic_layer/tuner.py +72 -0
- package/uv.lock +973 -9
- package/src/devcouncil/artifacts/migrations.py +0 -20
- package/src/devcouncil/artifacts/schemas.py +0 -23
- package/src/devcouncil/artifacts/serializer.py +0 -21
- package/src/devcouncil/integrations/gitnexus.py +0 -70
- package/src/devcouncil/integrations/graphify.py +0 -34
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""Detect when a verification command failed to run vs. a real test failure."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional, Tuple
|
|
8
|
+
|
|
9
|
+
from devcouncil.domain.evidence import CommandResult
|
|
10
|
+
|
|
11
|
+
# Signatures that mean the verification command itself could not run (or had
|
|
12
|
+
# nothing to run), so its non-zero exit says nothing about whether the
|
|
13
|
+
# implementation is correct — a tooling/plan defect, not a code defect.
|
|
14
|
+
MALFORMED_COMMAND_SIGNATURES = (
|
|
15
|
+
"syntaxerror",
|
|
16
|
+
"invalid syntax",
|
|
17
|
+
"indentationerror",
|
|
18
|
+
"no module named",
|
|
19
|
+
"can't open file",
|
|
20
|
+
"no such file or directory",
|
|
21
|
+
"file or directory not found",
|
|
22
|
+
"no tests ran",
|
|
23
|
+
"no tests collected",
|
|
24
|
+
"error: not found",
|
|
25
|
+
"is not recognized as an internal or external command",
|
|
26
|
+
"command not found",
|
|
27
|
+
"executable file not found",
|
|
28
|
+
"failed to run command",
|
|
29
|
+
"importerror",
|
|
30
|
+
"modulenotfounderror",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
UNCONDITIONAL_UNRUNNABLE_SIGNATURES = (
|
|
34
|
+
"syntaxerror",
|
|
35
|
+
"invalid syntax",
|
|
36
|
+
"indentationerror",
|
|
37
|
+
"can't open file",
|
|
38
|
+
"is not recognized as an internal or external command",
|
|
39
|
+
"command not found",
|
|
40
|
+
"executable file not found",
|
|
41
|
+
"failed to run command",
|
|
42
|
+
"no tests ran",
|
|
43
|
+
"no tests collected",
|
|
44
|
+
"error: not found",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
PYTEST_NONRUN_EXIT_CODES = {4, 5}
|
|
48
|
+
|
|
49
|
+
TRACEBACK_FRAME_RE = re.compile(r'File "(?P<file>[^"]+)", line (?P<line>\d+)')
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def is_traceback_frame(line: str) -> bool:
|
|
53
|
+
"""True for a Python traceback frame line: `` File "...", line N``."""
|
|
54
|
+
stripped = line.strip()
|
|
55
|
+
return stripped.startswith('File "') and ", line " in stripped
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def malformed_signature_precedes_traceback(text: str) -> bool:
|
|
59
|
+
"""Decide whether an unrunnable-launcher signature is authoritative."""
|
|
60
|
+
if not text:
|
|
61
|
+
return False
|
|
62
|
+
low_all = text.lower()
|
|
63
|
+
if any(sig in low_all for sig in UNCONDITIONAL_UNRUNNABLE_SIGNATURES):
|
|
64
|
+
return True
|
|
65
|
+
lines = text.splitlines()
|
|
66
|
+
lowered_lines = [ln.lower() for ln in lines]
|
|
67
|
+
first_frame_idx: Optional[int] = None
|
|
68
|
+
for idx, line in enumerate(lines):
|
|
69
|
+
if is_traceback_frame(line):
|
|
70
|
+
first_frame_idx = idx
|
|
71
|
+
break
|
|
72
|
+
for idx, low in enumerate(lowered_lines):
|
|
73
|
+
if any(sig in low for sig in MALFORMED_COMMAND_SIGNATURES):
|
|
74
|
+
if first_frame_idx is None or idx < first_frame_idx:
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
return False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def launcher_text(result: CommandResult) -> str:
|
|
81
|
+
"""Captured output for launcher-vs-test analysis, ordered stderr then stdout."""
|
|
82
|
+
parts: list[str] = []
|
|
83
|
+
for path in (result.stderr_path, result.stdout_path):
|
|
84
|
+
if not path:
|
|
85
|
+
continue
|
|
86
|
+
try:
|
|
87
|
+
content = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
88
|
+
if content.strip():
|
|
89
|
+
parts.append(content)
|
|
90
|
+
except Exception:
|
|
91
|
+
pass
|
|
92
|
+
if parts:
|
|
93
|
+
return "\n".join(parts)
|
|
94
|
+
return result.summary or ""
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def relativize(project_root: Path, raw_path: str) -> str:
|
|
98
|
+
"""Normalize a traceback file path to a repo-relative posix path when possible."""
|
|
99
|
+
normalized = raw_path.replace("\\", "/")
|
|
100
|
+
try:
|
|
101
|
+
candidate = Path(raw_path)
|
|
102
|
+
if candidate.is_absolute():
|
|
103
|
+
rel = candidate.resolve().relative_to(project_root.resolve())
|
|
104
|
+
return rel.as_posix()
|
|
105
|
+
except Exception:
|
|
106
|
+
pass
|
|
107
|
+
return normalized
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def failure_location(project_root: Path, result: CommandResult) -> Tuple[Optional[str], Optional[int]]:
|
|
111
|
+
"""Best-effort (file, line) of a failing command's deepest traceback frame."""
|
|
112
|
+
sources: list[str] = []
|
|
113
|
+
for path in (result.stdout_path, result.stderr_path):
|
|
114
|
+
if path:
|
|
115
|
+
try:
|
|
116
|
+
content = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
117
|
+
if content.strip():
|
|
118
|
+
sources.append(content)
|
|
119
|
+
except Exception:
|
|
120
|
+
pass
|
|
121
|
+
sources.append(result.summary or "")
|
|
122
|
+
best_file: Optional[str] = None
|
|
123
|
+
best_line: Optional[int] = None
|
|
124
|
+
for text in sources:
|
|
125
|
+
for match in TRACEBACK_FRAME_RE.finditer(text):
|
|
126
|
+
raw_file = match.group("file")
|
|
127
|
+
if not raw_file or raw_file.startswith("<"):
|
|
128
|
+
continue
|
|
129
|
+
best_file = relativize(project_root, raw_file)
|
|
130
|
+
try:
|
|
131
|
+
best_line = int(match.group("line"))
|
|
132
|
+
except ValueError:
|
|
133
|
+
best_line = None
|
|
134
|
+
if best_file is not None:
|
|
135
|
+
return best_file, best_line
|
|
136
|
+
return best_file, best_line
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def command_is_malformed(result: CommandResult) -> bool:
|
|
140
|
+
"""True when a non-zero exit reflects a broken/unrunnable command."""
|
|
141
|
+
if result.timed_out:
|
|
142
|
+
return False
|
|
143
|
+
is_pytest = "pytest" in (result.command or "")
|
|
144
|
+
if is_pytest and result.exit_code in PYTEST_NONRUN_EXIT_CODES:
|
|
145
|
+
return True
|
|
146
|
+
text = launcher_text(result)
|
|
147
|
+
return malformed_signature_precedes_traceback(text)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Run verification evidence commands with sanitized env and logging.
|
|
2
|
+
|
|
3
|
+
Extracted from ``verifier.py`` so orchestration stays focused on gate logic.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import hashlib
|
|
9
|
+
import logging
|
|
10
|
+
import shlex
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List
|
|
16
|
+
|
|
17
|
+
from devcouncil.domain.evidence import CommandResult
|
|
18
|
+
from devcouncil.utils.redaction import redact_string
|
|
19
|
+
from devcouncil.utils.subprocess_env import clean_subprocess_env
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _timeout_stream(content: str | bytes | None) -> str:
|
|
25
|
+
"""Normalize partial output attached to ``TimeoutExpired``."""
|
|
26
|
+
if isinstance(content, bytes):
|
|
27
|
+
return content.decode("utf-8", errors="replace")
|
|
28
|
+
return content or ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def split_command(command: str) -> List[str]:
|
|
32
|
+
# Use POSIX splitting so quotes are interpreted, not preserved. With
|
|
33
|
+
# posix=False, `python -c "assert x"` keeps the surrounding quotes, so the
|
|
34
|
+
# interpreter receives the literal string `"assert x"` and treats it as a
|
|
35
|
+
# no-op string expression that exits 0 — every quoted-argument evidence
|
|
36
|
+
# command would then silently "pass" without running, producing false
|
|
37
|
+
# verification. posix=True strips the quotes correctly; planner-generated
|
|
38
|
+
# commands use forward-slash paths, which the interpreter accepts on Windows.
|
|
39
|
+
return shlex.split(command, posix=True)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def summarize_stream(content: str, budget: int = 360) -> str:
|
|
43
|
+
"""Condense a command's stdout/stderr for the evidence summary."""
|
|
44
|
+
if not content or not content.strip():
|
|
45
|
+
return "(empty)"
|
|
46
|
+
lines = [ln.rstrip() for ln in content.splitlines() if ln.strip()]
|
|
47
|
+
markers = ("error", "exception", "assert", "traceback", "failed", "not found", "no module named")
|
|
48
|
+
salient = ""
|
|
49
|
+
for ln in reversed(lines):
|
|
50
|
+
low = ln.lower()
|
|
51
|
+
if any(m in low for m in markers):
|
|
52
|
+
salient = ln.strip()
|
|
53
|
+
break
|
|
54
|
+
if not salient:
|
|
55
|
+
salient = lines[-1].strip()
|
|
56
|
+
salient = salient[:240]
|
|
57
|
+
tail = content.strip()[-budget:]
|
|
58
|
+
summary = f"{salient} | {tail}" if salient not in tail[: len(salient) + 5] else tail
|
|
59
|
+
return summary[: budget + len(salient) + 8]
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_command_log(
|
|
63
|
+
project_root: Path,
|
|
64
|
+
label: str,
|
|
65
|
+
command: str,
|
|
66
|
+
stream: str,
|
|
67
|
+
content: str,
|
|
68
|
+
) -> str:
|
|
69
|
+
"""Save command output to a log file and return the path."""
|
|
70
|
+
log_dir = project_root / ".devcouncil" / "logs"
|
|
71
|
+
log_dir.mkdir(parents=True, exist_ok=True)
|
|
72
|
+
cmd_hash = hashlib.sha256(command.encode()).hexdigest()[:8]
|
|
73
|
+
filename = f"{label}-{cmd_hash}-{stream}.log"
|
|
74
|
+
log_path = log_dir / filename
|
|
75
|
+
log_path.write_text(redact_string(content), encoding="utf-8")
|
|
76
|
+
return str(log_path)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def run_verification_command(
|
|
80
|
+
project_root: Path,
|
|
81
|
+
command: str,
|
|
82
|
+
*,
|
|
83
|
+
task_id: str = "verify",
|
|
84
|
+
timeout: int = 900,
|
|
85
|
+
) -> CommandResult:
|
|
86
|
+
"""Execute one evidence command in the target repo with a cleaned environment."""
|
|
87
|
+
env = clean_subprocess_env()
|
|
88
|
+
try:
|
|
89
|
+
argv = split_command(command)
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
return CommandResult(
|
|
92
|
+
command=command,
|
|
93
|
+
exit_code=-1,
|
|
94
|
+
stdout_path="",
|
|
95
|
+
stderr_path="",
|
|
96
|
+
summary=f"Failed to run command: unparseable shell syntax ({e})",
|
|
97
|
+
)
|
|
98
|
+
if argv and argv[0] in {"python", "python3"}:
|
|
99
|
+
# Planner-generated checks use portable launcher names, but macOS may have
|
|
100
|
+
# no ``python`` and its system ``python3`` does not contain project deps.
|
|
101
|
+
# Verification must run under the interpreter hosting DevCouncil.
|
|
102
|
+
argv = [sys.executable, *argv[1:]]
|
|
103
|
+
elif argv:
|
|
104
|
+
resolved = shutil.which(argv[0], path=env.get("PATH"))
|
|
105
|
+
if resolved:
|
|
106
|
+
argv = [resolved, *argv[1:]]
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
result = subprocess.run(
|
|
110
|
+
argv,
|
|
111
|
+
shell=False,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
text=True,
|
|
114
|
+
encoding="utf-8",
|
|
115
|
+
errors="replace",
|
|
116
|
+
cwd=project_root,
|
|
117
|
+
timeout=timeout,
|
|
118
|
+
env=env,
|
|
119
|
+
)
|
|
120
|
+
stdout = result.stdout or ""
|
|
121
|
+
stderr = result.stderr or ""
|
|
122
|
+
stdout_path = save_command_log(project_root, task_id, command, "stdout", stdout)
|
|
123
|
+
stderr_path = save_command_log(project_root, task_id, command, "stderr", stderr)
|
|
124
|
+
stdout_summary = redact_string(summarize_stream(stdout))
|
|
125
|
+
stderr_summary = redact_string(summarize_stream(stderr))
|
|
126
|
+
return CommandResult(
|
|
127
|
+
command=command,
|
|
128
|
+
exit_code=result.returncode,
|
|
129
|
+
stdout_path=stdout_path,
|
|
130
|
+
stderr_path=stderr_path,
|
|
131
|
+
summary=(
|
|
132
|
+
f"Exit code {result.returncode}. "
|
|
133
|
+
f"stderr: {stderr_summary}. "
|
|
134
|
+
f"stdout: {stdout_summary}"
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
except subprocess.TimeoutExpired as exc:
|
|
138
|
+
stdout = _timeout_stream(exc.stdout)
|
|
139
|
+
stderr = _timeout_stream(exc.stderr)
|
|
140
|
+
stdout_path = save_command_log(project_root, task_id, command, "stdout", stdout)
|
|
141
|
+
stderr_path = save_command_log(project_root, task_id, command, "stderr", stderr)
|
|
142
|
+
stdout_summary = redact_string(summarize_stream(stdout))
|
|
143
|
+
stderr_summary = redact_string(summarize_stream(stderr))
|
|
144
|
+
return CommandResult(
|
|
145
|
+
command=command,
|
|
146
|
+
exit_code=124,
|
|
147
|
+
stdout_path=stdout_path,
|
|
148
|
+
stderr_path=stderr_path,
|
|
149
|
+
summary=(
|
|
150
|
+
f"Verification command timed out after {timeout} seconds. "
|
|
151
|
+
f"stderr: {stderr_summary}. "
|
|
152
|
+
f"stdout: {stdout_summary}"
|
|
153
|
+
),
|
|
154
|
+
timed_out=True,
|
|
155
|
+
)
|
|
156
|
+
except Exception as e:
|
|
157
|
+
logger.debug("Command run failed for %r: %s", command, e)
|
|
158
|
+
return CommandResult(
|
|
159
|
+
command=command,
|
|
160
|
+
exit_code=-1,
|
|
161
|
+
stdout_path="",
|
|
162
|
+
stderr_path="",
|
|
163
|
+
summary=f"Failed to run command: {e}",
|
|
164
|
+
)
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Diff-coverage instrumentation extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, Dict, List, Optional
|
|
12
|
+
|
|
13
|
+
from devcouncil.app.config import load_config
|
|
14
|
+
from devcouncil.domain.task import Task
|
|
15
|
+
from devcouncil.utils.subprocess_env import clean_subprocess_env
|
|
16
|
+
from devcouncil.verification import command_runner as cmd_runner
|
|
17
|
+
from devcouncil.verification import diff_coverage as dc
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
SplitCommand = Callable[[str], List[str]]
|
|
22
|
+
VerificationEnv = Callable[[], Dict[str, str]]
|
|
23
|
+
LoadCommands = Callable[[], Dict[str, List[str]]]
|
|
24
|
+
CommandCanProve = Callable[[str, str], bool]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DiffCoverageMeasurer:
|
|
28
|
+
"""Run task test commands under coverage and intersect with a diff."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
project_root: Path,
|
|
33
|
+
*,
|
|
34
|
+
get_coverage_python: Callable[[], Optional[str]],
|
|
35
|
+
split_command: SplitCommand,
|
|
36
|
+
verification_env: VerificationEnv,
|
|
37
|
+
load_commands: LoadCommands,
|
|
38
|
+
command_can_prove_acceptance: CommandCanProve,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.project_root = project_root
|
|
41
|
+
self._get_coverage_python = get_coverage_python
|
|
42
|
+
self._split_command = split_command
|
|
43
|
+
self._verification_env = verification_env
|
|
44
|
+
self._load_commands = load_commands
|
|
45
|
+
self._command_can_prove_acceptance = command_can_prove_acceptance
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def from_verifier(cls, verifier: object) -> DiffCoverageMeasurer:
|
|
49
|
+
"""Build a measurer bound to a Verifier instance."""
|
|
50
|
+
return cls(
|
|
51
|
+
verifier.project_root, # type: ignore[attr-defined]
|
|
52
|
+
get_coverage_python=lambda: verifier._coverage_python, # type: ignore[attr-defined]
|
|
53
|
+
split_command=cmd_runner.split_command,
|
|
54
|
+
verification_env=clean_subprocess_env,
|
|
55
|
+
load_commands=verifier._load_commands, # type: ignore[attr-defined]
|
|
56
|
+
command_can_prove_acceptance=verifier._command_can_prove_acceptance, # type: ignore[attr-defined]
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
def coverage_target_commands(self, task: Task) -> List[str]:
|
|
60
|
+
"""The test command(s) to instrument — the ones that purport to prove the ACs."""
|
|
61
|
+
if task.expected_tests:
|
|
62
|
+
return list(task.expected_tests)
|
|
63
|
+
test_like = [
|
|
64
|
+
c for c in task.allowed_commands
|
|
65
|
+
if self._command_can_prove_acceptance("allowed", c)
|
|
66
|
+
]
|
|
67
|
+
if test_like:
|
|
68
|
+
return test_like
|
|
69
|
+
return list(self._load_commands().get("test", []))
|
|
70
|
+
|
|
71
|
+
def measure(self, task: Task, diff_content: str) -> dc.DiffCoverageResult:
|
|
72
|
+
"""Run instrumentation for Python, JS/TS, and Go changed paths."""
|
|
73
|
+
parsed = dc.parse_changed_lines(diff_content)
|
|
74
|
+
py_changed = dc.measurable_python_changes(parsed)
|
|
75
|
+
js_changed = dc.measurable_js_changes(parsed)
|
|
76
|
+
go_changed = dc.measurable_go_changes(parsed)
|
|
77
|
+
if not py_changed and not js_changed and not go_changed:
|
|
78
|
+
return dc.DiffCoverageResult(measured=False, reason="no measurable source changes in diff")
|
|
79
|
+
|
|
80
|
+
commands = self.coverage_target_commands(task)
|
|
81
|
+
if not commands:
|
|
82
|
+
return dc.DiffCoverageResult(measured=False, reason="no test command to instrument")
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
timeout = load_config(self.project_root).execution.command_timeout
|
|
86
|
+
except Exception:
|
|
87
|
+
timeout = 300
|
|
88
|
+
|
|
89
|
+
env = self._verification_env()
|
|
90
|
+
tmp_dir = self.project_root / ".devcouncil" / "tmp"
|
|
91
|
+
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
92
|
+
results: List[dc.DiffCoverageResult] = []
|
|
93
|
+
|
|
94
|
+
if py_changed:
|
|
95
|
+
results.append(self._measure_python(task, commands, py_changed, tmp_dir, env, timeout))
|
|
96
|
+
if js_changed:
|
|
97
|
+
results.append(self._measure_js(task, commands, js_changed, tmp_dir, env, timeout))
|
|
98
|
+
if go_changed:
|
|
99
|
+
results.append(self._measure_go(task, commands, go_changed, tmp_dir, env, timeout))
|
|
100
|
+
|
|
101
|
+
return dc.merge_diff_coverage_results(results)
|
|
102
|
+
|
|
103
|
+
def _resolve_coverage_python(self, env: Dict[str, str]) -> str:
|
|
104
|
+
override = self._get_coverage_python()
|
|
105
|
+
if override:
|
|
106
|
+
return override
|
|
107
|
+
for name in ("python", "python3", "py"):
|
|
108
|
+
found = shutil.which(name, path=env.get("PATH"))
|
|
109
|
+
if found:
|
|
110
|
+
return found
|
|
111
|
+
return sys.executable
|
|
112
|
+
|
|
113
|
+
def _coverage_available(self, python: str, env: Dict[str, str]) -> bool:
|
|
114
|
+
try:
|
|
115
|
+
result = subprocess.run(
|
|
116
|
+
[python, "-m", "coverage", "--version"],
|
|
117
|
+
cwd=self.project_root,
|
|
118
|
+
capture_output=True,
|
|
119
|
+
text=True,
|
|
120
|
+
encoding="utf-8",
|
|
121
|
+
errors="replace",
|
|
122
|
+
timeout=30,
|
|
123
|
+
env=env,
|
|
124
|
+
)
|
|
125
|
+
return result.returncode == 0
|
|
126
|
+
except Exception:
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _measure_python(
|
|
130
|
+
self,
|
|
131
|
+
task: Task,
|
|
132
|
+
commands: List[str],
|
|
133
|
+
changed: Dict[str, Dict[int, str]],
|
|
134
|
+
tmp_dir: Path,
|
|
135
|
+
env: dict,
|
|
136
|
+
timeout: int,
|
|
137
|
+
) -> dc.DiffCoverageResult:
|
|
138
|
+
python = self._resolve_coverage_python(env)
|
|
139
|
+
if not self._coverage_available(python, env):
|
|
140
|
+
return dc.DiffCoverageResult(measured=False, reason="coverage.py not available")
|
|
141
|
+
data_file = tmp_dir / f"diffcov-{task.id}.coverage"
|
|
142
|
+
json_file = tmp_dir / f"diffcov-{task.id}.json"
|
|
143
|
+
for stale in (data_file, json_file):
|
|
144
|
+
try:
|
|
145
|
+
stale.unlink()
|
|
146
|
+
except FileNotFoundError:
|
|
147
|
+
pass
|
|
148
|
+
ran_any = False
|
|
149
|
+
append = False
|
|
150
|
+
inline_scripts: List[Path] = []
|
|
151
|
+
try:
|
|
152
|
+
for idx, cmd in enumerate(commands):
|
|
153
|
+
try:
|
|
154
|
+
argv = self._split_command(cmd)
|
|
155
|
+
except ValueError as exc:
|
|
156
|
+
logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
|
|
157
|
+
continue
|
|
158
|
+
inline = dc.inline_python_code(argv)
|
|
159
|
+
if inline is not None:
|
|
160
|
+
script = tmp_dir / f"diffcov-inline-{task.id}-{idx}.py"
|
|
161
|
+
try:
|
|
162
|
+
script.write_text(dc.inline_script_content(inline, self.project_root), encoding="utf-8")
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
logger.warning("Diff-coverage inline script write failed for %s: %s", task.id, exc)
|
|
165
|
+
continue
|
|
166
|
+
inline_scripts.append(script)
|
|
167
|
+
cov_argv: Optional[List[str]] = dc.coverage_run_script_argv(
|
|
168
|
+
str(script), python, append=append, data_file=str(data_file)
|
|
169
|
+
)
|
|
170
|
+
else:
|
|
171
|
+
cov_argv = dc.coverage_run_argv(argv, python, append=append, data_file=str(data_file))
|
|
172
|
+
if cov_argv is None:
|
|
173
|
+
continue
|
|
174
|
+
try:
|
|
175
|
+
subprocess.run(
|
|
176
|
+
cov_argv, cwd=self.project_root, capture_output=True,
|
|
177
|
+
text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
|
|
178
|
+
)
|
|
179
|
+
except Exception as exc:
|
|
180
|
+
logger.warning("Diff-coverage run failed for %s: %s", task.id, exc)
|
|
181
|
+
continue
|
|
182
|
+
ran_any = True
|
|
183
|
+
append = True
|
|
184
|
+
if not ran_any or not data_file.exists():
|
|
185
|
+
return dc.DiffCoverageResult(measured=False, reason="no instrumentable Python test command")
|
|
186
|
+
subprocess.run(
|
|
187
|
+
[python, "-m", "coverage", "json", f"--data-file={data_file}", "-o", str(json_file)],
|
|
188
|
+
cwd=self.project_root, capture_output=True, text=True,
|
|
189
|
+
encoding="utf-8", errors="replace", timeout=120, env=env,
|
|
190
|
+
)
|
|
191
|
+
data = json.loads(json_file.read_text(encoding="utf-8"))
|
|
192
|
+
coverage = dc.parse_coverage_json(data, self.project_root)
|
|
193
|
+
return dc.intersect(changed, coverage, tool="coverage.py")
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
return dc.DiffCoverageResult(measured=False, reason=f"coverage.py failed: {exc}")
|
|
196
|
+
finally:
|
|
197
|
+
for path in [data_file, json_file, *inline_scripts]:
|
|
198
|
+
try:
|
|
199
|
+
path.unlink()
|
|
200
|
+
except OSError:
|
|
201
|
+
pass
|
|
202
|
+
|
|
203
|
+
def _measure_js(
|
|
204
|
+
self,
|
|
205
|
+
task: Task,
|
|
206
|
+
commands: List[str],
|
|
207
|
+
changed: Dict[str, Dict[int, str]],
|
|
208
|
+
tmp_dir: Path,
|
|
209
|
+
env: dict,
|
|
210
|
+
timeout: int,
|
|
211
|
+
) -> dc.DiffCoverageResult:
|
|
212
|
+
reports_dir = tmp_dir / f"c8-{task.id}"
|
|
213
|
+
reports_dir.mkdir(parents=True, exist_ok=True)
|
|
214
|
+
try:
|
|
215
|
+
for stale in reports_dir.glob("coverage-final.json"):
|
|
216
|
+
stale.unlink()
|
|
217
|
+
except OSError:
|
|
218
|
+
pass
|
|
219
|
+
ran_any = False
|
|
220
|
+
for cmd in commands:
|
|
221
|
+
try:
|
|
222
|
+
argv = self._split_command(cmd)
|
|
223
|
+
except ValueError as exc:
|
|
224
|
+
logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
|
|
225
|
+
continue
|
|
226
|
+
cov_argv = dc.c8_run_argv(argv, reports_dir=str(reports_dir))
|
|
227
|
+
if cov_argv is None:
|
|
228
|
+
continue
|
|
229
|
+
try:
|
|
230
|
+
subprocess.run(
|
|
231
|
+
cov_argv, cwd=self.project_root, capture_output=True,
|
|
232
|
+
text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
|
|
233
|
+
)
|
|
234
|
+
except Exception as exc:
|
|
235
|
+
logger.warning("c8 diff-coverage run failed for %s: %s", task.id, exc)
|
|
236
|
+
continue
|
|
237
|
+
ran_any = True
|
|
238
|
+
candidates = list(reports_dir.glob("**/coverage-final.json"))
|
|
239
|
+
if not ran_any or not candidates:
|
|
240
|
+
return dc.DiffCoverageResult(measured=False, reason="c8 not available or no JS test command")
|
|
241
|
+
try:
|
|
242
|
+
data = json.loads(candidates[0].read_text(encoding="utf-8"))
|
|
243
|
+
coverage = dc.parse_istanbul_json(data, self.project_root)
|
|
244
|
+
return dc.intersect(changed, coverage, tool="c8")
|
|
245
|
+
except Exception as exc:
|
|
246
|
+
return dc.DiffCoverageResult(measured=False, reason=f"c8 report unreadable: {exc}")
|
|
247
|
+
|
|
248
|
+
def _measure_go(
|
|
249
|
+
self,
|
|
250
|
+
task: Task,
|
|
251
|
+
commands: List[str],
|
|
252
|
+
changed: Dict[str, Dict[int, str]],
|
|
253
|
+
tmp_dir: Path,
|
|
254
|
+
env: dict,
|
|
255
|
+
timeout: int,
|
|
256
|
+
) -> dc.DiffCoverageResult:
|
|
257
|
+
profile = tmp_dir / f"diffcov-{task.id}.out"
|
|
258
|
+
try:
|
|
259
|
+
profile.unlink()
|
|
260
|
+
except FileNotFoundError:
|
|
261
|
+
pass
|
|
262
|
+
ran_any = False
|
|
263
|
+
for cmd in commands:
|
|
264
|
+
try:
|
|
265
|
+
argv = self._split_command(cmd)
|
|
266
|
+
except ValueError as exc:
|
|
267
|
+
logger.warning("Diff-coverage skipping unparseable command %r: %s", cmd, exc)
|
|
268
|
+
continue
|
|
269
|
+
cov_argv = dc.go_cover_run_argv(argv, str(profile))
|
|
270
|
+
if cov_argv is None:
|
|
271
|
+
continue
|
|
272
|
+
try:
|
|
273
|
+
subprocess.run(
|
|
274
|
+
cov_argv, cwd=self.project_root, capture_output=True,
|
|
275
|
+
text=True, encoding="utf-8", errors="replace", timeout=timeout, env=env,
|
|
276
|
+
)
|
|
277
|
+
except Exception as exc:
|
|
278
|
+
logger.warning("go cover run failed for %s: %s", task.id, exc)
|
|
279
|
+
continue
|
|
280
|
+
ran_any = True
|
|
281
|
+
if not ran_any or not profile.exists():
|
|
282
|
+
return dc.DiffCoverageResult(measured=False, reason="go test -coverprofile not available")
|
|
283
|
+
try:
|
|
284
|
+
coverage = dc.parse_go_coverprofile(profile.read_text(encoding="utf-8"), self.project_root)
|
|
285
|
+
return dc.intersect(changed, coverage, tool="go-cover")
|
|
286
|
+
except Exception as exc:
|
|
287
|
+
return dc.DiffCoverageResult(measured=False, reason=f"go cover profile unreadable: {exc}")
|
|
288
|
+
finally:
|
|
289
|
+
try:
|
|
290
|
+
profile.unlink()
|
|
291
|
+
except OSError:
|
|
292
|
+
pass
|