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,577 @@
|
|
|
1
|
+
"""Minimal read-only LSP client (stdio JSON-RPC).
|
|
2
|
+
|
|
3
|
+
Spawns a detected language server from :class:`~devcouncil.indexing.lsp.LspInspector`
|
|
4
|
+
and speaks only ``initialize``, ``textDocument/references``, and
|
|
5
|
+
``textDocument/definition``. Off by default — enable via ``indexing.lsp_refs``
|
|
6
|
+
config or ``dev map --lsp-refs``. Hard timeouts and clean shutdown; never raises
|
|
7
|
+
to callers (methods return ``None`` / empty on failure).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import threading
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Callable, Iterator
|
|
20
|
+
from urllib.parse import unquote, urlparse
|
|
21
|
+
from urllib.request import url2pathname
|
|
22
|
+
|
|
23
|
+
from devcouncil.indexing.lsp import LspInspector
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# Default per-request / session budgets. Language servers can be slow to start;
|
|
28
|
+
# keep these tight so map/verify never hang when a server is wedged.
|
|
29
|
+
_DEFAULT_REQUEST_TIMEOUT = 12.0
|
|
30
|
+
_DEFAULT_SHUTDOWN_TIMEOUT = 3.0
|
|
31
|
+
_DEFAULT_INIT_TIMEOUT = 20.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class LspLocation:
|
|
36
|
+
"""A single LSP location (repo-relative path + 1-based line)."""
|
|
37
|
+
|
|
38
|
+
path: str
|
|
39
|
+
line: int # 1-based
|
|
40
|
+
character: int # 0-based
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _uri_to_rel(project_root: Path, uri: str) -> str | None:
|
|
44
|
+
"""Convert a ``file://`` URI to a repo-relative POSIX path, or ``None``."""
|
|
45
|
+
try:
|
|
46
|
+
parsed = urlparse(uri)
|
|
47
|
+
if parsed.scheme != "file":
|
|
48
|
+
return None
|
|
49
|
+
raw = url2pathname(unquote(parsed.path))
|
|
50
|
+
# On Windows url2pathname may yield ``/C:/...``; Path handles it.
|
|
51
|
+
abs_path = Path(raw)
|
|
52
|
+
if os.name == "nt" and raw.startswith("/") and len(raw) > 2 and raw[2] == ":":
|
|
53
|
+
abs_path = Path(raw[1:])
|
|
54
|
+
rel = abs_path.resolve().relative_to(project_root.resolve())
|
|
55
|
+
return rel.as_posix()
|
|
56
|
+
except (OSError, ValueError):
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _path_to_uri(project_root: Path, rel: str) -> str:
|
|
61
|
+
return (project_root / rel).resolve().as_uri()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _symbol_position(source: str, line: int, name: str) -> tuple[int, int] | None:
|
|
65
|
+
"""Return 0-based ``(line, character)`` for ``name`` on 1-based ``line``."""
|
|
66
|
+
lines = source.splitlines()
|
|
67
|
+
if line < 1 or line > len(lines):
|
|
68
|
+
return None
|
|
69
|
+
text = lines[line - 1]
|
|
70
|
+
col = text.find(name)
|
|
71
|
+
if col < 0:
|
|
72
|
+
# Fall back to first identifier-ish occurrence after ``def``/``class``/``export``.
|
|
73
|
+
col = text.find(name.split(".")[-1]) if "." in name else -1
|
|
74
|
+
if col < 0:
|
|
75
|
+
return None
|
|
76
|
+
return line - 1, col
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _parse_locations(project_root: Path, result: Any) -> list[LspLocation]:
|
|
80
|
+
"""Normalize ``Location | Location[] | LocationLink[] | null`` to locations."""
|
|
81
|
+
if result is None:
|
|
82
|
+
return []
|
|
83
|
+
items = result if isinstance(result, list) else [result]
|
|
84
|
+
out: list[LspLocation] = []
|
|
85
|
+
for item in items:
|
|
86
|
+
if not isinstance(item, dict):
|
|
87
|
+
continue
|
|
88
|
+
uri = item.get("uri") or (item.get("targetUri") if "targetUri" in item else None)
|
|
89
|
+
rng = item.get("range") or item.get("targetSelectionRange") or item.get("targetRange")
|
|
90
|
+
if not uri or not isinstance(rng, dict):
|
|
91
|
+
continue
|
|
92
|
+
start = rng.get("start") or {}
|
|
93
|
+
if not isinstance(start, dict):
|
|
94
|
+
continue
|
|
95
|
+
rel = _uri_to_rel(project_root, str(uri))
|
|
96
|
+
if rel is None:
|
|
97
|
+
continue
|
|
98
|
+
line = int(start.get("line", 0)) + 1
|
|
99
|
+
character = int(start.get("character", 0))
|
|
100
|
+
out.append(LspLocation(path=rel, line=line, character=character))
|
|
101
|
+
return out
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def language_for_path(path: str) -> str | None:
|
|
105
|
+
return LspInspector._EXTENSIONS.get(Path(path).suffix.lower())
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def first_available_command(language: str) -> list[str] | None:
|
|
109
|
+
"""First PATH-available command for ``language``, or ``None``."""
|
|
110
|
+
for command in LspInspector._LANGUAGE_SERVERS.get(language, []):
|
|
111
|
+
from shutil import which
|
|
112
|
+
|
|
113
|
+
if which(command[0]) is not None:
|
|
114
|
+
return list(command)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class LspClient:
|
|
119
|
+
"""One stdio language-server session (read-only: initialize / refs / definition)."""
|
|
120
|
+
|
|
121
|
+
def __init__(
|
|
122
|
+
self,
|
|
123
|
+
project_root: Path,
|
|
124
|
+
language: str,
|
|
125
|
+
command: list[str],
|
|
126
|
+
*,
|
|
127
|
+
request_timeout: float = _DEFAULT_REQUEST_TIMEOUT,
|
|
128
|
+
init_timeout: float = _DEFAULT_INIT_TIMEOUT,
|
|
129
|
+
) -> None:
|
|
130
|
+
self.project_root = project_root.resolve()
|
|
131
|
+
self.language = language
|
|
132
|
+
self.command = list(command)
|
|
133
|
+
self.request_timeout = request_timeout
|
|
134
|
+
self.init_timeout = init_timeout
|
|
135
|
+
self._proc: subprocess.Popen[bytes] | None = None
|
|
136
|
+
self._reader: threading.Thread | None = None
|
|
137
|
+
self._lock = threading.Lock()
|
|
138
|
+
self._next_id = 1
|
|
139
|
+
self._pending: dict[int, dict[str, Any]] = {}
|
|
140
|
+
self._events: dict[int, threading.Event] = {}
|
|
141
|
+
self._alive = False
|
|
142
|
+
self._opened: set[str] = set()
|
|
143
|
+
|
|
144
|
+
def start(self) -> bool:
|
|
145
|
+
"""Spawn the server and complete ``initialize``. Returns False on failure."""
|
|
146
|
+
if self._alive:
|
|
147
|
+
return True
|
|
148
|
+
try:
|
|
149
|
+
self._proc = subprocess.Popen(
|
|
150
|
+
self.command,
|
|
151
|
+
stdin=subprocess.PIPE,
|
|
152
|
+
stdout=subprocess.PIPE,
|
|
153
|
+
stderr=subprocess.DEVNULL,
|
|
154
|
+
cwd=str(self.project_root),
|
|
155
|
+
bufsize=0,
|
|
156
|
+
)
|
|
157
|
+
except OSError:
|
|
158
|
+
logger.debug("Failed to spawn LSP %s", self.command, exc_info=True)
|
|
159
|
+
self._proc = None
|
|
160
|
+
return False
|
|
161
|
+
self._reader = threading.Thread(target=self._read_loop, name=f"lsp-{self.language}", daemon=True)
|
|
162
|
+
self._reader.start()
|
|
163
|
+
try:
|
|
164
|
+
init_result = self._request(
|
|
165
|
+
"initialize",
|
|
166
|
+
{
|
|
167
|
+
"processId": os.getpid(),
|
|
168
|
+
"rootUri": self.project_root.as_uri(),
|
|
169
|
+
"rootPath": str(self.project_root),
|
|
170
|
+
"capabilities": {
|
|
171
|
+
"textDocument": {
|
|
172
|
+
"definition": {"linkSupport": True},
|
|
173
|
+
"references": {},
|
|
174
|
+
},
|
|
175
|
+
"workspace": {},
|
|
176
|
+
},
|
|
177
|
+
"initializationOptions": {},
|
|
178
|
+
"trace": "off",
|
|
179
|
+
},
|
|
180
|
+
timeout=self.init_timeout,
|
|
181
|
+
)
|
|
182
|
+
if init_result is None and self._proc is not None and self._proc.poll() is not None:
|
|
183
|
+
self.shutdown()
|
|
184
|
+
return False
|
|
185
|
+
self._notify("initialized", {})
|
|
186
|
+
self._alive = True
|
|
187
|
+
return True
|
|
188
|
+
except Exception:
|
|
189
|
+
logger.debug("LSP initialize failed for %s", self.language, exc_info=True)
|
|
190
|
+
self.shutdown()
|
|
191
|
+
return False
|
|
192
|
+
|
|
193
|
+
def shutdown(self) -> None:
|
|
194
|
+
"""Best-effort ``shutdown`` / ``exit`` then terminate the process."""
|
|
195
|
+
proc = self._proc
|
|
196
|
+
self._alive = False
|
|
197
|
+
if proc is None:
|
|
198
|
+
return
|
|
199
|
+
try:
|
|
200
|
+
if proc.poll() is None:
|
|
201
|
+
try:
|
|
202
|
+
self._request("shutdown", None, timeout=_DEFAULT_SHUTDOWN_TIMEOUT)
|
|
203
|
+
except Exception:
|
|
204
|
+
pass
|
|
205
|
+
try:
|
|
206
|
+
self._notify("exit", None)
|
|
207
|
+
except Exception:
|
|
208
|
+
pass
|
|
209
|
+
try:
|
|
210
|
+
proc.wait(timeout=_DEFAULT_SHUTDOWN_TIMEOUT)
|
|
211
|
+
except subprocess.TimeoutExpired:
|
|
212
|
+
proc.kill()
|
|
213
|
+
try:
|
|
214
|
+
proc.wait(timeout=1.0)
|
|
215
|
+
except subprocess.TimeoutExpired:
|
|
216
|
+
pass
|
|
217
|
+
finally:
|
|
218
|
+
self._proc = None
|
|
219
|
+
self._opened.clear()
|
|
220
|
+
with self._lock:
|
|
221
|
+
for ev in self._events.values():
|
|
222
|
+
ev.set()
|
|
223
|
+
self._pending.clear()
|
|
224
|
+
self._events.clear()
|
|
225
|
+
|
|
226
|
+
def __enter__(self) -> LspClient:
|
|
227
|
+
if not self.start():
|
|
228
|
+
raise RuntimeError(f"Failed to start LSP for {self.language}")
|
|
229
|
+
return self
|
|
230
|
+
|
|
231
|
+
def __exit__(self, *exc: object) -> None:
|
|
232
|
+
self.shutdown()
|
|
233
|
+
|
|
234
|
+
def references(
|
|
235
|
+
self,
|
|
236
|
+
rel_path: str,
|
|
237
|
+
line: int,
|
|
238
|
+
character: int,
|
|
239
|
+
*,
|
|
240
|
+
include_declaration: bool = False,
|
|
241
|
+
) -> list[LspLocation] | None:
|
|
242
|
+
"""``textDocument/references`` at 0-based position. ``None`` on failure."""
|
|
243
|
+
if not self._ensure_open(rel_path):
|
|
244
|
+
return None
|
|
245
|
+
result = self._request(
|
|
246
|
+
"textDocument/references",
|
|
247
|
+
{
|
|
248
|
+
"textDocument": {"uri": _path_to_uri(self.project_root, rel_path)},
|
|
249
|
+
"position": {"line": line, "character": character},
|
|
250
|
+
"context": {"includeDeclaration": include_declaration},
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
if result is None and not self._alive:
|
|
254
|
+
return None
|
|
255
|
+
return _parse_locations(self.project_root, result)
|
|
256
|
+
|
|
257
|
+
def _ensure_open(self, rel_path: str) -> bool:
|
|
258
|
+
if not self._alive:
|
|
259
|
+
return False
|
|
260
|
+
norm = rel_path.replace("\\", "/")
|
|
261
|
+
if norm in self._opened:
|
|
262
|
+
return True
|
|
263
|
+
abs_path = self.project_root / norm
|
|
264
|
+
try:
|
|
265
|
+
text = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
266
|
+
except OSError:
|
|
267
|
+
return False
|
|
268
|
+
lang_id = {
|
|
269
|
+
"python": "python",
|
|
270
|
+
"typescript": "typescript",
|
|
271
|
+
"javascript": "javascript",
|
|
272
|
+
"go": "go",
|
|
273
|
+
"rust": "rust",
|
|
274
|
+
}.get(self.language, self.language)
|
|
275
|
+
self._notify(
|
|
276
|
+
"textDocument/didOpen",
|
|
277
|
+
{
|
|
278
|
+
"textDocument": {
|
|
279
|
+
"uri": _path_to_uri(self.project_root, norm),
|
|
280
|
+
"languageId": lang_id,
|
|
281
|
+
"version": 1,
|
|
282
|
+
"text": text,
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
)
|
|
286
|
+
self._opened.add(norm)
|
|
287
|
+
return True
|
|
288
|
+
|
|
289
|
+
def _read_loop(self) -> None:
|
|
290
|
+
proc = self._proc
|
|
291
|
+
if proc is None or proc.stdout is None:
|
|
292
|
+
return
|
|
293
|
+
stdout = proc.stdout
|
|
294
|
+
try:
|
|
295
|
+
while True:
|
|
296
|
+
headers: dict[str, str] = {}
|
|
297
|
+
while True:
|
|
298
|
+
line = stdout.readline()
|
|
299
|
+
if not line:
|
|
300
|
+
return
|
|
301
|
+
if line in (b"\r\n", b"\n"):
|
|
302
|
+
break
|
|
303
|
+
try:
|
|
304
|
+
key, _, value = line.decode("utf-8", errors="replace").partition(":")
|
|
305
|
+
headers[key.strip().lower()] = value.strip()
|
|
306
|
+
except Exception:
|
|
307
|
+
continue
|
|
308
|
+
length_s = headers.get("content-length")
|
|
309
|
+
if not length_s:
|
|
310
|
+
continue
|
|
311
|
+
try:
|
|
312
|
+
length = int(length_s)
|
|
313
|
+
except ValueError:
|
|
314
|
+
continue
|
|
315
|
+
body = b""
|
|
316
|
+
while len(body) < length:
|
|
317
|
+
chunk = stdout.read(length - len(body))
|
|
318
|
+
if not chunk:
|
|
319
|
+
return
|
|
320
|
+
body += chunk
|
|
321
|
+
try:
|
|
322
|
+
message = json.loads(body.decode("utf-8"))
|
|
323
|
+
except json.JSONDecodeError:
|
|
324
|
+
continue
|
|
325
|
+
if not isinstance(message, dict):
|
|
326
|
+
continue
|
|
327
|
+
msg_id = message.get("id")
|
|
328
|
+
if msg_id is not None and ("result" in message or "error" in message):
|
|
329
|
+
with self._lock:
|
|
330
|
+
self._pending[int(msg_id)] = message
|
|
331
|
+
ev = self._events.get(int(msg_id))
|
|
332
|
+
if ev is not None:
|
|
333
|
+
ev.set()
|
|
334
|
+
# Notifications / server requests are ignored (read-only client).
|
|
335
|
+
except Exception:
|
|
336
|
+
logger.debug("LSP read loop ended for %s", self.language, exc_info=True)
|
|
337
|
+
|
|
338
|
+
def _write(self, payload: dict[str, Any]) -> None:
|
|
339
|
+
proc = self._proc
|
|
340
|
+
if proc is None or proc.stdin is None or proc.poll() is not None:
|
|
341
|
+
raise RuntimeError("LSP process not running")
|
|
342
|
+
raw = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
343
|
+
header = f"Content-Length: {len(raw)}\r\n\r\n".encode("ascii")
|
|
344
|
+
proc.stdin.write(header + raw)
|
|
345
|
+
proc.stdin.flush()
|
|
346
|
+
|
|
347
|
+
def _notify(self, method: str, params: Any) -> None:
|
|
348
|
+
msg: dict[str, Any] = {"jsonrpc": "2.0", "method": method}
|
|
349
|
+
if params is not None:
|
|
350
|
+
msg["params"] = params
|
|
351
|
+
with self._lock:
|
|
352
|
+
self._write(msg)
|
|
353
|
+
|
|
354
|
+
def _request(self, method: str, params: Any, *, timeout: float | None = None) -> Any:
|
|
355
|
+
wait = self.request_timeout if timeout is None else timeout
|
|
356
|
+
with self._lock:
|
|
357
|
+
req_id = self._next_id
|
|
358
|
+
self._next_id += 1
|
|
359
|
+
event = threading.Event()
|
|
360
|
+
self._events[req_id] = event
|
|
361
|
+
msg: dict[str, Any] = {"jsonrpc": "2.0", "id": req_id, "method": method}
|
|
362
|
+
if params is not None:
|
|
363
|
+
msg["params"] = params
|
|
364
|
+
try:
|
|
365
|
+
self._write(msg)
|
|
366
|
+
except Exception:
|
|
367
|
+
self._events.pop(req_id, None)
|
|
368
|
+
raise
|
|
369
|
+
if not event.wait(timeout=wait):
|
|
370
|
+
with self._lock:
|
|
371
|
+
self._events.pop(req_id, None)
|
|
372
|
+
self._pending.pop(req_id, None)
|
|
373
|
+
logger.debug("LSP request %s timed out after %.1fs", method, wait)
|
|
374
|
+
return None
|
|
375
|
+
with self._lock:
|
|
376
|
+
self._events.pop(req_id, None)
|
|
377
|
+
response = self._pending.pop(req_id, None)
|
|
378
|
+
if response is None:
|
|
379
|
+
return None
|
|
380
|
+
if "error" in response:
|
|
381
|
+
logger.debug("LSP %s error: %s", method, response.get("error"))
|
|
382
|
+
return None
|
|
383
|
+
return response.get("result")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class LspSessionPool:
|
|
387
|
+
"""Lazy per-language :class:`LspClient` pool for one project root."""
|
|
388
|
+
|
|
389
|
+
def __init__(
|
|
390
|
+
self,
|
|
391
|
+
project_root: Path,
|
|
392
|
+
*,
|
|
393
|
+
request_timeout: float = _DEFAULT_REQUEST_TIMEOUT,
|
|
394
|
+
client_factory: Callable[[Path, str, list[str]], LspClient] | None = None,
|
|
395
|
+
) -> None:
|
|
396
|
+
self.project_root = project_root.resolve()
|
|
397
|
+
self.request_timeout = request_timeout
|
|
398
|
+
self._clients: dict[str, LspClient] = {}
|
|
399
|
+
self._failed: set[str] = set()
|
|
400
|
+
self._factory = client_factory
|
|
401
|
+
|
|
402
|
+
def close(self) -> None:
|
|
403
|
+
for client in list(self._clients.values()):
|
|
404
|
+
client.shutdown()
|
|
405
|
+
self._clients.clear()
|
|
406
|
+
self._failed.clear()
|
|
407
|
+
|
|
408
|
+
def __enter__(self) -> LspSessionPool:
|
|
409
|
+
return self
|
|
410
|
+
|
|
411
|
+
def __exit__(self, *exc: object) -> None:
|
|
412
|
+
self.close()
|
|
413
|
+
|
|
414
|
+
def client_for(self, rel_path: str) -> LspClient | None:
|
|
415
|
+
language = language_for_path(rel_path)
|
|
416
|
+
if not language or language in self._failed:
|
|
417
|
+
return None
|
|
418
|
+
existing = self._clients.get(language)
|
|
419
|
+
if existing is not None:
|
|
420
|
+
return existing if existing._alive else None
|
|
421
|
+
command = first_available_command(language)
|
|
422
|
+
if command is None:
|
|
423
|
+
self._failed.add(language)
|
|
424
|
+
return None
|
|
425
|
+
if self._factory is not None:
|
|
426
|
+
client = self._factory(self.project_root, language, command)
|
|
427
|
+
else:
|
|
428
|
+
client = LspClient(
|
|
429
|
+
self.project_root,
|
|
430
|
+
language,
|
|
431
|
+
command,
|
|
432
|
+
request_timeout=self.request_timeout,
|
|
433
|
+
)
|
|
434
|
+
if not client.start():
|
|
435
|
+
self._failed.add(language)
|
|
436
|
+
return None
|
|
437
|
+
self._clients[language] = client
|
|
438
|
+
return client
|
|
439
|
+
|
|
440
|
+
def confirm_unreferenced(self, rel_path: str, line: int, name: str) -> bool | None:
|
|
441
|
+
"""Confirm a token-scan dead symbol has zero external LSP references.
|
|
442
|
+
|
|
443
|
+
Returns:
|
|
444
|
+
``True`` — LSP confirmed unreferenced (still dead).
|
|
445
|
+
``False`` — LSP found external references (not dead).
|
|
446
|
+
``None`` — no server / request failed (keep token-scan result).
|
|
447
|
+
"""
|
|
448
|
+
client = self.client_for(rel_path)
|
|
449
|
+
if client is None:
|
|
450
|
+
return None
|
|
451
|
+
abs_path = self.project_root / rel_path
|
|
452
|
+
try:
|
|
453
|
+
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
454
|
+
except OSError:
|
|
455
|
+
return None
|
|
456
|
+
pos = _symbol_position(source, line, name)
|
|
457
|
+
if pos is None:
|
|
458
|
+
return None
|
|
459
|
+
locs = client.references(rel_path, pos[0], pos[1], include_declaration=False)
|
|
460
|
+
if locs is None:
|
|
461
|
+
return None
|
|
462
|
+
norm = rel_path.replace("\\", "/")
|
|
463
|
+
for loc in locs:
|
|
464
|
+
if loc.path.replace("\\", "/") != norm:
|
|
465
|
+
return False
|
|
466
|
+
# Same-file hit outside the defining line → a real use.
|
|
467
|
+
if loc.line != line:
|
|
468
|
+
return False
|
|
469
|
+
return True
|
|
470
|
+
|
|
471
|
+
def dependents_of_file(self, rel_path: str) -> list[str] | None:
|
|
472
|
+
"""Files that reference any top-level public symbol in ``rel_path``.
|
|
473
|
+
|
|
474
|
+
``None`` when no server is available or all queries fail.
|
|
475
|
+
"""
|
|
476
|
+
client = self.client_for(rel_path)
|
|
477
|
+
if client is None:
|
|
478
|
+
return None
|
|
479
|
+
abs_path = self.project_root / rel_path
|
|
480
|
+
try:
|
|
481
|
+
source = abs_path.read_text(encoding="utf-8", errors="replace")
|
|
482
|
+
except OSError:
|
|
483
|
+
return None
|
|
484
|
+
symbols = list(_iter_public_symbols(rel_path, source))
|
|
485
|
+
if not symbols:
|
|
486
|
+
return []
|
|
487
|
+
norm = rel_path.replace("\\", "/")
|
|
488
|
+
dependents: set[str] = set()
|
|
489
|
+
any_ok = False
|
|
490
|
+
for line, name in symbols:
|
|
491
|
+
pos = _symbol_position(source, line, name)
|
|
492
|
+
if pos is None:
|
|
493
|
+
continue
|
|
494
|
+
locs = client.references(rel_path, pos[0], pos[1], include_declaration=False)
|
|
495
|
+
if locs is None:
|
|
496
|
+
continue
|
|
497
|
+
any_ok = True
|
|
498
|
+
for loc in locs:
|
|
499
|
+
other = loc.path.replace("\\", "/")
|
|
500
|
+
if other != norm:
|
|
501
|
+
dependents.add(other)
|
|
502
|
+
if not any_ok:
|
|
503
|
+
return None
|
|
504
|
+
return sorted(dependents)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _iter_public_symbols(rel_path: str, source: str) -> Iterator[tuple[int, str]]:
|
|
508
|
+
"""Yield ``(1-based line, name)`` for public top-level defs (Python / JS-ish)."""
|
|
509
|
+
suffix = Path(rel_path).suffix.lower()
|
|
510
|
+
if suffix == ".py":
|
|
511
|
+
try:
|
|
512
|
+
import ast
|
|
513
|
+
|
|
514
|
+
tree = ast.parse(source)
|
|
515
|
+
except SyntaxError:
|
|
516
|
+
return
|
|
517
|
+
for node in tree.body:
|
|
518
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
|
|
519
|
+
name = node.name
|
|
520
|
+
if name.startswith("_"):
|
|
521
|
+
continue
|
|
522
|
+
yield int(getattr(node, "lineno", 1) or 1), name
|
|
523
|
+
return
|
|
524
|
+
if suffix in {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}:
|
|
525
|
+
import re
|
|
526
|
+
|
|
527
|
+
export_re = re.compile(
|
|
528
|
+
r"(?m)^\s*export\s+(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
529
|
+
)
|
|
530
|
+
for m in export_re.finditer(source):
|
|
531
|
+
name = m.group(1)
|
|
532
|
+
if name.startswith("_"):
|
|
533
|
+
continue
|
|
534
|
+
line = source[: m.start()].count("\n") + 1
|
|
535
|
+
yield line, name
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def lsp_refs_enabled(project_root: Path) -> bool:
|
|
539
|
+
"""Read ``indexing.lsp_refs`` from config; default False. Never raises."""
|
|
540
|
+
try:
|
|
541
|
+
from devcouncil.app.config import load_config
|
|
542
|
+
|
|
543
|
+
return bool(load_config(project_root).indexing.lsp_refs)
|
|
544
|
+
except Exception:
|
|
545
|
+
return False
|
|
546
|
+
|
|
547
|
+
|
|
548
|
+
def filter_dead_symbols_with_lsp(
|
|
549
|
+
project_root: Path,
|
|
550
|
+
candidates: list[str],
|
|
551
|
+
*,
|
|
552
|
+
pool: LspSessionPool | None = None,
|
|
553
|
+
) -> list[str]:
|
|
554
|
+
"""Drop token-scan dead symbols that LSP finds referenced.
|
|
555
|
+
|
|
556
|
+
When LSP is unavailable for a candidate, keep it (token-scan stands).
|
|
557
|
+
"""
|
|
558
|
+
if not candidates:
|
|
559
|
+
return candidates
|
|
560
|
+
own_pool = pool is None
|
|
561
|
+
session = pool or LspSessionPool(project_root)
|
|
562
|
+
try:
|
|
563
|
+
kept: list[str] = []
|
|
564
|
+
for entry in candidates:
|
|
565
|
+
loc, _, name = entry.partition(" ")
|
|
566
|
+
path, _, line_s = loc.rpartition(":")
|
|
567
|
+
if not path or not name or not line_s.isdigit():
|
|
568
|
+
kept.append(entry)
|
|
569
|
+
continue
|
|
570
|
+
confirmed = session.confirm_unreferenced(path.replace("\\", "/"), int(line_s), name.strip())
|
|
571
|
+
if confirmed is False:
|
|
572
|
+
continue
|
|
573
|
+
kept.append(entry)
|
|
574
|
+
return kept
|
|
575
|
+
finally:
|
|
576
|
+
if own_pool:
|
|
577
|
+
session.close()
|