devcouncil 0.3.0 → 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,216 @@
|
|
|
1
|
+
"""Unwired-file gate: flag newly added modules nothing (non-test) imports.
|
|
2
|
+
|
|
3
|
+
Diff-scoped — only files this task *added*. Requires an importer outside the
|
|
4
|
+
task's added set that is not a test file. Shares exemptions/entry-roots with
|
|
5
|
+
``indexing/wiring.py``. Never raises.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Callable, List, Optional, Set
|
|
13
|
+
|
|
14
|
+
from devcouncil.domain.gap import Gap
|
|
15
|
+
from devcouncil.domain.task import Task
|
|
16
|
+
from devcouncil.indexing.wiring import (
|
|
17
|
+
ALLOW_UNWIRED,
|
|
18
|
+
build_dynamic_import_index,
|
|
19
|
+
entry_roots,
|
|
20
|
+
has_allow_unwired,
|
|
21
|
+
is_liveness_code_file,
|
|
22
|
+
is_test_path,
|
|
23
|
+
reference_cleared,
|
|
24
|
+
structural_exemptions,
|
|
25
|
+
)
|
|
26
|
+
from devcouncil.verification.checks.orphan_diff import classify_change_paths
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger(__name__)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _norm(path: str) -> str:
|
|
32
|
+
s = path.replace("\\", "/")
|
|
33
|
+
while s.startswith("./"):
|
|
34
|
+
s = s[2:]
|
|
35
|
+
return s
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def added_files_from_diff(diff_content: str) -> Set[str]:
|
|
39
|
+
"""Paths introduced via ``--- /dev/null`` / ``+++ b/...`` headers."""
|
|
40
|
+
added: Set[str] = set()
|
|
41
|
+
pending_new = False
|
|
42
|
+
for raw in (diff_content or "").splitlines():
|
|
43
|
+
if raw.startswith("--- "):
|
|
44
|
+
target = raw[4:].strip()
|
|
45
|
+
pending_new = target == "/dev/null"
|
|
46
|
+
continue
|
|
47
|
+
if raw.startswith("+++ ") and pending_new:
|
|
48
|
+
target = raw[4:].strip()
|
|
49
|
+
pending_new = False
|
|
50
|
+
if target == "/dev/null":
|
|
51
|
+
continue
|
|
52
|
+
path = target[2:] if target.startswith(("a/", "b/")) else target
|
|
53
|
+
added.add(_norm(path))
|
|
54
|
+
return added
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def collect_added_files(
|
|
58
|
+
*,
|
|
59
|
+
project_root: Path,
|
|
60
|
+
changed_files: List[str],
|
|
61
|
+
diff_content: str,
|
|
62
|
+
get_untracked_files: Callable[[], List[str]],
|
|
63
|
+
classify_fn: Callable[[List[str]], tuple[List[str], List[str]]] | None = None,
|
|
64
|
+
) -> Set[str]:
|
|
65
|
+
"""Union of classify_change_paths added set and committed-diff ``/dev/null`` headers.
|
|
66
|
+
|
|
67
|
+
Restricted to paths that appear in the change set or are currently untracked
|
|
68
|
+
(new) so stray diff headers outside the task's change set are dropped.
|
|
69
|
+
"""
|
|
70
|
+
if classify_fn is not None:
|
|
71
|
+
classified, _ = classify_fn(changed_files)
|
|
72
|
+
added = set(classified)
|
|
73
|
+
else:
|
|
74
|
+
classified, _ = classify_change_paths(project_root, changed_files, get_untracked_files)
|
|
75
|
+
added = set(classified)
|
|
76
|
+
added |= added_files_from_diff(diff_content)
|
|
77
|
+
changed_norm = {_norm(p) for p in changed_files}
|
|
78
|
+
try:
|
|
79
|
+
untracked_norm = {_norm(p) for p in get_untracked_files()}
|
|
80
|
+
except Exception:
|
|
81
|
+
untracked_norm = set()
|
|
82
|
+
return {
|
|
83
|
+
_norm(p) for p in added
|
|
84
|
+
if _norm(p) in changed_norm or _norm(p) in untracked_norm
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def detect_unwired_file_gaps(
|
|
89
|
+
*,
|
|
90
|
+
task: Task,
|
|
91
|
+
project_root: Path,
|
|
92
|
+
changed_files: List[str],
|
|
93
|
+
diff_content: str,
|
|
94
|
+
get_untracked_files: Callable[[], List[str]],
|
|
95
|
+
next_gap_id: Callable[[str, str], str],
|
|
96
|
+
unwired_enabled: bool = True,
|
|
97
|
+
unwired_blocking: bool = False,
|
|
98
|
+
classify_fn: Callable[[List[str]], tuple[List[str], List[str]]] | None = None,
|
|
99
|
+
git_files: Optional[List[str]] = None,
|
|
100
|
+
) -> List[Gap]:
|
|
101
|
+
"""Flag added Python/JS/TS files with no external non-test importer."""
|
|
102
|
+
gaps: List[Gap] = []
|
|
103
|
+
if not unwired_enabled:
|
|
104
|
+
return gaps
|
|
105
|
+
try:
|
|
106
|
+
added = collect_added_files(
|
|
107
|
+
project_root=project_root,
|
|
108
|
+
changed_files=changed_files,
|
|
109
|
+
diff_content=diff_content,
|
|
110
|
+
get_untracked_files=get_untracked_files,
|
|
111
|
+
classify_fn=classify_fn,
|
|
112
|
+
)
|
|
113
|
+
candidates = sorted(p for p in added if is_liveness_code_file(p))
|
|
114
|
+
if not candidates:
|
|
115
|
+
return gaps
|
|
116
|
+
|
|
117
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
118
|
+
|
|
119
|
+
mapper = RepoMapper(project_root)
|
|
120
|
+
# Include untracked/new files in the edge graph so same-task imports resolve.
|
|
121
|
+
if git_files is None:
|
|
122
|
+
try:
|
|
123
|
+
tracked = mapper.get_git_files()
|
|
124
|
+
except Exception:
|
|
125
|
+
tracked = []
|
|
126
|
+
else:
|
|
127
|
+
tracked = list(git_files)
|
|
128
|
+
all_files = sorted(set(tracked) | added | {_norm(p) for p in changed_files})
|
|
129
|
+
dependents = mapper.dependents_for(all_files)
|
|
130
|
+
roots = set(entry_roots(project_root, all_files))
|
|
131
|
+
added_set = set(candidates)
|
|
132
|
+
dyn_index = build_dynamic_import_index(project_root, all_files)
|
|
133
|
+
|
|
134
|
+
for path in candidates:
|
|
135
|
+
if path in roots or structural_exemptions(path):
|
|
136
|
+
continue
|
|
137
|
+
|
|
138
|
+
if has_allow_unwired(project_root, path):
|
|
139
|
+
# Marker always clears the blocking gap; still emit an advisory so
|
|
140
|
+
# intentional unwired files stay visible in the repair loop.
|
|
141
|
+
gaps.append(Gap(
|
|
142
|
+
id=next_gap_id(task.id, f"UNWIREDDECL-{path}"),
|
|
143
|
+
severity="medium",
|
|
144
|
+
gap_type="unwired_file",
|
|
145
|
+
task_id=task.id,
|
|
146
|
+
description=(
|
|
147
|
+
f"Intentional unwired file declared at {path} "
|
|
148
|
+
f"({ALLOW_UNWIRED})."
|
|
149
|
+
),
|
|
150
|
+
evidence=[path, ALLOW_UNWIRED],
|
|
151
|
+
recommended_fix=(
|
|
152
|
+
"Review declared unwired files before marking done; wire each "
|
|
153
|
+
"into its intended caller when scaffolding is complete."
|
|
154
|
+
),
|
|
155
|
+
blocking=False,
|
|
156
|
+
file=path,
|
|
157
|
+
))
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
importers = dependents.get(path, set())
|
|
161
|
+
external_non_test = {
|
|
162
|
+
i for i in importers
|
|
163
|
+
if i not in added_set and not is_test_path(i)
|
|
164
|
+
}
|
|
165
|
+
if external_non_test:
|
|
166
|
+
continue
|
|
167
|
+
|
|
168
|
+
if reference_cleared(
|
|
169
|
+
project_root,
|
|
170
|
+
path,
|
|
171
|
+
skip_files=added_set,
|
|
172
|
+
git_files=all_files,
|
|
173
|
+
dynamic_index=dyn_index,
|
|
174
|
+
):
|
|
175
|
+
continue
|
|
176
|
+
|
|
177
|
+
only_test = {i for i in importers if is_test_path(i) and i not in added_set}
|
|
178
|
+
only_same_task = {i for i in importers if i in added_set}
|
|
179
|
+
if only_test and not only_same_task:
|
|
180
|
+
desc = (
|
|
181
|
+
f"New file {path} is only imported by test files "
|
|
182
|
+
f"({', '.join(sorted(only_test)[:3])}); production code must import it."
|
|
183
|
+
)
|
|
184
|
+
elif only_same_task and not only_test:
|
|
185
|
+
desc = (
|
|
186
|
+
f"New file {path} is only imported by other files added in this task "
|
|
187
|
+
f"({', '.join(sorted(only_same_task)[:3])}); a pre-existing non-test "
|
|
188
|
+
"caller must import it (same-task island rule)."
|
|
189
|
+
)
|
|
190
|
+
elif importers:
|
|
191
|
+
desc = (
|
|
192
|
+
f"New file {path} has no external non-test importer "
|
|
193
|
+
f"(importers: {', '.join(sorted(importers)[:5])})."
|
|
194
|
+
)
|
|
195
|
+
else:
|
|
196
|
+
desc = f"New file {path} is never imported by any other module."
|
|
197
|
+
|
|
198
|
+
gaps.append(Gap(
|
|
199
|
+
id=next_gap_id(task.id, f"UNWIRED-{path}"),
|
|
200
|
+
severity="high" if unwired_blocking else "medium",
|
|
201
|
+
gap_type="unwired_file",
|
|
202
|
+
task_id=task.id,
|
|
203
|
+
description=desc,
|
|
204
|
+
evidence=[path],
|
|
205
|
+
recommended_fix=(
|
|
206
|
+
f"Import or register `{path}` from its intended non-test caller "
|
|
207
|
+
f"(append the caller with `dev scope update {task.id} --lease-token "
|
|
208
|
+
f"<token> --planned-file <caller>` if needed), or delete the unused file."
|
|
209
|
+
),
|
|
210
|
+
blocking=unwired_blocking,
|
|
211
|
+
file=path,
|
|
212
|
+
))
|
|
213
|
+
except Exception:
|
|
214
|
+
logger.debug("detect_unwired_file_gaps failed; degrading to zero gaps", exc_info=True)
|
|
215
|
+
return []
|
|
216
|
+
return gaps
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Claim lie-detector: map completion text → assertions → independent checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from devcouncil.verification.claims.checks import execute_checks
|
|
6
|
+
from devcouncil.verification.claims.mapper import map_claims
|
|
7
|
+
from devcouncil.verification.claims.models import Assertion, CheckResult, Kind, Status
|
|
8
|
+
from devcouncil.verification.claims.transcript import last_assistant_text, last_assistant_sentence
|
|
9
|
+
from devcouncil.verification.claims.verdict import ClaimVerdict, decide_claims, summary_line
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Assertion",
|
|
13
|
+
"CheckResult",
|
|
14
|
+
"ClaimVerdict",
|
|
15
|
+
"Kind",
|
|
16
|
+
"Status",
|
|
17
|
+
"decide_claims",
|
|
18
|
+
"execute_checks",
|
|
19
|
+
"last_assistant_sentence",
|
|
20
|
+
"last_assistant_text",
|
|
21
|
+
"map_claims",
|
|
22
|
+
"summary_line",
|
|
23
|
+
]
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
"""Independent re-execution of configured commands and git/fs cross-checks.
|
|
2
|
+
|
|
3
|
+
Commands come from DevCouncil ``config.commands`` (test/lint/typecheck), never
|
|
4
|
+
from claim text — except COMMAND_SUCCEEDED matching a configured command.
|
|
5
|
+
Timeouts yield UNVERIFIABLE (never block).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import shlex
|
|
11
|
+
import shutil
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
import time
|
|
15
|
+
from dataclasses import dataclass
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from devcouncil.verification.claims.models import Assertion, CheckResult, Kind, Status
|
|
19
|
+
|
|
20
|
+
TAIL_LINES = 50
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class ClaimCheckBudget:
|
|
25
|
+
"""Timeouts for claim command re-runs."""
|
|
26
|
+
|
|
27
|
+
per_command_timeout: int = 90
|
|
28
|
+
total_timeout: int = 120
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class CommandOutcome:
|
|
33
|
+
exit_code: int | None
|
|
34
|
+
output_tail: str
|
|
35
|
+
duration: float
|
|
36
|
+
timed_out: bool
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _tail(*chunks: str | bytes | None) -> str:
|
|
40
|
+
parts: list[str] = []
|
|
41
|
+
for chunk in chunks:
|
|
42
|
+
if chunk is None:
|
|
43
|
+
continue
|
|
44
|
+
if isinstance(chunk, bytes):
|
|
45
|
+
chunk = chunk.decode("utf-8", errors="replace")
|
|
46
|
+
if chunk.strip():
|
|
47
|
+
parts.append(chunk)
|
|
48
|
+
combined = "\n".join(parts)
|
|
49
|
+
lines = combined.splitlines()
|
|
50
|
+
return "\n".join(lines[-TAIL_LINES:])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _argv_to_shell(cmd: list[str] | str) -> str:
|
|
54
|
+
if isinstance(cmd, str):
|
|
55
|
+
return cmd
|
|
56
|
+
return shlex.join(cmd)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def run_command(cmd: str, cwd: Path, timeout: int) -> CommandOutcome:
|
|
60
|
+
try:
|
|
61
|
+
argv = shlex.split(cmd)
|
|
62
|
+
except ValueError:
|
|
63
|
+
argv = []
|
|
64
|
+
if argv and argv[0] in {"python", "python3"} and shutil.which(argv[0]) is None:
|
|
65
|
+
# Configs commonly use the portable `python` spelling even when macOS
|
|
66
|
+
# exposes only the interpreter running DevCouncil. Preserve the rest of
|
|
67
|
+
# the shell command while resolving that launcher deterministically.
|
|
68
|
+
cmd = f"{shlex.quote(sys.executable)}{cmd[len(argv[0]):]}"
|
|
69
|
+
start = time.monotonic()
|
|
70
|
+
try:
|
|
71
|
+
proc = subprocess.run(
|
|
72
|
+
cmd,
|
|
73
|
+
shell=True,
|
|
74
|
+
cwd=str(cwd),
|
|
75
|
+
capture_output=True,
|
|
76
|
+
text=True,
|
|
77
|
+
errors="replace",
|
|
78
|
+
timeout=timeout,
|
|
79
|
+
)
|
|
80
|
+
return CommandOutcome(
|
|
81
|
+
exit_code=proc.returncode,
|
|
82
|
+
output_tail=_tail(proc.stdout, proc.stderr),
|
|
83
|
+
duration=time.monotonic() - start,
|
|
84
|
+
timed_out=False,
|
|
85
|
+
)
|
|
86
|
+
except subprocess.TimeoutExpired as exc:
|
|
87
|
+
return CommandOutcome(
|
|
88
|
+
exit_code=None,
|
|
89
|
+
output_tail=_tail(exc.stdout, exc.stderr),
|
|
90
|
+
duration=time.monotonic() - start,
|
|
91
|
+
timed_out=True,
|
|
92
|
+
)
|
|
93
|
+
except OSError as exc:
|
|
94
|
+
return CommandOutcome(
|
|
95
|
+
exit_code=None,
|
|
96
|
+
output_tail=f"failed to launch command: {exc}",
|
|
97
|
+
duration=time.monotonic() - start,
|
|
98
|
+
timed_out=False,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _git_output(cwd: Path, *args: str) -> str | None:
|
|
103
|
+
try:
|
|
104
|
+
proc = subprocess.run(
|
|
105
|
+
["git", *args],
|
|
106
|
+
cwd=str(cwd),
|
|
107
|
+
capture_output=True,
|
|
108
|
+
text=True,
|
|
109
|
+
errors="replace",
|
|
110
|
+
timeout=15,
|
|
111
|
+
)
|
|
112
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
113
|
+
return None
|
|
114
|
+
return proc.stdout if proc.returncode == 0 else None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def git_toplevel(cwd: Path) -> Path | None:
|
|
118
|
+
out = _git_output(cwd, "rev-parse", "--show-toplevel")
|
|
119
|
+
return Path(out.strip()) if out and out.strip() else None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def last_commit_paths(cwd: Path) -> set[str]:
|
|
123
|
+
out = _git_output(cwd, "log", "-1", "--name-only", "--pretty=format:")
|
|
124
|
+
if not out:
|
|
125
|
+
return set()
|
|
126
|
+
return {line.strip().strip('"') for line in out.splitlines() if line.strip()}
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def git_changed_files(cwd: Path) -> dict[str, str] | None:
|
|
130
|
+
try:
|
|
131
|
+
proc = subprocess.run(
|
|
132
|
+
["git", "status", "--porcelain", "-uall"],
|
|
133
|
+
cwd=str(cwd),
|
|
134
|
+
capture_output=True,
|
|
135
|
+
text=True,
|
|
136
|
+
errors="replace",
|
|
137
|
+
timeout=15,
|
|
138
|
+
)
|
|
139
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
140
|
+
return None
|
|
141
|
+
if proc.returncode != 0:
|
|
142
|
+
return None
|
|
143
|
+
changed: dict[str, str] = {}
|
|
144
|
+
for line in proc.stdout.splitlines():
|
|
145
|
+
if len(line) < 4:
|
|
146
|
+
continue
|
|
147
|
+
status, path = line[:2], line[3:]
|
|
148
|
+
if " -> " in path:
|
|
149
|
+
path = path.split(" -> ", 1)[1]
|
|
150
|
+
changed[path.strip().strip('"')] = status
|
|
151
|
+
return changed
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _normalize(path: str) -> str:
|
|
155
|
+
return path.replace("\\", "/").strip("/").lower()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _in_changed(rel_path: str, changed: dict[str, str]) -> bool:
|
|
159
|
+
target = _normalize(rel_path)
|
|
160
|
+
return any(_normalize(p) == target for p in changed)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class _Runner:
|
|
164
|
+
def __init__(self, budget: ClaimCheckBudget, cwd: Path):
|
|
165
|
+
self.budget = budget
|
|
166
|
+
self.cwd = cwd
|
|
167
|
+
self.deadline = time.monotonic() + budget.total_timeout
|
|
168
|
+
self.cache: dict[str, CommandOutcome] = {}
|
|
169
|
+
|
|
170
|
+
def run(self, cmd: str) -> CommandOutcome | None:
|
|
171
|
+
if cmd in self.cache:
|
|
172
|
+
return self.cache[cmd]
|
|
173
|
+
remaining = self.deadline - time.monotonic()
|
|
174
|
+
if remaining <= 0:
|
|
175
|
+
return None
|
|
176
|
+
outcome = run_command(
|
|
177
|
+
cmd, self.cwd, timeout=int(min(self.budget.per_command_timeout, max(remaining, 1)))
|
|
178
|
+
)
|
|
179
|
+
self.cache[cmd] = outcome
|
|
180
|
+
return outcome
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
_NOT_FOUND_MARKERS = (
|
|
184
|
+
"is not recognized as an internal or external command",
|
|
185
|
+
"is not recognized as a name of a cmdlet",
|
|
186
|
+
"command not found",
|
|
187
|
+
"commandnotfoundexception",
|
|
188
|
+
"no such file or directory",
|
|
189
|
+
"failed to launch command",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _command_not_found(outcome: CommandOutcome) -> bool:
|
|
194
|
+
if outcome.exit_code in (127, 9009):
|
|
195
|
+
return True
|
|
196
|
+
tail = outcome.output_tail.lower()
|
|
197
|
+
return outcome.exit_code != 0 and any(marker in tail for marker in _NOT_FOUND_MARKERS)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _command_result(assertion: Assertion, cmd: str | None, label: str, runner: _Runner) -> CheckResult:
|
|
201
|
+
if not cmd:
|
|
202
|
+
return CheckResult(assertion, Status.UNVERIFIABLE, f"no {label} command configured")
|
|
203
|
+
outcome = runner.run(cmd)
|
|
204
|
+
if outcome is None:
|
|
205
|
+
return CheckResult(assertion, Status.SKIPPED, "verification budget exhausted")
|
|
206
|
+
if outcome.timed_out:
|
|
207
|
+
return CheckResult(
|
|
208
|
+
assertion, Status.UNVERIFIABLE, f"`{cmd}` timed out after {outcome.duration:.0f}s"
|
|
209
|
+
)
|
|
210
|
+
if _command_not_found(outcome):
|
|
211
|
+
return CheckResult(
|
|
212
|
+
assertion,
|
|
213
|
+
Status.UNVERIFIABLE,
|
|
214
|
+
f"`{cmd}` could not be launched (command not found) — fix the configured {label} command",
|
|
215
|
+
)
|
|
216
|
+
if outcome.exit_code == 0:
|
|
217
|
+
return CheckResult(assertion, Status.PASS, f"`{cmd}` exited 0 in {outcome.duration:.1f}s")
|
|
218
|
+
return CheckResult(
|
|
219
|
+
assertion,
|
|
220
|
+
Status.FAIL,
|
|
221
|
+
f"`{cmd}` exited with exit code {outcome.exit_code}.\nOutput tail:\n{outcome.output_tail}",
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def _resolve_in_repo(target: str, cwd: Path) -> Path | None:
|
|
226
|
+
candidate = Path(target)
|
|
227
|
+
if not candidate.is_absolute():
|
|
228
|
+
candidate = cwd / candidate
|
|
229
|
+
try:
|
|
230
|
+
resolved = candidate.resolve()
|
|
231
|
+
if not resolved.is_relative_to(cwd.resolve()):
|
|
232
|
+
return None
|
|
233
|
+
return resolved
|
|
234
|
+
except OSError:
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
@dataclass
|
|
239
|
+
class _GitInfo:
|
|
240
|
+
toplevel: Path
|
|
241
|
+
changed: dict[str, str]
|
|
242
|
+
last_commit: set[str]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _git_info(cwd: Path) -> _GitInfo | None:
|
|
246
|
+
toplevel = git_toplevel(cwd)
|
|
247
|
+
if toplevel is None:
|
|
248
|
+
return None
|
|
249
|
+
changed = git_changed_files(cwd) or {}
|
|
250
|
+
return _GitInfo(toplevel=toplevel, changed=changed, last_commit=last_commit_paths(cwd))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _find_unique_basename(name: str, cwd: Path, git: _GitInfo | None) -> Path | None:
|
|
254
|
+
if git is None:
|
|
255
|
+
return None
|
|
256
|
+
tracked = _git_output(cwd, "ls-files")
|
|
257
|
+
pool = set(tracked.splitlines() if tracked else []) | set(git.changed) | git.last_commit
|
|
258
|
+
matches = {p for p in pool if p.strip() and _normalize(p).rsplit("/", 1)[-1] == _normalize(name)}
|
|
259
|
+
if len(matches) != 1:
|
|
260
|
+
return None
|
|
261
|
+
return git.toplevel / matches.pop()
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _changed_status(rel_path: str, changed: dict[str, str]) -> str:
|
|
265
|
+
target = _normalize(rel_path)
|
|
266
|
+
for path, status in changed.items():
|
|
267
|
+
if _normalize(path) == target:
|
|
268
|
+
return status.strip() or status
|
|
269
|
+
return "?"
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def _file_result(assertion: Assertion, cwd: Path, git: _GitInfo | None) -> CheckResult:
|
|
273
|
+
target = assertion.target or ""
|
|
274
|
+
resolved = _resolve_in_repo(target, cwd)
|
|
275
|
+
if resolved is None:
|
|
276
|
+
return CheckResult(
|
|
277
|
+
assertion, Status.UNVERIFIABLE, f"claimed path `{target}` is outside the working directory"
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
if not resolved.exists() and "/" not in target and "\\" not in target:
|
|
281
|
+
alt = _find_unique_basename(target, cwd, git)
|
|
282
|
+
if alt is not None and alt.exists():
|
|
283
|
+
resolved = alt.resolve()
|
|
284
|
+
|
|
285
|
+
if not resolved.exists():
|
|
286
|
+
return CheckResult(assertion, Status.FAIL, f"claimed file `{target}` does not exist")
|
|
287
|
+
|
|
288
|
+
if git is None:
|
|
289
|
+
return CheckResult(assertion, Status.PASS, f"`{target}` exists")
|
|
290
|
+
|
|
291
|
+
try:
|
|
292
|
+
rel = resolved.relative_to(git.toplevel.resolve()).as_posix()
|
|
293
|
+
except ValueError:
|
|
294
|
+
return CheckResult(assertion, Status.PASS, f"`{target}` exists (outside the git repo)")
|
|
295
|
+
|
|
296
|
+
if _in_changed(rel, git.changed):
|
|
297
|
+
return CheckResult(
|
|
298
|
+
assertion,
|
|
299
|
+
Status.PASS,
|
|
300
|
+
f"`{target}` exists (git status: {_changed_status(rel, git.changed)})",
|
|
301
|
+
)
|
|
302
|
+
if any(_normalize(p) == _normalize(rel) for p in git.last_commit):
|
|
303
|
+
return CheckResult(
|
|
304
|
+
assertion, Status.PASS, f"`{target}` exists (touched by the most recent commit)"
|
|
305
|
+
)
|
|
306
|
+
return CheckResult(
|
|
307
|
+
assertion,
|
|
308
|
+
Status.UNVERIFIABLE,
|
|
309
|
+
f"`{target}` exists but git shows no recent change to it; cannot confirm it was "
|
|
310
|
+
"created/modified in this session",
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@dataclass
|
|
315
|
+
class ResolvedCommands:
|
|
316
|
+
test: str | None = None
|
|
317
|
+
build: str | None = None # typecheck in DevCouncil
|
|
318
|
+
lint: str | None = None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def resolve_commands_from_config(commands_cfg: object) -> ResolvedCommands:
|
|
322
|
+
"""Map DevCouncil CommandsConfig lists to shell command strings."""
|
|
323
|
+
def first(attr: str) -> str | None:
|
|
324
|
+
value = getattr(commands_cfg, attr, None) or []
|
|
325
|
+
if isinstance(value, str) and value.strip():
|
|
326
|
+
return value.strip()
|
|
327
|
+
if isinstance(value, list) and value:
|
|
328
|
+
first_cmd = value[0]
|
|
329
|
+
if isinstance(first_cmd, list):
|
|
330
|
+
return _argv_to_shell(first_cmd)
|
|
331
|
+
if isinstance(first_cmd, str) and first_cmd.strip():
|
|
332
|
+
return first_cmd.strip()
|
|
333
|
+
return None
|
|
334
|
+
|
|
335
|
+
return ResolvedCommands(test=first("test"), build=first("typecheck"), lint=first("lint"))
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def _matching_config_command(target: str, resolved: ResolvedCommands) -> str | None:
|
|
339
|
+
wanted = " ".join(target.split()).lower()
|
|
340
|
+
for cmd in (resolved.test, resolved.build, resolved.lint):
|
|
341
|
+
if cmd and wanted == " ".join(cmd.split()).lower():
|
|
342
|
+
return cmd
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def execute_checks(
|
|
347
|
+
assertions: list[Assertion],
|
|
348
|
+
*,
|
|
349
|
+
cwd: Path,
|
|
350
|
+
commands: ResolvedCommands,
|
|
351
|
+
budget: ClaimCheckBudget | None = None,
|
|
352
|
+
) -> list[CheckResult]:
|
|
353
|
+
"""Independently verify each assertion against the real environment."""
|
|
354
|
+
runner = _Runner(budget or ClaimCheckBudget(), cwd)
|
|
355
|
+
git = _git_info(cwd)
|
|
356
|
+
results: list[CheckResult] = []
|
|
357
|
+
|
|
358
|
+
for assertion in assertions:
|
|
359
|
+
kind = assertion.kind
|
|
360
|
+
if kind is Kind.TESTS_PASS:
|
|
361
|
+
results.append(_command_result(assertion, commands.test, "test", runner))
|
|
362
|
+
elif kind is Kind.BUILD_SUCCEEDS:
|
|
363
|
+
results.append(_command_result(assertion, commands.build, "typecheck/build", runner))
|
|
364
|
+
elif kind is Kind.LINT_CLEAN:
|
|
365
|
+
results.append(_command_result(assertion, commands.lint, "lint", runner))
|
|
366
|
+
elif kind in (Kind.FILE_CREATED, Kind.FILE_UPDATED):
|
|
367
|
+
results.append(_file_result(assertion, cwd, git))
|
|
368
|
+
elif kind is Kind.COMMAND_SUCCEEDED:
|
|
369
|
+
cmd = _matching_config_command(assertion.target or "", commands)
|
|
370
|
+
if cmd is None:
|
|
371
|
+
results.append(
|
|
372
|
+
CheckResult(
|
|
373
|
+
assertion,
|
|
374
|
+
Status.UNVERIFIABLE,
|
|
375
|
+
f"claimed command `{assertion.target}` does not match any configured "
|
|
376
|
+
"command; not re-running arbitrary commands",
|
|
377
|
+
)
|
|
378
|
+
)
|
|
379
|
+
else:
|
|
380
|
+
results.append(_command_result(assertion, cmd, "matched", runner))
|
|
381
|
+
elif kind is Kind.GENERIC_DONE:
|
|
382
|
+
if commands.test:
|
|
383
|
+
results.append(_command_result(assertion, commands.test, "test", runner))
|
|
384
|
+
else:
|
|
385
|
+
results.append(
|
|
386
|
+
CheckResult(
|
|
387
|
+
assertion,
|
|
388
|
+
Status.UNVERIFIABLE,
|
|
389
|
+
"generic completion claim with no configured checks",
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
else:
|
|
393
|
+
results.append(CheckResult(assertion, Status.UNVERIFIABLE, f"no checker for kind {kind}"))
|
|
394
|
+
|
|
395
|
+
return results
|