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,98 @@
|
|
|
1
|
+
"""Planned-file compliance checks extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, List
|
|
7
|
+
|
|
8
|
+
from devcouncil.domain.gap import Gap
|
|
9
|
+
from devcouncil.domain.task import Task
|
|
10
|
+
|
|
11
|
+
_DEP_FILES = {
|
|
12
|
+
"package.json", "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
|
|
13
|
+
"requirements.txt", "pyproject.toml", "uv.lock", "Pipfile.lock",
|
|
14
|
+
"go.mod", "go.sum", "Cargo.toml", "Cargo.lock",
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def dependency_files_changed(changed_files: List[str]) -> List[str]:
|
|
19
|
+
"""Return changed dependency manifest paths."""
|
|
20
|
+
return [path for path in changed_files if Path(path).name in _DEP_FILES]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def detect_no_work_gap(
|
|
24
|
+
*,
|
|
25
|
+
task: Task,
|
|
26
|
+
work_present: bool,
|
|
27
|
+
next_gap_id: Callable[[str, str], str],
|
|
28
|
+
) -> Gap | None:
|
|
29
|
+
"""Block when a task expects file changes but produced none."""
|
|
30
|
+
expects_change = any(pf.allowed_change != "read_only" for pf in task.planned_files)
|
|
31
|
+
if work_present or not expects_change:
|
|
32
|
+
return None
|
|
33
|
+
return Gap(
|
|
34
|
+
id=next_gap_id(task.id, "NODIFF"),
|
|
35
|
+
severity="high",
|
|
36
|
+
gap_type="task_not_implemented",
|
|
37
|
+
task_id=task.id,
|
|
38
|
+
description=(
|
|
39
|
+
f"Task {task.id} declares files to create or modify, but produced no "
|
|
40
|
+
"changes. Verification cannot prove work that does not exist."
|
|
41
|
+
),
|
|
42
|
+
evidence=[f"planned files expecting change: {sorted(p.path for p in task.planned_files if p.allowed_change != 'read_only')}"],
|
|
43
|
+
recommended_fix=(
|
|
44
|
+
"Implement the planned changes so the diff is non-empty, then re-verify. "
|
|
45
|
+
"If you did make changes, ensure they are saved and visible to git "
|
|
46
|
+
"(not reverted, stashed, or written outside the project root)."
|
|
47
|
+
),
|
|
48
|
+
blocking=True,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def detect_planned_file_gaps(
|
|
53
|
+
*,
|
|
54
|
+
task: Task,
|
|
55
|
+
changed_files: List[str],
|
|
56
|
+
next_gap_id: Callable[[str, str], str],
|
|
57
|
+
) -> List[Gap]:
|
|
58
|
+
"""Advisory gaps for planned files that were not modified."""
|
|
59
|
+
gaps: List[Gap] = []
|
|
60
|
+
changed_set = set(changed_files)
|
|
61
|
+
for pf in task.planned_files:
|
|
62
|
+
if pf.path not in changed_set and pf.allowed_change != "read_only":
|
|
63
|
+
gaps.append(Gap(
|
|
64
|
+
id=next_gap_id(task.id, f"FILE-{pf.path}"),
|
|
65
|
+
severity="medium",
|
|
66
|
+
gap_type="planned_file_not_changed",
|
|
67
|
+
task_id=task.id,
|
|
68
|
+
description=f"Planned file {pf.path} was not modified.",
|
|
69
|
+
recommended_fix=f"Modify {pf.path} as planned or update the task.",
|
|
70
|
+
blocking=False,
|
|
71
|
+
file=pf.path,
|
|
72
|
+
))
|
|
73
|
+
return gaps
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def detect_dependency_risk_gaps(
|
|
77
|
+
*,
|
|
78
|
+
task: Task,
|
|
79
|
+
changed_files: List[str],
|
|
80
|
+
planned_paths: set[str],
|
|
81
|
+
next_gap_id: Callable[[str, str], str],
|
|
82
|
+
) -> List[Gap]:
|
|
83
|
+
"""Blocking gaps for dependency manifest edits outside the plan."""
|
|
84
|
+
gaps: List[Gap] = []
|
|
85
|
+
for dep_file in dependency_files_changed(changed_files):
|
|
86
|
+
if dep_file not in planned_paths:
|
|
87
|
+
gaps.append(Gap(
|
|
88
|
+
id=next_gap_id(task.id, f"DEP-{dep_file}"),
|
|
89
|
+
severity="high",
|
|
90
|
+
gap_type="dependency_risk",
|
|
91
|
+
task_id=task.id,
|
|
92
|
+
description=f"Dependency file {dep_file} was modified without being in planned files.",
|
|
93
|
+
evidence=[dep_file],
|
|
94
|
+
recommended_fix=f"Justify the dependency change or revert {dep_file}.",
|
|
95
|
+
blocking=True,
|
|
96
|
+
file=dep_file,
|
|
97
|
+
))
|
|
98
|
+
return gaps
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Semantic-index drift checks extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Callable, List, Optional
|
|
10
|
+
|
|
11
|
+
from devcouncil.domain.gap import Gap
|
|
12
|
+
from devcouncil.domain.requirement import Requirement
|
|
13
|
+
from devcouncil.domain.task import Task
|
|
14
|
+
from devcouncil.utils.json_persist import read_json
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def task_intent_text(task: Task, requirements: Optional[List[Requirement]]) -> str:
|
|
20
|
+
"""Lowercased text describing what the task is meant to do."""
|
|
21
|
+
parts = [task.title or "", task.description or ""]
|
|
22
|
+
if requirements:
|
|
23
|
+
ac_ids = set(task.acceptance_criterion_ids)
|
|
24
|
+
for req in requirements:
|
|
25
|
+
for ac in req.acceptance_criteria:
|
|
26
|
+
if ac.id in ac_ids:
|
|
27
|
+
parts.append(ac.description or "")
|
|
28
|
+
return " ".join(parts).lower()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def import_top_level(statement: str) -> Optional[str]:
|
|
32
|
+
"""Top-level package of an import statement, or None for relative/local/unparseable."""
|
|
33
|
+
s = (statement or "").strip()
|
|
34
|
+
if s.startswith("import "):
|
|
35
|
+
first = s[len("import "):].split(",")[0].strip()
|
|
36
|
+
top = first.split(" as ")[0].strip().split(".")[0].strip()
|
|
37
|
+
return top or None
|
|
38
|
+
if s.startswith("from "):
|
|
39
|
+
rest = s[len("from "):].lstrip()
|
|
40
|
+
if rest.startswith("."):
|
|
41
|
+
return None
|
|
42
|
+
mod = rest.split(" import ")[0].strip()
|
|
43
|
+
return (mod.split(".")[0].strip() or None) if mod else None
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def stdlib_modules() -> frozenset[str]:
|
|
48
|
+
names = getattr(sys, "stdlib_module_names", None)
|
|
49
|
+
return frozenset(names) if names else frozenset()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def load_project_dependencies(project_root: Path) -> set[str]:
|
|
53
|
+
"""Lower-cased distribution names declared by the project."""
|
|
54
|
+
deps: set[str] = set()
|
|
55
|
+
split_re = r"[><=!~;\[\] ]"
|
|
56
|
+
pyproject = project_root / "pyproject.toml"
|
|
57
|
+
if pyproject.exists():
|
|
58
|
+
try:
|
|
59
|
+
import tomllib
|
|
60
|
+
|
|
61
|
+
data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
|
|
62
|
+
project = data.get("project", {}) or {}
|
|
63
|
+
for dep in project.get("dependencies", []) or []:
|
|
64
|
+
pkg = re.split(split_re, dep.strip())[0].strip().lower()
|
|
65
|
+
if pkg:
|
|
66
|
+
deps.add(pkg)
|
|
67
|
+
for group in (project.get("optional-dependencies", {}) or {}).values():
|
|
68
|
+
for dep in group or []:
|
|
69
|
+
pkg = re.split(split_re, dep.strip())[0].strip().lower()
|
|
70
|
+
if pkg:
|
|
71
|
+
deps.add(pkg)
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
requirements = project_root / "requirements.txt"
|
|
75
|
+
if requirements.exists():
|
|
76
|
+
try:
|
|
77
|
+
for line in requirements.read_text(encoding="utf-8").splitlines():
|
|
78
|
+
line = line.strip()
|
|
79
|
+
if line and not line.startswith("#"):
|
|
80
|
+
pkg = re.split(split_re, line)[0].strip().lower()
|
|
81
|
+
if pkg:
|
|
82
|
+
deps.add(pkg)
|
|
83
|
+
except Exception:
|
|
84
|
+
pass
|
|
85
|
+
package_json = project_root / "package.json"
|
|
86
|
+
if package_json.exists():
|
|
87
|
+
try:
|
|
88
|
+
data = read_json(package_json)
|
|
89
|
+
for key in ("dependencies", "devDependencies", "optionalDependencies"):
|
|
90
|
+
deps.update(k.lower() for k in (data.get(key) or {}).keys())
|
|
91
|
+
except Exception:
|
|
92
|
+
pass
|
|
93
|
+
return deps
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def is_new_third_party_import(top: Optional[str], *, project_deps: set[str]) -> bool:
|
|
97
|
+
"""True only when ``top`` is a genuinely new, undeclared third-party package."""
|
|
98
|
+
if not top:
|
|
99
|
+
return False
|
|
100
|
+
if top in stdlib_modules():
|
|
101
|
+
return False
|
|
102
|
+
if top.lower() in project_deps:
|
|
103
|
+
return False
|
|
104
|
+
try:
|
|
105
|
+
import importlib.util
|
|
106
|
+
|
|
107
|
+
if importlib.util.find_spec(top) is not None:
|
|
108
|
+
return False
|
|
109
|
+
except Exception:
|
|
110
|
+
return False
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def detect_semantic_diff_gaps(
|
|
115
|
+
*,
|
|
116
|
+
project_root: Path,
|
|
117
|
+
task: Task,
|
|
118
|
+
requirements: Optional[List[Requirement]],
|
|
119
|
+
next_gap_id: Callable[[str, str], str],
|
|
120
|
+
project_deps: set[str],
|
|
121
|
+
) -> List[Gap]:
|
|
122
|
+
"""Run semantic-index drift checks when an after snapshot exists."""
|
|
123
|
+
gaps: List[Gap] = []
|
|
124
|
+
after_path = project_root / ".devcouncil" / "semantic" / task.id / "after.json"
|
|
125
|
+
if not after_path.exists():
|
|
126
|
+
return gaps
|
|
127
|
+
try:
|
|
128
|
+
from devcouncil.indexing.semantic_index import SemanticIndex
|
|
129
|
+
|
|
130
|
+
result = SemanticIndex(project_root).diff(task.id)
|
|
131
|
+
except Exception as exc:
|
|
132
|
+
logger.warning("Semantic diff check failed for %s; skipping semantic gaps: %s", task.id, exc)
|
|
133
|
+
return gaps
|
|
134
|
+
|
|
135
|
+
planned_paths = {pf.path for pf in task.planned_files}
|
|
136
|
+
classifications = result.get("classifications", [])
|
|
137
|
+
readded_public = {
|
|
138
|
+
item.get("name") for item in classifications
|
|
139
|
+
if item.get("type") == "exported_symbol_added" and item.get("name")
|
|
140
|
+
}
|
|
141
|
+
intent_text = task_intent_text(task, requirements)
|
|
142
|
+
for item in classifications:
|
|
143
|
+
change_type = item.get("type", "")
|
|
144
|
+
path = item.get("path", "")
|
|
145
|
+
if change_type == "exported_symbol_removed":
|
|
146
|
+
name = item.get("name", "")
|
|
147
|
+
moved = name in readded_public
|
|
148
|
+
intended = bool(name) and name.lower() in intent_text
|
|
149
|
+
gaps.append(Gap(
|
|
150
|
+
id=next_gap_id(task.id, f"DRIFT-{path}:{name}"),
|
|
151
|
+
severity="high",
|
|
152
|
+
gap_type="architecture_drift",
|
|
153
|
+
task_id=task.id,
|
|
154
|
+
description=(
|
|
155
|
+
f"Public symbol '{name}' was removed from {path} — possible scope "
|
|
156
|
+
"drift: the executor changed a public API the task did not call for."
|
|
157
|
+
),
|
|
158
|
+
evidence=[f"{path}:{name}"],
|
|
159
|
+
recommended_fix=(
|
|
160
|
+
"Restore the removed public symbol. If its removal IS part of this "
|
|
161
|
+
"task, state that in the task description / acceptance criteria so the "
|
|
162
|
+
"change is an intended, reviewed decision rather than silent drift."
|
|
163
|
+
),
|
|
164
|
+
blocking=(not moved and not intended),
|
|
165
|
+
file=path,
|
|
166
|
+
))
|
|
167
|
+
elif change_type == "public_api_change" and path not in planned_paths:
|
|
168
|
+
gaps.append(Gap(
|
|
169
|
+
id=next_gap_id(task.id, f"SEM-{path}"),
|
|
170
|
+
severity="high",
|
|
171
|
+
gap_type="architecture_drift",
|
|
172
|
+
task_id=task.id,
|
|
173
|
+
description=f"Unplanned public API change detected in {path}.",
|
|
174
|
+
evidence=[path],
|
|
175
|
+
recommended_fix="Add file to planned_files and document acceptance criteria.",
|
|
176
|
+
blocking=not bool(task.acceptance_criterion_ids),
|
|
177
|
+
))
|
|
178
|
+
elif change_type == "public_api_change" and path in planned_paths:
|
|
179
|
+
gaps.append(Gap(
|
|
180
|
+
id=next_gap_id(task.id, f"SIGDRIFT-{path}:{item.get('name', '')}"),
|
|
181
|
+
severity="medium",
|
|
182
|
+
gap_type="architecture_drift",
|
|
183
|
+
task_id=task.id,
|
|
184
|
+
description=(
|
|
185
|
+
f"Public API signature change in planned file {path}"
|
|
186
|
+
+ (f" ({item.get('name')})" if item.get("name") else "")
|
|
187
|
+
+ ". Confirm callers are updated and the change is intended."
|
|
188
|
+
),
|
|
189
|
+
evidence=[f"{path}:{item.get('name', '')}"],
|
|
190
|
+
recommended_fix=(
|
|
191
|
+
"If the signature change is part of this task, note it in the task "
|
|
192
|
+
"description / acceptance criteria; otherwise revert it."
|
|
193
|
+
),
|
|
194
|
+
blocking=False,
|
|
195
|
+
))
|
|
196
|
+
elif change_type == "import_dependency_change":
|
|
197
|
+
statement = item.get("statement", "")
|
|
198
|
+
top = import_top_level(statement)
|
|
199
|
+
new_third_party = is_new_third_party_import(top, project_deps=project_deps)
|
|
200
|
+
if new_third_party:
|
|
201
|
+
gaps.append(Gap(
|
|
202
|
+
id=next_gap_id(task.id, f"DEPADD-{path}:{top}"),
|
|
203
|
+
severity="high",
|
|
204
|
+
gap_type="dependency_risk",
|
|
205
|
+
task_id=task.id,
|
|
206
|
+
description=(
|
|
207
|
+
f"New undeclared third-party dependency '{top}' imported in {path} "
|
|
208
|
+
f"({statement.strip()}). Adding a dependency the task did not plan is "
|
|
209
|
+
"supply-chain drift."
|
|
210
|
+
),
|
|
211
|
+
evidence=[path, statement.strip()],
|
|
212
|
+
recommended_fix=(
|
|
213
|
+
f"Declare '{top}' in the project's dependencies and plan the change, "
|
|
214
|
+
"or use an existing/standard-library alternative."
|
|
215
|
+
),
|
|
216
|
+
blocking=True,
|
|
217
|
+
file=path,
|
|
218
|
+
))
|
|
219
|
+
elif path not in planned_paths:
|
|
220
|
+
gaps.append(Gap(
|
|
221
|
+
id=next_gap_id(task.id, f"IMP-{path}"),
|
|
222
|
+
severity="medium",
|
|
223
|
+
gap_type="dependency_risk",
|
|
224
|
+
task_id=task.id,
|
|
225
|
+
description=f"Import dependency change in {path}.",
|
|
226
|
+
evidence=[path],
|
|
227
|
+
recommended_fix="Confirm dependency change is intentional.",
|
|
228
|
+
blocking=False,
|
|
229
|
+
))
|
|
230
|
+
elif change_type == "config_schema_dependency_change" and path not in planned_paths:
|
|
231
|
+
gaps.append(Gap(
|
|
232
|
+
id=next_gap_id(task.id, f"CFG-{path}"),
|
|
233
|
+
severity="high",
|
|
234
|
+
gap_type="dependency_risk",
|
|
235
|
+
task_id=task.id,
|
|
236
|
+
description=f"Config/schema change detected in {path}.",
|
|
237
|
+
evidence=[path],
|
|
238
|
+
recommended_fix="Plan the config change or revert it.",
|
|
239
|
+
blocking=True,
|
|
240
|
+
))
|
|
241
|
+
return gaps
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Stale-map gate: flag when ``repo_map.json`` is missing or lags git HEAD.
|
|
2
|
+
|
|
3
|
+
Missing map is always stale (fail-closed on hard rigor). Advisory on easy/normal;
|
|
4
|
+
blocking on hard. Never raises.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Callable, List, Mapping, Optional
|
|
12
|
+
|
|
13
|
+
from devcouncil.domain.gap import Gap
|
|
14
|
+
from devcouncil.domain.task import Task
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def detect_stale_map_gaps(
|
|
20
|
+
*,
|
|
21
|
+
task: Task,
|
|
22
|
+
project_root: Path,
|
|
23
|
+
next_gap_id: Callable[[str, str], str],
|
|
24
|
+
stale_map_enabled: bool = True,
|
|
25
|
+
stale_map_blocking: bool = False,
|
|
26
|
+
repo_map: Optional[Mapping] = None,
|
|
27
|
+
) -> List[Gap]:
|
|
28
|
+
"""Emit a ``stale_map`` gap when the on-disk map no longer matches the repo."""
|
|
29
|
+
if not stale_map_enabled:
|
|
30
|
+
return []
|
|
31
|
+
try:
|
|
32
|
+
data = repo_map
|
|
33
|
+
if data is None:
|
|
34
|
+
map_path = project_root / ".devcouncil" / "repo_map.json"
|
|
35
|
+
if not map_path.is_file():
|
|
36
|
+
return [Gap(
|
|
37
|
+
id=next_gap_id(task.id, "STALEMAP-MISSING"),
|
|
38
|
+
severity="high" if stale_map_blocking else "medium",
|
|
39
|
+
gap_type="stale_map",
|
|
40
|
+
task_id=task.id,
|
|
41
|
+
description=(
|
|
42
|
+
"Repository map ``.devcouncil/repo_map.json`` is missing. "
|
|
43
|
+
"Subsystem neighbors, dependents, and liveness gates cannot run."
|
|
44
|
+
),
|
|
45
|
+
evidence=[".devcouncil/repo_map.json"],
|
|
46
|
+
recommended_fix="Run `dev map` to generate the repository map, then re-verify.",
|
|
47
|
+
blocking=stale_map_blocking,
|
|
48
|
+
suggested_command="dev map",
|
|
49
|
+
)]
|
|
50
|
+
from devcouncil.utils.json_persist import read_json
|
|
51
|
+
|
|
52
|
+
loaded = read_json(map_path)
|
|
53
|
+
if not isinstance(loaded, dict):
|
|
54
|
+
return []
|
|
55
|
+
data = loaded
|
|
56
|
+
|
|
57
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
58
|
+
|
|
59
|
+
if not RepoMapper(project_root).map_is_stale(dict(data)):
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
stored_head = str(data.get("generated_head") or "") or "(unknown)"
|
|
63
|
+
return [Gap(
|
|
64
|
+
id=next_gap_id(task.id, "STALEMAP"),
|
|
65
|
+
severity="high" if stale_map_blocking else "medium",
|
|
66
|
+
gap_type="stale_map",
|
|
67
|
+
task_id=task.id,
|
|
68
|
+
description=(
|
|
69
|
+
"The repository map (``.devcouncil/repo_map.json``) is behind the "
|
|
70
|
+
f"current code (stored HEAD {stored_head}). Subsystem neighbors, "
|
|
71
|
+
"dependents, and liveness lists may be wrong until it is refreshed."
|
|
72
|
+
),
|
|
73
|
+
evidence=[".devcouncil/repo_map.json", f"generated_head:{stored_head}"],
|
|
74
|
+
recommended_fix="Run `dev map` to regenerate the repository map, then re-verify.",
|
|
75
|
+
blocking=stale_map_blocking,
|
|
76
|
+
suggested_command="dev map",
|
|
77
|
+
)]
|
|
78
|
+
except Exception:
|
|
79
|
+
logger.debug("detect_stale_map_gaps failed; degrading to zero gaps", exc_info=True)
|
|
80
|
+
return []
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Stub detection helpers extracted from Verifier."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Callable, List
|
|
7
|
+
|
|
8
|
+
from devcouncil.domain.gap import Gap
|
|
9
|
+
from devcouncil.domain.task import Task
|
|
10
|
+
from devcouncil.verification.stub_detector import (
|
|
11
|
+
detect_stub_declarations,
|
|
12
|
+
detect_stubs,
|
|
13
|
+
task_allows_scaffolding,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def detect_stub_gaps(
|
|
18
|
+
*,
|
|
19
|
+
task: Task,
|
|
20
|
+
diff_content: str,
|
|
21
|
+
project_root: Path,
|
|
22
|
+
stub_enabled: bool,
|
|
23
|
+
stub_blocking: bool,
|
|
24
|
+
next_gap_id: Callable[[str, str], str],
|
|
25
|
+
) -> List[Gap]:
|
|
26
|
+
"""Scan diff-added lines for stubs/TODOs and declared scaffolding markers."""
|
|
27
|
+
gaps: List[Gap] = []
|
|
28
|
+
if not stub_enabled or not diff_content:
|
|
29
|
+
return gaps
|
|
30
|
+
scaffolding_ok = task_allows_scaffolding(task)
|
|
31
|
+
for finding in detect_stubs(project_root, diff_content, honor_allow_stub=scaffolding_ok):
|
|
32
|
+
gaps.append(Gap(
|
|
33
|
+
id=next_gap_id(task.id, f"STUB-{finding.file}:{finding.line}"),
|
|
34
|
+
severity="high" if stub_blocking else "medium",
|
|
35
|
+
gap_type="stub_detected",
|
|
36
|
+
task_id=task.id,
|
|
37
|
+
description=(
|
|
38
|
+
f"Placeholder/incomplete code added at {finding.file}:{finding.line}: "
|
|
39
|
+
f"{finding.reason}."
|
|
40
|
+
),
|
|
41
|
+
evidence=[f"{finding.file}:{finding.line}", finding.snippet],
|
|
42
|
+
recommended_fix=(
|
|
43
|
+
f"Replace the stub/placeholder at {finding.file}:{finding.line} with a "
|
|
44
|
+
"real implementation, then re-verify. Intentional scaffolding requires "
|
|
45
|
+
"the task description to mention 'scaffolding' and the line to carry "
|
|
46
|
+
"'devcouncil: allow-stub'."
|
|
47
|
+
),
|
|
48
|
+
blocking=stub_blocking,
|
|
49
|
+
file=finding.file,
|
|
50
|
+
line=finding.line,
|
|
51
|
+
))
|
|
52
|
+
for decl in detect_stub_declarations(diff_content):
|
|
53
|
+
gaps.append(Gap(
|
|
54
|
+
id=next_gap_id(task.id, f"STUBDECL-{decl.file}:{decl.line}"),
|
|
55
|
+
severity="medium",
|
|
56
|
+
gap_type="stub_declared",
|
|
57
|
+
task_id=task.id,
|
|
58
|
+
description=(
|
|
59
|
+
f"Intentional stub declared at {decl.file}:{decl.line} "
|
|
60
|
+
f"({decl.snippet[:80]})."
|
|
61
|
+
),
|
|
62
|
+
evidence=[f"{decl.file}:{decl.line}", decl.snippet],
|
|
63
|
+
recommended_fix=(
|
|
64
|
+
"Review declared stubs before marking the task done; replace each "
|
|
65
|
+
"placeholder with a real implementation when scaffolding is complete."
|
|
66
|
+
),
|
|
67
|
+
blocking=False,
|
|
68
|
+
file=decl.file,
|
|
69
|
+
line=decl.line,
|
|
70
|
+
))
|
|
71
|
+
return gaps
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Advisory subsystem-boundary gate — architecture drift over the mapped area graph.
|
|
2
|
+
|
|
3
|
+
The repo map already knows which subsystems are *neighbors* (they hand off to each
|
|
4
|
+
other). A change that edits files in two subsystems the map does NOT consider neighbors,
|
|
5
|
+
and that the task plan never declared it would span, is a candidate architecture-drift
|
|
6
|
+
crossing: business logic leaking into a UI layer, a storage change reaching into the
|
|
7
|
+
council prompts, etc.
|
|
8
|
+
|
|
9
|
+
This gate flags those crossings. It is **advisory (non-blocking) by default** — an
|
|
10
|
+
undeclared crossing is often legitimate, so it should inform review rather than halt the
|
|
11
|
+
loop — but is ``blocking``-configurable for teams that want a hard architectural
|
|
12
|
+
boundary. It reads only ``repo_map.json`` (no model calls, no subprocess) and degrades to
|
|
13
|
+
a no-op when the map lacks ``subsystems``/``neighbors``.
|
|
14
|
+
|
|
15
|
+
"Plan coverage": a crossing between areas A and B is covered when the task's *planned
|
|
16
|
+
files* already live in both A and B — i.e. the plan declared the change would touch both
|
|
17
|
+
subsystems. Only crossings that reach an area the plan did not declare are flagged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
from typing import Callable, List, Mapping, Sequence
|
|
24
|
+
|
|
25
|
+
from devcouncil.domain.gap import Gap
|
|
26
|
+
from devcouncil.domain.task import Task
|
|
27
|
+
from devcouncil.indexing.subsystem_map import (
|
|
28
|
+
area_for_path,
|
|
29
|
+
areas_touched,
|
|
30
|
+
cross_boundary_pairs,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
# Cap emitted crossings so a sprawling change can't flood the gap list.
|
|
36
|
+
_MAX_CROSSINGS = 6
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def detect_subsystem_boundary_gaps(
|
|
40
|
+
*,
|
|
41
|
+
task: Task,
|
|
42
|
+
changed_files: Sequence[str],
|
|
43
|
+
repo_map: Mapping | None,
|
|
44
|
+
next_gap_id: Callable[[str, str], str],
|
|
45
|
+
blocking: bool = False,
|
|
46
|
+
) -> List[Gap]:
|
|
47
|
+
"""Flag edits that cross non-neighbor subsystem boundaries without plan coverage.
|
|
48
|
+
|
|
49
|
+
Returns advisory (or blocking, per ``blocking``) architecture-drift gaps — one per
|
|
50
|
+
undeclared crossing pair. Empty when the map is missing/degenerate, the change stays
|
|
51
|
+
within one area, or every crossing was already declared by the plan.
|
|
52
|
+
"""
|
|
53
|
+
if not repo_map or not (repo_map.get("subsystems") if hasattr(repo_map, "get") else None):
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
changed = [p.replace("\\", "/") for p in changed_files if p and p.strip()]
|
|
57
|
+
if len(areas_touched(changed, repo_map)) < 2:
|
|
58
|
+
return []
|
|
59
|
+
|
|
60
|
+
planned_paths = [pf.path.replace("\\", "/") for pf in task.planned_files]
|
|
61
|
+
planned_areas = set(areas_touched(planned_paths, repo_map))
|
|
62
|
+
|
|
63
|
+
crossings = cross_boundary_pairs(changed, repo_map)
|
|
64
|
+
if not crossings:
|
|
65
|
+
return []
|
|
66
|
+
|
|
67
|
+
gaps: List[Gap] = []
|
|
68
|
+
for area_a, area_b in crossings[:_MAX_CROSSINGS]:
|
|
69
|
+
# Covered when the plan declared files in BOTH areas of the crossing.
|
|
70
|
+
if area_a in planned_areas and area_b in planned_areas:
|
|
71
|
+
continue
|
|
72
|
+
undeclared = sorted({area_a, area_b} - planned_areas)
|
|
73
|
+
files_a = [f for f in changed if area_for_path(f, repo_map) == area_a]
|
|
74
|
+
files_b = [f for f in changed if area_for_path(f, repo_map) == area_b]
|
|
75
|
+
evidence = [f"{area_a}: " + ", ".join(files_a[:5])] if files_a else []
|
|
76
|
+
if files_b:
|
|
77
|
+
evidence.append(f"{area_b}: " + ", ".join(files_b[:5]))
|
|
78
|
+
gaps.append(Gap(
|
|
79
|
+
id=next_gap_id(task.id, f"BOUNDARY-{area_a}-{area_b}"),
|
|
80
|
+
severity="medium",
|
|
81
|
+
gap_type="architecture_drift",
|
|
82
|
+
task_id=task.id,
|
|
83
|
+
description=(
|
|
84
|
+
f"Change spans non-neighbor subsystems `{area_a}` and `{area_b}` "
|
|
85
|
+
f"but the plan did not declare it would touch {', '.join(f'`{a}`' for a in undeclared)}. "
|
|
86
|
+
"Cross-boundary edits between areas the repo map does not consider "
|
|
87
|
+
"adjacent are a common source of architecture drift."
|
|
88
|
+
),
|
|
89
|
+
evidence=evidence or [f"{area_a} ⇄ {area_b}"],
|
|
90
|
+
recommended_fix=(
|
|
91
|
+
"If this crossing is intended, declare planned files in both subsystems "
|
|
92
|
+
"(or split the work into per-subsystem tasks); otherwise keep the edit "
|
|
93
|
+
"within its subsystem and route through the neighboring interface. "
|
|
94
|
+
"Run `dev map` if the subsystem neighbors are out of date."
|
|
95
|
+
),
|
|
96
|
+
blocking=blocking,
|
|
97
|
+
))
|
|
98
|
+
if gaps:
|
|
99
|
+
logger.info(
|
|
100
|
+
"subsystem-boundary gate: %d undeclared crossing(s) for task %s (blocking=%s)",
|
|
101
|
+
len(gaps), task.id, blocking,
|
|
102
|
+
)
|
|
103
|
+
return gaps
|