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,676 @@
|
|
|
1
|
+
"""Read-only MCP tools for querying ``.devcouncil/repo_map.json``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from mcp.types import TextContent
|
|
10
|
+
|
|
11
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
12
|
+
from devcouncil.indexing.subsystem_map import (
|
|
13
|
+
area_for_path,
|
|
14
|
+
cross_boundary_pairs,
|
|
15
|
+
dead_symbol_candidates_of,
|
|
16
|
+
dependents_total_of,
|
|
17
|
+
impact_targets,
|
|
18
|
+
is_entry_root,
|
|
19
|
+
unreachable_of,
|
|
20
|
+
unwired_candidates_of,
|
|
21
|
+
)
|
|
22
|
+
from devcouncil.integrations.mcp.util import (
|
|
23
|
+
error_text,
|
|
24
|
+
json_text,
|
|
25
|
+
optional_bool_argument,
|
|
26
|
+
optional_string_argument,
|
|
27
|
+
optional_string_list_argument,
|
|
28
|
+
with_codeintel_freshness,
|
|
29
|
+
)
|
|
30
|
+
from devcouncil.utils.json_persist import read_json
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _load_repo_map(root: Path) -> dict[str, Any] | None:
|
|
34
|
+
map_path = root / ".devcouncil" / "repo_map.json"
|
|
35
|
+
if not map_path.exists():
|
|
36
|
+
return None
|
|
37
|
+
try:
|
|
38
|
+
data = read_json(map_path)
|
|
39
|
+
return data if isinstance(data, dict) else None
|
|
40
|
+
except Exception:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _map_stale(root: Path, data: dict[str, Any] | None) -> bool:
|
|
45
|
+
if not data:
|
|
46
|
+
return False
|
|
47
|
+
try:
|
|
48
|
+
return RepoMapper(root).map_is_stale(data)
|
|
49
|
+
except Exception:
|
|
50
|
+
# Fail closed: unverifiable map must not be treated as fresh.
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _graph_degraded_fields(root: Path) -> dict[str, Any]:
|
|
55
|
+
"""Surface lean-map handshake on legacy graph tool responses."""
|
|
56
|
+
data = _load_repo_map(root) or {}
|
|
57
|
+
degraded = bool(data.get("graph_degraded"))
|
|
58
|
+
fields: dict[str, Any] = {"graph_degraded": degraded}
|
|
59
|
+
if degraded:
|
|
60
|
+
fields["graph_degraded_reason"] = str(data.get("graph_degraded_reason") or "")
|
|
61
|
+
return fields
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _subsystem_summary(sub: dict[str, Any]) -> dict[str, Any]:
|
|
65
|
+
return {
|
|
66
|
+
"area": sub.get("area"),
|
|
67
|
+
"summary": sub.get("summary") or "",
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _subsystem_detail(sub: dict[str, Any]) -> dict[str, Any]:
|
|
72
|
+
role_files = sub.get("role_files") or {}
|
|
73
|
+
if not isinstance(role_files, dict):
|
|
74
|
+
role_files = {}
|
|
75
|
+
return {
|
|
76
|
+
"area": sub.get("area"),
|
|
77
|
+
"summary": sub.get("summary") or "",
|
|
78
|
+
"entry_points": list(sub.get("entry_points") or []),
|
|
79
|
+
"critical_files": list(sub.get("critical_files") or []),
|
|
80
|
+
"neighbors": list(sub.get("neighbors") or []),
|
|
81
|
+
"handoff_paths": list(sub.get("handoff_paths") or []),
|
|
82
|
+
"role_files": {str(k): list(v or []) for k, v in role_files.items()},
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _find_subsystem(data: dict[str, Any], area: str) -> dict[str, Any] | None:
|
|
87
|
+
for sub in data.get("subsystems") or []:
|
|
88
|
+
if isinstance(sub, dict) and str(sub.get("area") or "") == area:
|
|
89
|
+
return sub
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _path_from_dead_symbol(entry: str) -> str:
|
|
94
|
+
"""Extract the file path from a ``path:line name`` dead-symbol entry."""
|
|
95
|
+
loc = entry.split(" ", 1)[0]
|
|
96
|
+
path, _, _ = loc.rpartition(":")
|
|
97
|
+
return path.replace("\\", "/")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _matches_filters(
|
|
101
|
+
path: str,
|
|
102
|
+
data: dict[str, Any],
|
|
103
|
+
*,
|
|
104
|
+
area: str | None,
|
|
105
|
+
path_prefix: str | None,
|
|
106
|
+
) -> bool:
|
|
107
|
+
norm = path.replace("\\", "/")
|
|
108
|
+
if path_prefix:
|
|
109
|
+
prefix = path_prefix.replace("\\", "/").rstrip("/")
|
|
110
|
+
if not (norm == prefix or norm.startswith(prefix + "/")):
|
|
111
|
+
return False
|
|
112
|
+
if area:
|
|
113
|
+
return area_for_path(norm, data) == area
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _filter_paths(
|
|
118
|
+
paths: list[str],
|
|
119
|
+
data: dict[str, Any],
|
|
120
|
+
*,
|
|
121
|
+
area: str | None,
|
|
122
|
+
path_prefix: str | None,
|
|
123
|
+
) -> list[str]:
|
|
124
|
+
if not area and not path_prefix:
|
|
125
|
+
return paths
|
|
126
|
+
return [p for p in paths if _matches_filters(p, data, area=area, path_prefix=path_prefix)]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _filter_dead_symbols(
|
|
130
|
+
entries: list[str],
|
|
131
|
+
data: dict[str, Any],
|
|
132
|
+
*,
|
|
133
|
+
area: str | None,
|
|
134
|
+
path_prefix: str | None,
|
|
135
|
+
) -> list[str]:
|
|
136
|
+
if not area and not path_prefix:
|
|
137
|
+
return entries
|
|
138
|
+
return [
|
|
139
|
+
e
|
|
140
|
+
for e in entries
|
|
141
|
+
if _matches_filters(_path_from_dead_symbol(e), data, area=area, path_prefix=path_prefix)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
async def handle_repo_map(root: Path, arguments: dict) -> list[TextContent]:
|
|
146
|
+
async def _run() -> list[TextContent]:
|
|
147
|
+
subsystem = optional_string_argument(arguments, "subsystem")
|
|
148
|
+
path = optional_string_argument(arguments, "path")
|
|
149
|
+
for arg_name, value in [("subsystem", subsystem), ("path", path)]:
|
|
150
|
+
if value == "":
|
|
151
|
+
return error_text(
|
|
152
|
+
f"{arg_name} must be a string",
|
|
153
|
+
code="invalid_arguments",
|
|
154
|
+
argument=arg_name,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
data = _load_repo_map(root)
|
|
158
|
+
if data is None:
|
|
159
|
+
return error_text(
|
|
160
|
+
"No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
|
|
161
|
+
code="map_missing",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
stale = _map_stale(root, data)
|
|
165
|
+
resolved_area: str | None = None
|
|
166
|
+
if path:
|
|
167
|
+
resolved_area = area_for_path(path, data)
|
|
168
|
+
if not subsystem and resolved_area:
|
|
169
|
+
subsystem = resolved_area
|
|
170
|
+
|
|
171
|
+
if subsystem:
|
|
172
|
+
sub = _find_subsystem(data, subsystem)
|
|
173
|
+
if sub is None:
|
|
174
|
+
return json_text({
|
|
175
|
+
"ok": True,
|
|
176
|
+
"stale": stale,
|
|
177
|
+
"subsystem": None,
|
|
178
|
+
"path": path,
|
|
179
|
+
"area": resolved_area,
|
|
180
|
+
"error": f"Unknown subsystem area: {subsystem}",
|
|
181
|
+
"code": "unknown_subsystem",
|
|
182
|
+
})
|
|
183
|
+
return json_text({
|
|
184
|
+
"ok": True,
|
|
185
|
+
"stale": stale,
|
|
186
|
+
"path": path,
|
|
187
|
+
"area": resolved_area or subsystem,
|
|
188
|
+
"subsystem": _subsystem_detail(sub),
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
subsystems = [
|
|
192
|
+
_subsystem_summary(s)
|
|
193
|
+
for s in (data.get("subsystems") or [])
|
|
194
|
+
if isinstance(s, dict) and s.get("area")
|
|
195
|
+
]
|
|
196
|
+
return json_text({
|
|
197
|
+
"ok": True,
|
|
198
|
+
"stale": stale,
|
|
199
|
+
"languages": list(data.get("languages") or []),
|
|
200
|
+
"frameworks": list(data.get("frameworks") or []),
|
|
201
|
+
"package_managers": list(data.get("package_managers") or []),
|
|
202
|
+
"subsystems": subsystems,
|
|
203
|
+
"path": path,
|
|
204
|
+
"area": resolved_area,
|
|
205
|
+
"symbols": _symbols_for_path(root, path) if path else [],
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
return await with_codeintel_freshness(root, _run)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _symbols_for_path(root: Path, path: str) -> list[dict[str, Any]]:
|
|
212
|
+
"""Per-path symbol listings from the code graph (empty when unavailable)."""
|
|
213
|
+
try:
|
|
214
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
215
|
+
|
|
216
|
+
graph = load_code_graph(root)
|
|
217
|
+
if graph is None:
|
|
218
|
+
return []
|
|
219
|
+
norm = path.replace("\\", "/")
|
|
220
|
+
out = []
|
|
221
|
+
for n in graph.nodes:
|
|
222
|
+
if n.path != norm:
|
|
223
|
+
continue
|
|
224
|
+
kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
|
|
225
|
+
if kind == "file":
|
|
226
|
+
continue
|
|
227
|
+
out.append(
|
|
228
|
+
{
|
|
229
|
+
"id": n.id,
|
|
230
|
+
"kind": kind,
|
|
231
|
+
"name": n.name,
|
|
232
|
+
"line": n.line,
|
|
233
|
+
}
|
|
234
|
+
)
|
|
235
|
+
return out[:200]
|
|
236
|
+
except Exception:
|
|
237
|
+
return []
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
async def handle_impact(root: Path, arguments: dict) -> list[TextContent]:
|
|
241
|
+
async def _run() -> list[TextContent]:
|
|
242
|
+
paths, list_error = optional_string_list_argument(arguments, "paths")
|
|
243
|
+
if list_error:
|
|
244
|
+
return list_error
|
|
245
|
+
if not paths:
|
|
246
|
+
return error_text("Missing paths", code="missing_argument", argument="paths")
|
|
247
|
+
precise, precise_error = optional_bool_argument(arguments, "precise")
|
|
248
|
+
if precise_error:
|
|
249
|
+
return precise_error
|
|
250
|
+
precise = bool(precise)
|
|
251
|
+
|
|
252
|
+
data = _load_repo_map(root)
|
|
253
|
+
if data is None:
|
|
254
|
+
return error_text(
|
|
255
|
+
"No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
|
|
256
|
+
code="map_missing",
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
stale = _map_stale(root, data)
|
|
260
|
+
lsp_pool = None
|
|
261
|
+
if precise:
|
|
262
|
+
try:
|
|
263
|
+
from devcouncil.indexing.lsp_client import LspSessionPool
|
|
264
|
+
|
|
265
|
+
lsp_pool = LspSessionPool(root)
|
|
266
|
+
except Exception:
|
|
267
|
+
lsp_pool = None
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
items: list[dict[str, Any]] = []
|
|
271
|
+
all_neighbor_areas: set[str] = set()
|
|
272
|
+
for raw in paths:
|
|
273
|
+
path = raw.replace("\\", "/")
|
|
274
|
+
dependents, neighbors = impact_targets(path, data)
|
|
275
|
+
resolution = "import"
|
|
276
|
+
if precise and lsp_pool is not None:
|
|
277
|
+
try:
|
|
278
|
+
lsp_deps = lsp_pool.dependents_of_file(path)
|
|
279
|
+
if lsp_deps is not None:
|
|
280
|
+
dependents = lsp_deps
|
|
281
|
+
resolution = "lsp"
|
|
282
|
+
except Exception:
|
|
283
|
+
pass
|
|
284
|
+
area = area_for_path(path, data)
|
|
285
|
+
all_neighbor_areas.update(neighbors)
|
|
286
|
+
item: dict[str, Any] = {
|
|
287
|
+
"path": path,
|
|
288
|
+
"area": area,
|
|
289
|
+
"is_entry_root": is_entry_root(path, data),
|
|
290
|
+
"dependents": dependents,
|
|
291
|
+
"neighbors": neighbors,
|
|
292
|
+
}
|
|
293
|
+
dep_total = dependents_total_of(path, data)
|
|
294
|
+
if dep_total is not None and dep_total > len(dependents):
|
|
295
|
+
item["dependents_total"] = dep_total
|
|
296
|
+
if precise:
|
|
297
|
+
item["resolution"] = resolution
|
|
298
|
+
items.append(item)
|
|
299
|
+
|
|
300
|
+
crossings = [
|
|
301
|
+
{"areas": [a, b]}
|
|
302
|
+
for a, b in cross_boundary_pairs(paths, data)
|
|
303
|
+
]
|
|
304
|
+
payload: dict[str, Any] = {
|
|
305
|
+
"ok": True,
|
|
306
|
+
"stale": stale,
|
|
307
|
+
"paths": items,
|
|
308
|
+
"neighbor_areas": sorted(all_neighbor_areas),
|
|
309
|
+
"cross_boundary_pairs": crossings,
|
|
310
|
+
}
|
|
311
|
+
if precise:
|
|
312
|
+
payload["precise"] = True
|
|
313
|
+
return json_text(payload)
|
|
314
|
+
finally:
|
|
315
|
+
if lsp_pool is not None:
|
|
316
|
+
lsp_pool.close()
|
|
317
|
+
|
|
318
|
+
return await with_codeintel_freshness(root, _run)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
async def handle_liveness(root: Path, arguments: dict) -> list[TextContent]:
|
|
322
|
+
async def _run() -> list[TextContent]:
|
|
323
|
+
area = optional_string_argument(arguments, "area")
|
|
324
|
+
path_prefix = optional_string_argument(arguments, "path_prefix")
|
|
325
|
+
min_confidence = optional_string_argument(arguments, "min_confidence") or "inferred"
|
|
326
|
+
for arg_name, value in [
|
|
327
|
+
("area", area),
|
|
328
|
+
("path_prefix", path_prefix),
|
|
329
|
+
("min_confidence", min_confidence),
|
|
330
|
+
]:
|
|
331
|
+
if value == "":
|
|
332
|
+
return error_text(
|
|
333
|
+
f"{arg_name} must be a string",
|
|
334
|
+
code="invalid_arguments",
|
|
335
|
+
argument=arg_name,
|
|
336
|
+
)
|
|
337
|
+
if min_confidence not in {"extracted", "inferred", "ambiguous"}:
|
|
338
|
+
return error_text(
|
|
339
|
+
"min_confidence must be extracted|inferred|ambiguous",
|
|
340
|
+
code="invalid_arguments",
|
|
341
|
+
argument="min_confidence",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
data = _load_repo_map(root)
|
|
345
|
+
if data is None:
|
|
346
|
+
return error_text(
|
|
347
|
+
"No repo map found. Run `dev map` to generate .devcouncil/repo_map.json.",
|
|
348
|
+
code="map_missing",
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
stale = _map_stale(root, data)
|
|
352
|
+
raw_roots = data.get("entry_roots") or []
|
|
353
|
+
if not isinstance(raw_roots, list):
|
|
354
|
+
raw_roots = []
|
|
355
|
+
global_entry_roots = [str(p) for p in raw_roots]
|
|
356
|
+
|
|
357
|
+
unwired = _filter_paths(
|
|
358
|
+
unwired_candidates_of(data), data, area=area, path_prefix=path_prefix
|
|
359
|
+
)
|
|
360
|
+
unreachable = _filter_paths(
|
|
361
|
+
unreachable_of(data), data, area=area, path_prefix=path_prefix
|
|
362
|
+
)
|
|
363
|
+
dead_symbols = _filter_dead_symbols(
|
|
364
|
+
dead_symbol_candidates_of(data), data, area=area, path_prefix=path_prefix
|
|
365
|
+
)
|
|
366
|
+
entry_roots = global_entry_roots
|
|
367
|
+
if area or path_prefix:
|
|
368
|
+
entry_roots = _filter_paths(entry_roots, data, area=area, path_prefix=path_prefix)
|
|
369
|
+
|
|
370
|
+
dead_code, hidden = _structured_dead_code(
|
|
371
|
+
root,
|
|
372
|
+
area=area,
|
|
373
|
+
path_prefix=path_prefix,
|
|
374
|
+
min_confidence=min_confidence,
|
|
375
|
+
)
|
|
376
|
+
# Reliability is a GLOBAL property of the map: an area/path_prefix scope that
|
|
377
|
+
# merely contains no entry root (normal for library subsystems) must not
|
|
378
|
+
# flip the response to unreliable and drop its unreachable list.
|
|
379
|
+
unreachable_unreliable = (
|
|
380
|
+
data.get("liveness_unreachable_unreliable") is True
|
|
381
|
+
or not global_entry_roots
|
|
382
|
+
)
|
|
383
|
+
warn = None
|
|
384
|
+
if unreachable_unreliable:
|
|
385
|
+
unreachable = []
|
|
386
|
+
warn = (
|
|
387
|
+
"entry_roots empty or liveness_unreachable_unreliable: "
|
|
388
|
+
"unreachable_files omitted; ignore mass inferred dead"
|
|
389
|
+
)
|
|
390
|
+
if data.get("graph_degraded"):
|
|
391
|
+
warn = (
|
|
392
|
+
(warn + "; " if warn else "")
|
|
393
|
+
+ "graph_degraded: lean map — treat liveness/dead tiers as unreliable"
|
|
394
|
+
)
|
|
395
|
+
payload: dict[str, object] = {
|
|
396
|
+
"ok": True,
|
|
397
|
+
"stale": stale,
|
|
398
|
+
"graph_degraded": bool(data.get("graph_degraded")),
|
|
399
|
+
"area": area,
|
|
400
|
+
"path_prefix": path_prefix,
|
|
401
|
+
"min_confidence": min_confidence,
|
|
402
|
+
"entry_roots": entry_roots,
|
|
403
|
+
"unwired_candidates": unwired,
|
|
404
|
+
"unreachable_files": unreachable,
|
|
405
|
+
"dead_symbol_candidates": dead_symbols,
|
|
406
|
+
"dead_code": dead_code,
|
|
407
|
+
"dead_code_hidden": hidden,
|
|
408
|
+
"unreachable_unreliable": unreachable_unreliable
|
|
409
|
+
or bool(data.get("graph_degraded")),
|
|
410
|
+
}
|
|
411
|
+
if warn:
|
|
412
|
+
payload["warning"] = warn
|
|
413
|
+
return json_text(payload)
|
|
414
|
+
|
|
415
|
+
return await with_codeintel_freshness(root, _run)
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def _structured_dead_code(
|
|
419
|
+
root: Path,
|
|
420
|
+
*,
|
|
421
|
+
area: str | None,
|
|
422
|
+
path_prefix: str | None,
|
|
423
|
+
min_confidence: str = "inferred",
|
|
424
|
+
) -> tuple[list[dict[str, Any]], int]:
|
|
425
|
+
try:
|
|
426
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
427
|
+
from devcouncil.indexing.graph.liveness import confidence_at_least
|
|
428
|
+
|
|
429
|
+
graph = load_code_graph(root)
|
|
430
|
+
if graph is None:
|
|
431
|
+
return [], 0
|
|
432
|
+
data = _load_repo_map(root) or {}
|
|
433
|
+
matched: list[dict[str, Any]] = []
|
|
434
|
+
hidden = 0
|
|
435
|
+
for d in graph.dead_code:
|
|
436
|
+
if not _matches_filters(d.path, data, area=area, path_prefix=path_prefix):
|
|
437
|
+
continue
|
|
438
|
+
if not confidence_at_least(d.confidence, min_confidence):
|
|
439
|
+
hidden += 1
|
|
440
|
+
continue
|
|
441
|
+
matched.append(d.model_dump())
|
|
442
|
+
return matched[:200], hidden
|
|
443
|
+
except Exception:
|
|
444
|
+
return [], 0
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
async def handle_graph_ingest(root: Path, arguments: dict) -> list[TextContent]:
|
|
448
|
+
paths, _ = optional_string_list_argument(arguments, "paths")
|
|
449
|
+
from devcouncil.indexing.map_artifacts import refresh_map_artifacts
|
|
450
|
+
from devcouncil.codeintel import get_codeintel_service
|
|
451
|
+
from devcouncil.codeintel.build_control import GraphBuildBusy
|
|
452
|
+
from devcouncil.codeintel.sync import get_sync_coordinator
|
|
453
|
+
from devcouncil.indexing.graph.embeddings import build_embeddings
|
|
454
|
+
|
|
455
|
+
coordinator = get_sync_coordinator(root)
|
|
456
|
+
changed = list(paths or [])
|
|
457
|
+
map_path = root / ".devcouncil" / "repo_map.json"
|
|
458
|
+
if paths is None:
|
|
459
|
+
try:
|
|
460
|
+
refresh = await asyncio.to_thread(
|
|
461
|
+
refresh_map_artifacts,
|
|
462
|
+
root,
|
|
463
|
+
map_path,
|
|
464
|
+
quiet=True,
|
|
465
|
+
)
|
|
466
|
+
except GraphBuildBusy as exc:
|
|
467
|
+
return error_text(str(exc), code="graph_writer_busy")
|
|
468
|
+
else:
|
|
469
|
+
try:
|
|
470
|
+
ok = await asyncio.to_thread(coordinator.sync_now, changed)
|
|
471
|
+
if not ok:
|
|
472
|
+
state = coordinator.status().as_dict()
|
|
473
|
+
return error_text(
|
|
474
|
+
str(state.get("last_error") or state.get("degraded_reason") or "sync failed"),
|
|
475
|
+
code="codeintel_sync_failed",
|
|
476
|
+
**state,
|
|
477
|
+
)
|
|
478
|
+
refresh = await asyncio.to_thread(
|
|
479
|
+
refresh_map_artifacts,
|
|
480
|
+
root,
|
|
481
|
+
map_path,
|
|
482
|
+
quiet=True,
|
|
483
|
+
graph=get_codeintel_service(root).load(),
|
|
484
|
+
paths=changed,
|
|
485
|
+
)
|
|
486
|
+
except GraphBuildBusy as exc:
|
|
487
|
+
# Same structured error as the full-refresh branch — a held writer
|
|
488
|
+
# lease must not surface as an unhandled MCP exception.
|
|
489
|
+
return error_text(str(exc), code="graph_writer_busy")
|
|
490
|
+
embedded = await asyncio.to_thread(build_embeddings, root)
|
|
491
|
+
return json_text({
|
|
492
|
+
"ok": not refresh.degraded,
|
|
493
|
+
"paths": changed,
|
|
494
|
+
"embeddings_built": embedded,
|
|
495
|
+
"generation": refresh.generation,
|
|
496
|
+
"mode": refresh.mode,
|
|
497
|
+
"degraded": refresh.degraded,
|
|
498
|
+
"reason": refresh.reason,
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
async def handle_graph_cypher(root: Path, arguments: dict) -> list[TextContent]:
|
|
503
|
+
async def _run() -> list[TextContent]:
|
|
504
|
+
query = optional_string_argument(arguments, "query")
|
|
505
|
+
if not query:
|
|
506
|
+
return error_text("Missing query", code="missing_argument", argument="query")
|
|
507
|
+
from devcouncil.indexing.graph.cypher import run_cypher
|
|
508
|
+
|
|
509
|
+
return json_text(run_cypher(root, query))
|
|
510
|
+
|
|
511
|
+
return await with_codeintel_freshness(root, _run)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
async def handle_pdg_query(root: Path, arguments: dict) -> list[TextContent]:
|
|
515
|
+
async def _run() -> list[TextContent]:
|
|
516
|
+
mode = optional_string_argument(arguments, "mode")
|
|
517
|
+
target = optional_string_argument(arguments, "target")
|
|
518
|
+
if not mode:
|
|
519
|
+
return error_text("Missing mode", code="missing_argument", argument="mode")
|
|
520
|
+
if not target:
|
|
521
|
+
return error_text("Missing target", code="missing_argument", argument="target")
|
|
522
|
+
variable = optional_string_argument(arguments, "variable")
|
|
523
|
+
from devcouncil.indexing.graph.query import query_pdg_controls, query_pdg_flows
|
|
524
|
+
|
|
525
|
+
if mode == "controls":
|
|
526
|
+
return json_text(query_pdg_controls(root, target))
|
|
527
|
+
if mode == "flows":
|
|
528
|
+
return json_text(query_pdg_flows(root, target, variable=variable or None))
|
|
529
|
+
return error_text(
|
|
530
|
+
"mode must be controls or flows",
|
|
531
|
+
code="invalid_arguments",
|
|
532
|
+
argument="mode",
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
return await with_codeintel_freshness(root, _run)
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
async def handle_explain(root: Path, arguments: dict) -> list[TextContent]:
|
|
539
|
+
async def _run() -> list[TextContent]:
|
|
540
|
+
path = optional_string_argument(arguments, "path")
|
|
541
|
+
category = optional_string_argument(arguments, "category")
|
|
542
|
+
from devcouncil.indexing.graph.query import explain_pdg_taint
|
|
543
|
+
|
|
544
|
+
return json_text(explain_pdg_taint(root, path=path or None, category=category or None))
|
|
545
|
+
|
|
546
|
+
return await with_codeintel_freshness(root, _run)
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
async def handle_graph_query(root: Path, arguments: dict) -> list[TextContent]:
|
|
550
|
+
async def _run() -> list[TextContent]:
|
|
551
|
+
name = optional_string_argument(arguments, "name_or_path")
|
|
552
|
+
if not name:
|
|
553
|
+
return error_text(
|
|
554
|
+
"Missing name_or_path", code="missing_argument", argument="name_or_path"
|
|
555
|
+
)
|
|
556
|
+
from devcouncil.indexing.graph import query_symbol
|
|
557
|
+
|
|
558
|
+
return json_text({"ok": True, **query_symbol(root, name), **_graph_degraded_fields(root)})
|
|
559
|
+
|
|
560
|
+
return await with_codeintel_freshness(root, _run)
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
async def handle_graph_trace(root: Path, arguments: dict) -> list[TextContent]:
|
|
564
|
+
async def _run() -> list[TextContent]:
|
|
565
|
+
start = optional_string_argument(arguments, "from")
|
|
566
|
+
end = optional_string_argument(arguments, "to")
|
|
567
|
+
if not start:
|
|
568
|
+
return error_text("Missing from", code="missing_argument", argument="from")
|
|
569
|
+
if not end:
|
|
570
|
+
return error_text("Missing to", code="missing_argument", argument="to")
|
|
571
|
+
from devcouncil.indexing.graph import trace_path
|
|
572
|
+
|
|
573
|
+
return json_text({"ok": True, **trace_path(root, start, end), **_graph_degraded_fields(root)})
|
|
574
|
+
|
|
575
|
+
return await with_codeintel_freshness(root, _run)
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
async def handle_graph_impact(root: Path, arguments: dict) -> list[TextContent]:
|
|
579
|
+
"""Symbol-level blast radius from paths or working-tree diff (code graph)."""
|
|
580
|
+
|
|
581
|
+
async def _run() -> list[TextContent]:
|
|
582
|
+
paths, list_error = optional_string_list_argument(arguments, "paths")
|
|
583
|
+
if list_error:
|
|
584
|
+
return list_error
|
|
585
|
+
use_diff, diff_error = optional_bool_argument(arguments, "diff")
|
|
586
|
+
if diff_error:
|
|
587
|
+
return diff_error
|
|
588
|
+
use_diff = bool(use_diff)
|
|
589
|
+
if not use_diff and not paths:
|
|
590
|
+
return error_text(
|
|
591
|
+
"Provide paths or set diff=true",
|
|
592
|
+
code="missing_argument",
|
|
593
|
+
argument="paths",
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
597
|
+
from devcouncil.indexing.graph.intel import diff_impact
|
|
598
|
+
|
|
599
|
+
graph = load_code_graph(root)
|
|
600
|
+
if graph is None:
|
|
601
|
+
return error_text(
|
|
602
|
+
"No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
|
|
603
|
+
code="graph_missing",
|
|
604
|
+
)
|
|
605
|
+
result = diff_impact(
|
|
606
|
+
root,
|
|
607
|
+
graph,
|
|
608
|
+
paths=paths,
|
|
609
|
+
use_diff=use_diff,
|
|
610
|
+
max_depth=3,
|
|
611
|
+
)
|
|
612
|
+
return json_text({"ok": True, **result, **_graph_degraded_fields(root)})
|
|
613
|
+
|
|
614
|
+
return await with_codeintel_freshness(root, _run)
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
async def handle_route_map(root: Path, arguments: dict) -> list[TextContent]:
|
|
618
|
+
async def _run() -> list[TextContent]:
|
|
619
|
+
from devcouncil.indexing.graph.api_routes import route_map
|
|
620
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
621
|
+
|
|
622
|
+
graph = load_code_graph(root)
|
|
623
|
+
if graph is None:
|
|
624
|
+
return error_text(
|
|
625
|
+
"No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
|
|
626
|
+
code="graph_missing",
|
|
627
|
+
)
|
|
628
|
+
return json_text({"ok": True, **route_map(root, graph), **_graph_degraded_fields(root)})
|
|
629
|
+
|
|
630
|
+
return await with_codeintel_freshness(root, _run)
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
async def handle_shape_check(root: Path, arguments: dict) -> list[TextContent]:
|
|
634
|
+
async def _run() -> list[TextContent]:
|
|
635
|
+
route = optional_string_argument(arguments, "route")
|
|
636
|
+
if route == "":
|
|
637
|
+
return error_text("route must be a string", code="invalid_arguments", argument="route")
|
|
638
|
+
from devcouncil.indexing.graph.api_routes import shape_check
|
|
639
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
640
|
+
|
|
641
|
+
graph = load_code_graph(root)
|
|
642
|
+
if graph is None:
|
|
643
|
+
return error_text(
|
|
644
|
+
"No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
|
|
645
|
+
code="graph_missing",
|
|
646
|
+
)
|
|
647
|
+
return json_text({
|
|
648
|
+
"ok": True,
|
|
649
|
+
**shape_check(root, graph, route_filter=route),
|
|
650
|
+
**_graph_degraded_fields(root),
|
|
651
|
+
})
|
|
652
|
+
|
|
653
|
+
return await with_codeintel_freshness(root, _run)
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
async def handle_api_impact(root: Path, arguments: dict) -> list[TextContent]:
|
|
657
|
+
async def _run() -> list[TextContent]:
|
|
658
|
+
route_or_path = optional_string_argument(arguments, "route_or_path")
|
|
659
|
+
if not route_or_path:
|
|
660
|
+
return error_text(
|
|
661
|
+
"Missing route_or_path",
|
|
662
|
+
code="missing_argument",
|
|
663
|
+
argument="route_or_path",
|
|
664
|
+
)
|
|
665
|
+
from devcouncil.indexing.graph.api_routes import api_impact
|
|
666
|
+
from devcouncil.indexing.graph.build import load_code_graph
|
|
667
|
+
|
|
668
|
+
graph = load_code_graph(root)
|
|
669
|
+
if graph is None:
|
|
670
|
+
return error_text(
|
|
671
|
+
"No code graph found. Run `dev map` to generate .devcouncil/graph/code_graph.json.",
|
|
672
|
+
code="graph_missing",
|
|
673
|
+
)
|
|
674
|
+
return json_text({"ok": True, **api_impact(root, route_or_path, graph), **_graph_degraded_fields(root)})
|
|
675
|
+
|
|
676
|
+
return await with_codeintel_freshness(root, _run)
|