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,112 @@
|
|
|
1
|
+
"""Batch git worktree queries into a minimal set of subprocess calls.
|
|
2
|
+
|
|
3
|
+
Verifier hot paths previously invoked ``git diff``, ``git diff --name-only``, and
|
|
4
|
+
``git ls-files --others`` independently (and sometimes repeatedly within one
|
|
5
|
+
``verify_task``). ``GitWorktreeSnapshot.capture`` runs at most three plumbing
|
|
6
|
+
commands — ``rev-parse``, ``diff HEAD`` (when HEAD exists), and
|
|
7
|
+
``status --porcelain -z`` — and caches the parsed results for the rest of the
|
|
8
|
+
verification pass.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import List, Union
|
|
16
|
+
|
|
17
|
+
from devcouncil.utils.proc import GIT_TIMEOUT, git_output, run_git
|
|
18
|
+
|
|
19
|
+
PathLike = Union[str, Path]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class GitWorktreeSnapshot:
|
|
24
|
+
"""Cached worktree state from a single batched git probe."""
|
|
25
|
+
|
|
26
|
+
has_head: bool
|
|
27
|
+
tracked_diff: str = ""
|
|
28
|
+
tracked_changed_files: List[str] = field(default_factory=list)
|
|
29
|
+
untracked_files: List[str] = field(default_factory=list)
|
|
30
|
+
cached_diff: str = ""
|
|
31
|
+
working_diff: str = ""
|
|
32
|
+
status_paths: List[str] = field(default_factory=list)
|
|
33
|
+
|
|
34
|
+
@classmethod
|
|
35
|
+
def capture(cls, root: PathLike, *, timeout: float = GIT_TIMEOUT) -> GitWorktreeSnapshot:
|
|
36
|
+
root = Path(root)
|
|
37
|
+
has_head = (
|
|
38
|
+
run_git(["rev-parse", "--verify", "HEAD"], root, timeout=timeout).returncode == 0
|
|
39
|
+
)
|
|
40
|
+
status_entries = _parse_status(root, timeout=timeout)
|
|
41
|
+
untracked = [e.path for e in status_entries if e.xy == "??"]
|
|
42
|
+
status_paths = [e.path for e in status_entries]
|
|
43
|
+
|
|
44
|
+
if has_head:
|
|
45
|
+
tracked_diff = git_output(["diff", "HEAD"], root, timeout=timeout, default="")
|
|
46
|
+
tracked_changed = sorted({e.path for e in status_entries if e.xy != "??"})
|
|
47
|
+
if not tracked_changed and tracked_diff.strip():
|
|
48
|
+
name_out = git_output(
|
|
49
|
+
["diff", "HEAD", "--name-only"], root, timeout=timeout, default=""
|
|
50
|
+
)
|
|
51
|
+
tracked_changed = [p.replace("\\", "/") for p in name_out.splitlines() if p.strip()]
|
|
52
|
+
return cls(
|
|
53
|
+
has_head=True,
|
|
54
|
+
tracked_diff=tracked_diff,
|
|
55
|
+
tracked_changed_files=tracked_changed,
|
|
56
|
+
untracked_files=sorted(set(untracked)),
|
|
57
|
+
status_paths=status_paths,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
cached_diff = git_output(["diff", "--cached"], root, timeout=timeout, default="")
|
|
61
|
+
working_diff = git_output(["diff"], root, timeout=timeout, default="")
|
|
62
|
+
if not untracked:
|
|
63
|
+
untracked = _ls_untracked(root, timeout=timeout)
|
|
64
|
+
if not status_paths:
|
|
65
|
+
status_paths = sorted(set(untracked))
|
|
66
|
+
return cls(
|
|
67
|
+
has_head=False,
|
|
68
|
+
cached_diff=cached_diff,
|
|
69
|
+
working_diff=working_diff,
|
|
70
|
+
untracked_files=sorted(set(untracked)),
|
|
71
|
+
status_paths=status_paths,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class _StatusEntry:
|
|
77
|
+
xy: str
|
|
78
|
+
path: str
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_status(root: Path, *, timeout: float) -> List[_StatusEntry]:
|
|
82
|
+
result = run_git(["status", "--porcelain", "-z", "-u"], root, timeout=timeout)
|
|
83
|
+
if result.returncode != 0 or not result.stdout:
|
|
84
|
+
return []
|
|
85
|
+
entries: List[_StatusEntry] = []
|
|
86
|
+
parts = result.stdout.split("\0")
|
|
87
|
+
i = 0
|
|
88
|
+
while i < len(parts):
|
|
89
|
+
entry = parts[i]
|
|
90
|
+
if not entry or len(entry) < 3:
|
|
91
|
+
i += 1
|
|
92
|
+
continue
|
|
93
|
+
xy = entry[:2]
|
|
94
|
+
rest = entry[3:].replace("\\", "/")
|
|
95
|
+
if "R" in xy:
|
|
96
|
+
if i + 1 < len(parts) and parts[i + 1]:
|
|
97
|
+
entries.append(_StatusEntry(xy=xy, path=parts[i + 1].replace("\\", "/")))
|
|
98
|
+
i += 2
|
|
99
|
+
continue
|
|
100
|
+
entries.append(_StatusEntry(xy=xy, path=rest))
|
|
101
|
+
i += 1
|
|
102
|
+
return entries
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _ls_untracked(root: Path, *, timeout: float) -> List[str]:
|
|
106
|
+
out = git_output(
|
|
107
|
+
["ls-files", "--others", "--exclude-standard"],
|
|
108
|
+
root,
|
|
109
|
+
timeout=timeout,
|
|
110
|
+
default="",
|
|
111
|
+
)
|
|
112
|
+
return [p.replace("\\", "/") for p in out.splitlines() if p.strip()]
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Central JSON read/write helpers built on atomic fsio writes."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, TypeVar, Union
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
|
|
11
|
+
from devcouncil.utils.fsio import atomic_write_json, atomic_write_text
|
|
12
|
+
|
|
13
|
+
PathLike = Union[str, Path]
|
|
14
|
+
ModelT = TypeVar("ModelT", bound=BaseModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def write_json(path: PathLike, payload: Any, *, indent: int = 2, sort_keys: bool = False) -> None:
|
|
18
|
+
"""Atomically serialize ``payload`` to JSON at ``path``."""
|
|
19
|
+
atomic_write_json(path, payload, indent=indent, sort_keys=sort_keys)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def write_model_json(
|
|
23
|
+
path: PathLike,
|
|
24
|
+
model: BaseModel,
|
|
25
|
+
*,
|
|
26
|
+
indent: int | None = 2,
|
|
27
|
+
exclude_none: bool = False,
|
|
28
|
+
) -> None:
|
|
29
|
+
"""Atomically write a Pydantic model as JSON.
|
|
30
|
+
|
|
31
|
+
Pass ``indent=None`` for compact (minified) output.
|
|
32
|
+
"""
|
|
33
|
+
atomic_write_text(
|
|
34
|
+
path,
|
|
35
|
+
model.model_dump_json(indent=indent, exclude_none=exclude_none) + "\n",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def read_json(path: PathLike) -> Any:
|
|
40
|
+
"""Load JSON from ``path``; raises ``FileNotFoundError`` / ``json.JSONDecodeError``."""
|
|
41
|
+
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_model_json(path: PathLike, model_type: type[ModelT]) -> ModelT:
|
|
45
|
+
"""Load and validate JSON as a Pydantic model."""
|
|
46
|
+
return model_type.model_validate_json(Path(path).read_text(encoding="utf-8"))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def dump_json(payload: Any, *, indent: int | None = None, sort_keys: bool = False, separators: tuple[str, str] | None = None) -> str:
|
|
50
|
+
"""Serialize ``payload`` to a JSON string (stdout / DB columns)."""
|
|
51
|
+
if separators is not None:
|
|
52
|
+
return json.dumps(payload, indent=indent, sort_keys=sort_keys, separators=separators)
|
|
53
|
+
return json.dumps(payload, indent=indent, sort_keys=sort_keys)
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Shared subprocess helpers with consistent timeouts and error handling.
|
|
2
|
+
|
|
3
|
+
Every git/tool invocation in DevCouncil should go through these helpers (or at
|
|
4
|
+
minimum pass an explicit ``timeout=``) so a hung child process can never hang a
|
|
5
|
+
verification run or the CLI. Defaults are deliberately generous: they exist to
|
|
6
|
+
convert "hangs forever" into "fails loudly", not to race fast commands.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import List, Optional, Sequence, Union
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Default ceiling for git plumbing commands (status/diff/ls-files/rev-parse).
|
|
19
|
+
GIT_TIMEOUT: float = 60.0
|
|
20
|
+
# Default ceiling for generic tool invocations when the caller has no better bound.
|
|
21
|
+
DEFAULT_TIMEOUT: float = 120.0
|
|
22
|
+
|
|
23
|
+
Cmd = Sequence[str]
|
|
24
|
+
PathLike = Union[str, Path]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def run_git(
|
|
28
|
+
args: Cmd,
|
|
29
|
+
cwd: PathLike,
|
|
30
|
+
*,
|
|
31
|
+
timeout: float = GIT_TIMEOUT,
|
|
32
|
+
check: bool = False,
|
|
33
|
+
) -> subprocess.CompletedProcess[str]:
|
|
34
|
+
"""Run a git command with a timeout, capturing decoded text output.
|
|
35
|
+
|
|
36
|
+
Never raises ``TimeoutExpired``: a timeout is logged and surfaced as a
|
|
37
|
+
``CompletedProcess`` with returncode 124 so existing ``returncode``/stdout
|
|
38
|
+
checks keep working. Raises ``CalledProcessError`` only when ``check=True``
|
|
39
|
+
and the command failed (mirroring ``subprocess.run``).
|
|
40
|
+
"""
|
|
41
|
+
cmd: List[str] = ["git", *args] if args and args[0] != "git" else list(args)
|
|
42
|
+
try:
|
|
43
|
+
return subprocess.run(
|
|
44
|
+
cmd,
|
|
45
|
+
cwd=str(cwd),
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
encoding="utf-8",
|
|
49
|
+
errors="replace",
|
|
50
|
+
timeout=timeout,
|
|
51
|
+
check=check,
|
|
52
|
+
)
|
|
53
|
+
except subprocess.TimeoutExpired:
|
|
54
|
+
logger.warning("git command timed out after %.0fs: %s (cwd=%s)", timeout, " ".join(cmd), cwd)
|
|
55
|
+
return subprocess.CompletedProcess(cmd, returncode=124, stdout="", stderr=f"timed out after {timeout}s")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def git_output(
|
|
59
|
+
args: Cmd,
|
|
60
|
+
cwd: PathLike,
|
|
61
|
+
*,
|
|
62
|
+
timeout: float = GIT_TIMEOUT,
|
|
63
|
+
default: Optional[str] = None,
|
|
64
|
+
) -> str:
|
|
65
|
+
"""Return stdout of a git command, or ``default`` on any failure.
|
|
66
|
+
|
|
67
|
+
When ``default`` is None a failure raises ``CalledProcessError`` /
|
|
68
|
+
``TimeoutExpired`` for the caller's existing except paths; otherwise the
|
|
69
|
+
failure is logged at debug level and ``default`` is returned.
|
|
70
|
+
"""
|
|
71
|
+
cmd: List[str] = ["git", *args] if args and args[0] != "git" else list(args)
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
cmd,
|
|
75
|
+
cwd=str(cwd),
|
|
76
|
+
capture_output=True,
|
|
77
|
+
text=True,
|
|
78
|
+
encoding="utf-8",
|
|
79
|
+
errors="replace",
|
|
80
|
+
timeout=timeout,
|
|
81
|
+
)
|
|
82
|
+
if result.returncode != 0:
|
|
83
|
+
raise subprocess.CalledProcessError(result.returncode, cmd, result.stdout, result.stderr)
|
|
84
|
+
return result.stdout
|
|
85
|
+
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, OSError) as e:
|
|
86
|
+
if default is not None:
|
|
87
|
+
logger.debug("git command failed (%s): %s (cwd=%s)", e, " ".join(cmd), cwd)
|
|
88
|
+
return default
|
|
89
|
+
raise
|
|
@@ -10,6 +10,7 @@ text and the code under review, then maps each check 1:1 to its criterion.
|
|
|
10
10
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
|
+
import asyncio
|
|
13
14
|
import json
|
|
14
15
|
import logging
|
|
15
16
|
from typing import Dict, List
|
|
@@ -92,9 +93,17 @@ class AcceptanceTestCompiler:
|
|
|
92
93
|
return {}
|
|
93
94
|
|
|
94
95
|
if per_criterion and len(target) > 1:
|
|
96
|
+
# Independent per-criterion compiles: run them CONCURRENTLY. Each call is
|
|
97
|
+
# independent (own prompt, own criterion), so wall-clock drops from N
|
|
98
|
+
# sequential model calls to max-of-N — the difference between minutes and
|
|
99
|
+
# tens of minutes on a slow local monitor. A server that processes
|
|
100
|
+
# requests serially just queues them (no worse than before).
|
|
101
|
+
results = await asyncio.gather(
|
|
102
|
+
*(self._sample_checks([ac], code_context, samples) for ac in target)
|
|
103
|
+
)
|
|
95
104
|
out: Dict[str, List[str]] = {}
|
|
96
|
-
for
|
|
97
|
-
out.update(
|
|
105
|
+
for result in results:
|
|
106
|
+
out.update(result)
|
|
98
107
|
return out
|
|
99
108
|
return await self._sample_checks(target, code_context, samples)
|
|
100
109
|
|
|
@@ -104,21 +113,29 @@ class AcceptanceTestCompiler:
|
|
|
104
113
|
"""Generate up to ``samples`` independent check commands for ``target`` criteria."""
|
|
105
114
|
valid_ids = {ac.id for ac in target}
|
|
106
115
|
out: Dict[str, List[str]] = {}
|
|
116
|
+
|
|
107
117
|
# Independent attempts. Each varies temperature + an attempt marker so the router
|
|
108
118
|
# cache keys differ (otherwise an identical prompt+temp returns the cached answer
|
|
109
119
|
# and every "sample" is the same command). Attempt 0 stays deterministic (temp 0).
|
|
110
|
-
|
|
120
|
+
async def _one_attempt(attempt: int) -> "CompiledChecks":
|
|
111
121
|
temperature = 0.0 if attempt == 0 else min(0.8, 0.3 + 0.2 * attempt)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
return await self.router.complete_structured(
|
|
123
|
+
role=self.role,
|
|
124
|
+
messages=[{"role": "user", "content": self._compile_prompt(target, code_context, attempt)}],
|
|
125
|
+
schema=CompiledChecks,
|
|
126
|
+
temperature=temperature,
|
|
127
|
+
fallback=CompiledChecks(checks=[]),
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Attempts are independent by construction (distinct prompts/temperatures), so
|
|
131
|
+
# run them concurrently; results are merged in attempt order to keep candidate
|
|
132
|
+
# ordering deterministic. A failed attempt costs only itself.
|
|
133
|
+
results = await asyncio.gather(
|
|
134
|
+
*(_one_attempt(a) for a in range(max(1, samples))), return_exceptions=True
|
|
135
|
+
)
|
|
136
|
+
for attempt, result in enumerate(results):
|
|
137
|
+
if isinstance(result, BaseException):
|
|
138
|
+
logger.warning("Acceptance-check compile attempt %d failed: %s", attempt, result)
|
|
122
139
|
continue
|
|
123
140
|
for check in result.checks:
|
|
124
141
|
cmd = (check.command or "").strip()
|
|
@@ -228,6 +245,12 @@ Rules — the commands are executed verbatim by the verifier:
|
|
|
228
245
|
python -c "import m; \\ntry: m.f([])\\nexcept ValueError: pass\\nelse: raise SystemExit(1)"
|
|
229
246
|
(real newlines are fine; never put try/if/for after a ';').
|
|
230
247
|
- Use the actual module name implied by the code (e.g. file 'stats.py' -> import stats).
|
|
248
|
+
- Choose DISCRIMINATING inputs: pick the input a plausible-but-lazy implementation
|
|
249
|
+
would get WRONG, not the easiest happy-path example (which often passes on broken
|
|
250
|
+
code and proves nothing). Examples: to prove "comment lines are skipped", the
|
|
251
|
+
comment must CONTAIN the delimiter ('# c=3'); to prove "split on the first X",
|
|
252
|
+
the value must itself contain X; to prove sorting/ordering, the input must start
|
|
253
|
+
unsorted; to prove a boundary, test AT the boundary.
|
|
231
254
|
|
|
232
255
|
HARD PROHIBITIONS — a command that does any of these is INVALID; omit the
|
|
233
256
|
criterion instead of emitting such a command:
|
|
@@ -45,6 +45,7 @@ class AdHocCheckResult:
|
|
|
45
45
|
diff_coverage: Optional[DiffCoverageEvidence] = None
|
|
46
46
|
passed: bool = True
|
|
47
47
|
reason: str = ""
|
|
48
|
+
verification_mode: str = "coarse"
|
|
48
49
|
|
|
49
50
|
def to_dict(self) -> dict:
|
|
50
51
|
return {
|
|
@@ -53,6 +54,7 @@ class AdHocCheckResult:
|
|
|
53
54
|
"requirement": self.requirement,
|
|
54
55
|
"changed_files": self.changed_files,
|
|
55
56
|
"reason": self.reason,
|
|
57
|
+
"verification_mode": self.verification_mode,
|
|
56
58
|
"gap_count": len(self.gaps),
|
|
57
59
|
"blocking_gap_count": len([g for g in self.gaps if g.blocking]),
|
|
58
60
|
"gaps": [g.model_dump() for g in self.gaps],
|
|
@@ -61,13 +63,79 @@ class AdHocCheckResult:
|
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
|
|
66
|
+
def _load_verify_router(project_root: Path) -> Optional[ModelRouter]:
|
|
67
|
+
"""Best-effort router for compiled acceptance checks during ad-hoc verify."""
|
|
68
|
+
try:
|
|
69
|
+
from devcouncil.app.config import get_api_key, load_config
|
|
70
|
+
from devcouncil.llm.provider import create_provider, validate_model_provider
|
|
71
|
+
|
|
72
|
+
config = load_config(project_root)
|
|
73
|
+
validate_model_provider(config.models.provider)
|
|
74
|
+
api_key = get_api_key(config.models.provider, project_root)
|
|
75
|
+
provider = create_provider(
|
|
76
|
+
config.models.provider,
|
|
77
|
+
api_key,
|
|
78
|
+
project_root=project_root,
|
|
79
|
+
provider_prefs=config.provider,
|
|
80
|
+
)
|
|
81
|
+
role_config = {name: role.model_dump() for name, role in config.models.roles.items()}
|
|
82
|
+
return ModelRouter(provider, role_config, project_root=project_root)
|
|
83
|
+
except Exception as exc:
|
|
84
|
+
logger.debug("Ad-hoc check: no model router (%s)", exc)
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _persist_ad_hoc_result(
|
|
89
|
+
project_root: Path,
|
|
90
|
+
*,
|
|
91
|
+
requirement: Requirement,
|
|
92
|
+
task: Task,
|
|
93
|
+
gaps: List[Gap],
|
|
94
|
+
evidence: list,
|
|
95
|
+
) -> None:
|
|
96
|
+
"""Write CHECK task gaps/evidence so ``dev report`` can export CI artifacts."""
|
|
97
|
+
from devcouncil.domain.evidence import CommandResult, DiffEvidence, TestEvidence
|
|
98
|
+
from devcouncil.storage.db import get_db
|
|
99
|
+
from devcouncil.storage.repositories import (
|
|
100
|
+
EvidenceRepository,
|
|
101
|
+
GapRepository,
|
|
102
|
+
RequirementRepository,
|
|
103
|
+
TaskRepository,
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
db = get_db(project_root)
|
|
107
|
+
if not db:
|
|
108
|
+
return
|
|
109
|
+
with db.get_session() as session:
|
|
110
|
+
RequirementRepository(session).save(requirement)
|
|
111
|
+
gap_repo = GapRepository(session)
|
|
112
|
+
ev_repo = EvidenceRepository(session)
|
|
113
|
+
gap_repo.delete_for_task(task.id)
|
|
114
|
+
ev_repo.delete_for_task(task.id)
|
|
115
|
+
for gap in gaps:
|
|
116
|
+
gap_repo.save(gap)
|
|
117
|
+
for ev in evidence:
|
|
118
|
+
if isinstance(ev, CommandResult):
|
|
119
|
+
ev_repo.save_command_result(task.id, ev)
|
|
120
|
+
elif isinstance(ev, DiffCoverageEvidence):
|
|
121
|
+
ev_repo.save_diff_coverage_evidence(ev)
|
|
122
|
+
elif isinstance(ev, DiffEvidence):
|
|
123
|
+
ev_repo.save_diff_evidence(ev)
|
|
124
|
+
elif isinstance(ev, TestEvidence):
|
|
125
|
+
ev_repo.save_test_evidence(ev, task.id)
|
|
126
|
+
task.status = "blocked" if any(g.blocking for g in gaps) else "verified"
|
|
127
|
+
TaskRepository(session).save(task)
|
|
128
|
+
|
|
129
|
+
|
|
64
130
|
def run_working_tree_check(
|
|
65
131
|
project_root: Path,
|
|
66
132
|
requirement: Optional[str] = None,
|
|
67
133
|
*,
|
|
134
|
+
base: Optional[str] = None,
|
|
68
135
|
test_commands: Optional[List[str]] = None,
|
|
69
136
|
enforce_coverage: bool = False,
|
|
70
137
|
min_ratio: float = 0.0,
|
|
138
|
+
persist: bool = False,
|
|
71
139
|
router: Optional[ModelRouter] = None,
|
|
72
140
|
verifier: Optional[Verifier] = None,
|
|
73
141
|
) -> AdHocCheckResult:
|
|
@@ -77,14 +145,25 @@ def run_working_tree_check(
|
|
|
77
145
|
result is about evidence, not scope noise) and whose expected tests are
|
|
78
146
|
``test_commands``. Diff coverage is always measured; pass ``enforce_coverage`` (or a
|
|
79
147
|
positive ``min_ratio``) to make an unexercised diff blocking.
|
|
148
|
+
|
|
149
|
+
When ``base`` is set, the diff is ``git diff <base>`` (PR/base scope) instead of
|
|
150
|
+
uncommitted changes vs HEAD. Pass ``persist=True`` to write gaps/evidence for
|
|
151
|
+
``dev report`` artifact export (typical in CI).
|
|
80
152
|
"""
|
|
81
|
-
verifier = verifier or Verifier(project_root, router=router)
|
|
153
|
+
verifier = verifier or Verifier(project_root, router=router or _load_verify_router(project_root))
|
|
154
|
+
if base:
|
|
155
|
+
verifier._git_fallback.diff_base = base
|
|
82
156
|
|
|
83
157
|
diff = verifier.get_diff()
|
|
84
158
|
changed_files = verifier.get_changed_files()
|
|
85
159
|
if not diff or not changed_files:
|
|
86
|
-
|
|
87
|
-
|
|
160
|
+
scope = f"against {base}" if base else "in working tree"
|
|
161
|
+
logger.info("Ad-hoc check: no changes %s; passing trivially", scope)
|
|
162
|
+
return AdHocCheckResult(
|
|
163
|
+
requirement=requirement or "",
|
|
164
|
+
passed=True,
|
|
165
|
+
reason="no_changes",
|
|
166
|
+
)
|
|
88
167
|
logger.info("Ad-hoc check: %d changed file(s), enforce_coverage=%s", len(changed_files), enforce_coverage or min_ratio > 0)
|
|
89
168
|
|
|
90
169
|
criterion = requirement or _DEFAULT_CRITERION
|
|
@@ -114,6 +193,7 @@ def run_working_tree_check(
|
|
|
114
193
|
for path in changed_files
|
|
115
194
|
],
|
|
116
195
|
expected_tests=list(test_commands or []),
|
|
196
|
+
difficulty="easy",
|
|
117
197
|
)
|
|
118
198
|
|
|
119
199
|
# Always measure diff coverage in lite mode; block on it only when asked. A positive
|
|
@@ -124,6 +204,17 @@ def run_working_tree_check(
|
|
|
124
204
|
gaps, evidence = asyncio.run(verifier.verify_task(task, [req]))
|
|
125
205
|
coverage = next((ev for ev in evidence if isinstance(ev, DiffCoverageEvidence)), None)
|
|
126
206
|
blocking = [g for g in gaps if g.blocking]
|
|
207
|
+
outcome_mode = verifier.last_outcome.mode if verifier.last_outcome else (
|
|
208
|
+
"compiled" if verifier.acceptance_compiler else "coarse"
|
|
209
|
+
)
|
|
210
|
+
if persist:
|
|
211
|
+
_persist_ad_hoc_result(
|
|
212
|
+
project_root,
|
|
213
|
+
requirement=req,
|
|
214
|
+
task=task,
|
|
215
|
+
gaps=gaps,
|
|
216
|
+
evidence=evidence,
|
|
217
|
+
)
|
|
127
218
|
logger.info("Ad-hoc check result: passed=%s (%d gap(s), %d blocking)", not blocking, len(gaps), len(blocking))
|
|
128
219
|
return AdHocCheckResult(
|
|
129
220
|
requirement=criterion,
|
|
@@ -132,4 +223,5 @@ def run_working_tree_check(
|
|
|
132
223
|
next_actions=build_next_actions(gaps),
|
|
133
224
|
diff_coverage=coverage,
|
|
134
225
|
passed=not blocking,
|
|
226
|
+
verification_mode=outcome_mode,
|
|
135
227
|
)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""Verification check helpers."""
|
|
2
|
+
|
|
3
|
+
from devcouncil.verification.checks.acceptance import coarse_proven_acceptance_ids, unproven_acceptance_ids
|
|
4
|
+
from devcouncil.verification.checks.command_evidence import run_verification_commands
|
|
5
|
+
from devcouncil.verification.checks.corpus_verification import (
|
|
6
|
+
detect_acceptance_corpus_gaps,
|
|
7
|
+
detect_corpus_stale_gaps,
|
|
8
|
+
detect_doc_code_ref_gaps,
|
|
9
|
+
)
|
|
10
|
+
from devcouncil.verification.checks.dead_symbols import detect_dead_symbol_gaps
|
|
11
|
+
from devcouncil.verification.checks.diff_coverage_gate import DiffCoverageGateResult, run_diff_coverage_gate
|
|
12
|
+
from devcouncil.verification.checks.liveness_ratchet import detect_liveness_regressions
|
|
13
|
+
from devcouncil.verification.checks.orphan_diff import detect_orphan_diff_gaps
|
|
14
|
+
from devcouncil.verification.checks.planned_files import (
|
|
15
|
+
detect_dependency_risk_gaps,
|
|
16
|
+
detect_no_work_gap,
|
|
17
|
+
detect_planned_file_gaps,
|
|
18
|
+
)
|
|
19
|
+
from devcouncil.verification.checks.stale_map import detect_stale_map_gaps
|
|
20
|
+
from devcouncil.verification.checks.stub_scan import detect_stub_gaps
|
|
21
|
+
from devcouncil.verification.checks.wiring import detect_unwired_file_gaps
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"DiffCoverageGateResult",
|
|
25
|
+
"detect_acceptance_corpus_gaps",
|
|
26
|
+
"detect_corpus_stale_gaps",
|
|
27
|
+
"detect_doc_code_ref_gaps",
|
|
28
|
+
"coarse_proven_acceptance_ids",
|
|
29
|
+
"detect_dead_symbol_gaps",
|
|
30
|
+
"detect_dependency_risk_gaps",
|
|
31
|
+
"detect_liveness_regressions",
|
|
32
|
+
"detect_no_work_gap",
|
|
33
|
+
"detect_orphan_diff_gaps",
|
|
34
|
+
"detect_planned_file_gaps",
|
|
35
|
+
"detect_stale_map_gaps",
|
|
36
|
+
"detect_stub_gaps",
|
|
37
|
+
"detect_unwired_file_gaps",
|
|
38
|
+
"run_diff_coverage_gate",
|
|
39
|
+
"run_verification_commands",
|
|
40
|
+
"unproven_acceptance_ids",
|
|
41
|
+
]
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Acceptance-criteria evidence helpers extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Callable, Dict, Iterable, List, Set
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def coarse_proven_acceptance_ids(
|
|
9
|
+
*,
|
|
10
|
+
task_acceptance_ids: Iterable[str],
|
|
11
|
+
successful_commands: List[str],
|
|
12
|
+
command_can_prove: Callable[[str, str], bool],
|
|
13
|
+
) -> Set[str]:
|
|
14
|
+
"""Return AC ids coarse-proven by any passing acceptance-capable command."""
|
|
15
|
+
if not task_acceptance_ids or not successful_commands:
|
|
16
|
+
return set()
|
|
17
|
+
if not any(command_can_prove("expected", cmd) for cmd in successful_commands):
|
|
18
|
+
return set()
|
|
19
|
+
return set(task_acceptance_ids)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def unproven_acceptance_ids(
|
|
23
|
+
*,
|
|
24
|
+
task_acceptance_ids: Iterable[str],
|
|
25
|
+
compiled_pass: Dict[str, bool],
|
|
26
|
+
coarse_proven: Set[str],
|
|
27
|
+
inconclusive: Set[str],
|
|
28
|
+
) -> List[str]:
|
|
29
|
+
"""AC ids still lacking decisive proof after compiled + coarse passes."""
|
|
30
|
+
unproven: List[str] = []
|
|
31
|
+
for ac_id in task_acceptance_ids:
|
|
32
|
+
if ac_id in inconclusive:
|
|
33
|
+
continue
|
|
34
|
+
if compiled_pass.get(ac_id):
|
|
35
|
+
continue
|
|
36
|
+
if ac_id in coarse_proven:
|
|
37
|
+
continue
|
|
38
|
+
unproven.append(ac_id)
|
|
39
|
+
return unproven
|