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,484 @@
|
|
|
1
|
+
"""Affected-file graph replacement without global static re-resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from devcouncil.codeintel.resolution import enrich_semantic_edges
|
|
10
|
+
from devcouncil.codeintel.service import CodeIntelService
|
|
11
|
+
from devcouncil.codeintel.build_control import (
|
|
12
|
+
graph_build_session,
|
|
13
|
+
record_inline_build_status,
|
|
14
|
+
run_isolated_full_build,
|
|
15
|
+
)
|
|
16
|
+
from devcouncil.indexing.graph.build import (
|
|
17
|
+
_CODE_SUFFIXES,
|
|
18
|
+
_code_files,
|
|
19
|
+
_files_fingerprint,
|
|
20
|
+
_git_head,
|
|
21
|
+
content_fingerprint,
|
|
22
|
+
extract_all,
|
|
23
|
+
extract_paths,
|
|
24
|
+
write_code_graph,
|
|
25
|
+
)
|
|
26
|
+
from devcouncil.indexing.graph.liveness import (
|
|
27
|
+
build_liveness_shard,
|
|
28
|
+
dynamic_import_index_from_shards,
|
|
29
|
+
extraction_from_liveness_shard,
|
|
30
|
+
file_liveness_from_shards,
|
|
31
|
+
legacy_dead_strings,
|
|
32
|
+
project_call_edges_to_files,
|
|
33
|
+
symbol_reachability_dead,
|
|
34
|
+
token_dead_from_shards,
|
|
35
|
+
)
|
|
36
|
+
from devcouncil.indexing.graph.resolve import (
|
|
37
|
+
build_file_and_symbol_nodes,
|
|
38
|
+
contains_and_defines_edges,
|
|
39
|
+
decorator_edges,
|
|
40
|
+
import_graph_edges,
|
|
41
|
+
inherit_edges,
|
|
42
|
+
named_import_edges,
|
|
43
|
+
resolve_calls,
|
|
44
|
+
resolve_import_edges,
|
|
45
|
+
)
|
|
46
|
+
from devcouncil.indexing.graph.schema import CodeGraph, NodeKind
|
|
47
|
+
from devcouncil.indexing.map_refresh import refresh_repo_map_from_graph
|
|
48
|
+
from devcouncil.indexing.repo_mapper import RepoMapper
|
|
49
|
+
|
|
50
|
+
logger = logging.getLogger(__name__)
|
|
51
|
+
# Kept as a patch point for existing correctness tests; this is the shard-based
|
|
52
|
+
# implementation, not the repository-scanning legacy helper.
|
|
53
|
+
_token_scan_dead = token_dead_from_shards
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sync_affected_paths(
|
|
57
|
+
service: CodeIntelService,
|
|
58
|
+
paths: list[str],
|
|
59
|
+
*,
|
|
60
|
+
liveness: bool = True,
|
|
61
|
+
) -> CodeGraph:
|
|
62
|
+
"""Replace affected rows and dependent resolution as one committed generation."""
|
|
63
|
+
with graph_build_session(service.project_root):
|
|
64
|
+
return _sync_affected_paths_locked(service, paths, liveness=liveness)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _sync_affected_paths_locked(
|
|
68
|
+
service: CodeIntelService,
|
|
69
|
+
paths: list[str],
|
|
70
|
+
*,
|
|
71
|
+
liveness: bool,
|
|
72
|
+
) -> CodeGraph:
|
|
73
|
+
root = service.project_root
|
|
74
|
+
current = service.load()
|
|
75
|
+
if current is None:
|
|
76
|
+
result = run_isolated_full_build(
|
|
77
|
+
root,
|
|
78
|
+
changed_paths={_normal(path) for path in paths},
|
|
79
|
+
liveness=liveness,
|
|
80
|
+
)
|
|
81
|
+
return result.graph
|
|
82
|
+
previous_communities = {
|
|
83
|
+
(node.id, node.path, node.line, node.kind.value): node.community
|
|
84
|
+
for node in current.nodes
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
mapper = RepoMapper(root)
|
|
88
|
+
files = mapper.get_git_files()
|
|
89
|
+
code_files = _code_files(files)
|
|
90
|
+
changed = {_normal(path) for path in paths}
|
|
91
|
+
affected = _affected_closure(current, changed, service)
|
|
92
|
+
selected = extract_paths(root, affected)
|
|
93
|
+
shards = service.store.analysis_shards()
|
|
94
|
+
if not shards:
|
|
95
|
+
# One-time migration path for v1 stores. Subsequent edits use persisted
|
|
96
|
+
# shards and never scan unchanged source files.
|
|
97
|
+
migrated = extract_all(root, files, changed_paths=affected)
|
|
98
|
+
shards = {
|
|
99
|
+
path: build_liveness_shard(root, extraction)
|
|
100
|
+
for path, extraction in migrated.items()
|
|
101
|
+
}
|
|
102
|
+
live_paths = set(code_files)
|
|
103
|
+
stale_shard_paths = set(shards) - live_paths
|
|
104
|
+
shards = {
|
|
105
|
+
path: shard
|
|
106
|
+
for path, shard in shards.items()
|
|
107
|
+
if path in live_paths
|
|
108
|
+
}
|
|
109
|
+
code_changed = {
|
|
110
|
+
path for path in changed
|
|
111
|
+
if path in live_paths or path in shards or Path(path).suffix.lower() in _CODE_SUFFIXES
|
|
112
|
+
}
|
|
113
|
+
if not _incremental_surface_is_stable(code_changed, shards, selected):
|
|
114
|
+
result = run_isolated_full_build(
|
|
115
|
+
root,
|
|
116
|
+
changed_paths=code_changed,
|
|
117
|
+
liveness=liveness,
|
|
118
|
+
)
|
|
119
|
+
refresh_repo_map_from_graph(
|
|
120
|
+
root,
|
|
121
|
+
result.graph,
|
|
122
|
+
changed,
|
|
123
|
+
files,
|
|
124
|
+
mapper=mapper,
|
|
125
|
+
)
|
|
126
|
+
return result.graph
|
|
127
|
+
for path in affected:
|
|
128
|
+
shards.pop(path, None)
|
|
129
|
+
shards.update({
|
|
130
|
+
path: build_liveness_shard(root, extraction)
|
|
131
|
+
for path, extraction in selected.items()
|
|
132
|
+
})
|
|
133
|
+
extractions = {
|
|
134
|
+
path: extraction_from_liveness_shard(shard)
|
|
135
|
+
for path, shard in shards.items()
|
|
136
|
+
}
|
|
137
|
+
replacement_nodes, _ = build_file_and_symbol_nodes(
|
|
138
|
+
selected,
|
|
139
|
+
area_fn=mapper._area_for_file,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
old_ids = {node.id for node in current.nodes if node.path in affected}
|
|
143
|
+
nodes = [
|
|
144
|
+
node for node in current.nodes
|
|
145
|
+
if node.path not in affected and (not node.path or node.path in live_paths)
|
|
146
|
+
]
|
|
147
|
+
nodes.extend(replacement_nodes)
|
|
148
|
+
symbol_index = _symbol_index(nodes)
|
|
149
|
+
class_ids = {node.id for node in nodes if node.kind == NodeKind.CLASS}
|
|
150
|
+
|
|
151
|
+
kept_edges = [
|
|
152
|
+
edge for edge in current.edges
|
|
153
|
+
if edge.source not in old_ids
|
|
154
|
+
and edge.source not in affected
|
|
155
|
+
]
|
|
156
|
+
old_file_edges = {
|
|
157
|
+
(edge.source, edge.target)
|
|
158
|
+
for edge in kept_edges
|
|
159
|
+
if edge.kind == "imports" and "::" not in edge.source and "::" not in edge.target
|
|
160
|
+
}
|
|
161
|
+
resolved_file_edges = {
|
|
162
|
+
edge for edge in resolve_import_edges(selected, files, root=root, mapper=mapper)
|
|
163
|
+
if edge[0] in affected
|
|
164
|
+
}
|
|
165
|
+
file_edges = sorted(old_file_edges | resolved_file_edges)
|
|
166
|
+
# File-to-file imports are rebuilt from the updated adjacency below.
|
|
167
|
+
# Preserve symbol-level named imports for unchanged files: dropping them
|
|
168
|
+
# makes their targets appear newly dead and expands a one-file write into
|
|
169
|
+
# hundreds or thousands of unrelated liveness payloads.
|
|
170
|
+
kept_edges = [
|
|
171
|
+
edge for edge in kept_edges
|
|
172
|
+
if not (
|
|
173
|
+
edge.kind == "imports"
|
|
174
|
+
and "::" not in edge.source
|
|
175
|
+
and "::" not in edge.target
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
replacement_edges = []
|
|
180
|
+
replacement_edges.extend(contains_and_defines_edges(selected))
|
|
181
|
+
replacement_edges.extend(inherit_edges(selected, symbol_index))
|
|
182
|
+
replacement_edges.extend(decorator_edges(selected, symbol_index))
|
|
183
|
+
replacement_edges.extend(import_graph_edges(file_edges))
|
|
184
|
+
replacement_edges.extend(named_import_edges(selected, symbol_index, file_edges))
|
|
185
|
+
unresolved_refs: list[dict[str, object]] = []
|
|
186
|
+
replacement_edges.extend(
|
|
187
|
+
resolve_calls(
|
|
188
|
+
selected,
|
|
189
|
+
symbol_index,
|
|
190
|
+
file_edges,
|
|
191
|
+
class_ids=class_ids,
|
|
192
|
+
unresolved_out=unresolved_refs,
|
|
193
|
+
)
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
graph = current.model_copy(deep=True)
|
|
197
|
+
graph.nodes = nodes
|
|
198
|
+
graph.edges = kept_edges + replacement_edges
|
|
199
|
+
enrich_budget = 120.0
|
|
200
|
+
try:
|
|
201
|
+
from devcouncil.app.config import load_config
|
|
202
|
+
|
|
203
|
+
enrich_budget = float(load_config(root).indexing.semantic_enrich_timeout_seconds)
|
|
204
|
+
except Exception:
|
|
205
|
+
logger.debug("semantic enrich budget config load failed", exc_info=True)
|
|
206
|
+
enrich_semantic_edges(
|
|
207
|
+
graph,
|
|
208
|
+
root=root,
|
|
209
|
+
paths=affected,
|
|
210
|
+
extractions=extractions,
|
|
211
|
+
budget_seconds=enrich_budget,
|
|
212
|
+
)
|
|
213
|
+
referenced_ids = {
|
|
214
|
+
identifier
|
|
215
|
+
for edge in graph.edges
|
|
216
|
+
for identifier in (edge.source, edge.target)
|
|
217
|
+
}
|
|
218
|
+
graph.nodes = [
|
|
219
|
+
node for node in graph.nodes
|
|
220
|
+
if node.path
|
|
221
|
+
or node.kind not in {NodeKind.EVENT, NodeKind.DYNAMIC}
|
|
222
|
+
or node.id in referenced_ids
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
if liveness:
|
|
226
|
+
from devcouncil.indexing.wiring import build_dynamic_import_index, entry_roots
|
|
227
|
+
|
|
228
|
+
all_roots = entry_roots(root, files)
|
|
229
|
+
fresh_roots = entry_roots(root, files, production_only=True)
|
|
230
|
+
if any("dynamic_import_keys" not in shard for shard in shards.values()):
|
|
231
|
+
dynamic_index = build_dynamic_import_index(root, files)
|
|
232
|
+
for shard in shards.values():
|
|
233
|
+
shard["dynamic_import_keys"] = []
|
|
234
|
+
for key, referencing_paths in dynamic_index.items():
|
|
235
|
+
for referencing_path in referencing_paths:
|
|
236
|
+
target_shard = shards.get(referencing_path)
|
|
237
|
+
if target_shard is not None:
|
|
238
|
+
raw_keys = target_shard.setdefault("dynamic_import_keys", [])
|
|
239
|
+
if isinstance(raw_keys, list):
|
|
240
|
+
raw_keys.append(key)
|
|
241
|
+
else:
|
|
242
|
+
dynamic_index = dynamic_import_index_from_shards(shards)
|
|
243
|
+
unsharded_files = [path for path in files if path not in shards]
|
|
244
|
+
for key, referencing_paths in build_dynamic_import_index(
|
|
245
|
+
root, unsharded_files
|
|
246
|
+
).items():
|
|
247
|
+
dynamic_index.setdefault(key, set()).update(referencing_paths)
|
|
248
|
+
# Same call-edge projection as the full build (see assemble_graph) so a
|
|
249
|
+
# Go same-package callee doesn't flip back to unwired on the first
|
|
250
|
+
# incremental refresh after a full map said it was live.
|
|
251
|
+
liveness_file_edges = sorted(
|
|
252
|
+
set(file_edges) | project_call_edges_to_files(graph.edges)
|
|
253
|
+
)
|
|
254
|
+
entry_roots_list, unwired, unreachable, unreliable = file_liveness_from_shards(
|
|
255
|
+
files,
|
|
256
|
+
liveness_file_edges,
|
|
257
|
+
shards,
|
|
258
|
+
root=root,
|
|
259
|
+
entry_roots=all_roots,
|
|
260
|
+
production_entry_roots=fresh_roots,
|
|
261
|
+
dynamic_index=dynamic_index,
|
|
262
|
+
)
|
|
263
|
+
token_dead, _token_index, token_keys = _token_scan_dead(
|
|
264
|
+
graph.nodes, shards
|
|
265
|
+
)
|
|
266
|
+
graph.dead_code = symbol_reachability_dead(
|
|
267
|
+
root,
|
|
268
|
+
files,
|
|
269
|
+
graph.nodes,
|
|
270
|
+
graph.edges,
|
|
271
|
+
extractions,
|
|
272
|
+
entry_roots_list,
|
|
273
|
+
token_dead_keys=token_keys,
|
|
274
|
+
file_edges=file_edges,
|
|
275
|
+
unreachable=[] if unreliable else unreachable,
|
|
276
|
+
dynamic_index=dynamic_index,
|
|
277
|
+
)
|
|
278
|
+
graph.entry_roots = entry_roots_list
|
|
279
|
+
graph.unwired_candidates = unwired
|
|
280
|
+
graph.unreachable_files = unreachable
|
|
281
|
+
graph.meta["liveness_unreachable_unreliable"] = unreliable
|
|
282
|
+
graph.meta["token_dead_count"] = len(token_dead)
|
|
283
|
+
prior_unresolved = [
|
|
284
|
+
ref
|
|
285
|
+
for ref in (current.meta.get("unresolved_references") or [])
|
|
286
|
+
if isinstance(ref, dict) and str(ref.get("path") or "") not in affected
|
|
287
|
+
]
|
|
288
|
+
merged_unresolved = prior_unresolved + unresolved_refs
|
|
289
|
+
graph.meta["unresolved_references"] = merged_unresolved[:200]
|
|
290
|
+
graph.meta["unresolved_total"] = len(merged_unresolved)
|
|
291
|
+
graph.meta["legacy_dead_symbol_candidates"] = legacy_dead_strings(
|
|
292
|
+
graph.dead_code,
|
|
293
|
+
token_dead,
|
|
294
|
+
cap=None,
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
from devcouncil.indexing.graph.intel import enrich_graph_intel
|
|
299
|
+
|
|
300
|
+
enrich_graph_intel(graph, root=root)
|
|
301
|
+
except Exception:
|
|
302
|
+
logger.debug("incremental graph-intelligence refresh failed", exc_info=True)
|
|
303
|
+
|
|
304
|
+
graph.generated_head = _git_head(root)
|
|
305
|
+
graph.indexed_hash = _files_fingerprint(files)
|
|
306
|
+
graph.content_fingerprint = content_fingerprint(root, files)
|
|
307
|
+
graph.meta.update({
|
|
308
|
+
"incremental": True,
|
|
309
|
+
"changed_paths": sorted(changed),
|
|
310
|
+
"affected_paths": sorted(affected),
|
|
311
|
+
"affected_fraction": len(affected) / max(1, len(code_files)),
|
|
312
|
+
"resolution_scope": "affected",
|
|
313
|
+
})
|
|
314
|
+
previous_dead = {
|
|
315
|
+
(entry.id, entry.confidence.value, entry.reason)
|
|
316
|
+
for entry in current.dead_code
|
|
317
|
+
}
|
|
318
|
+
next_dead = {
|
|
319
|
+
(entry.id, entry.confidence.value, entry.reason)
|
|
320
|
+
for entry in graph.dead_code
|
|
321
|
+
}
|
|
322
|
+
dead_changed_ids = {
|
|
323
|
+
item[0] for item in previous_dead.symmetric_difference(next_dead)
|
|
324
|
+
}
|
|
325
|
+
liveness_changed_paths = {
|
|
326
|
+
node.path for node in graph.nodes
|
|
327
|
+
if node.id in dead_changed_ids and node.path
|
|
328
|
+
} | {
|
|
329
|
+
entry.path for entry in current.dead_code
|
|
330
|
+
if entry.id in dead_changed_ids and entry.path
|
|
331
|
+
}
|
|
332
|
+
community_changed_paths = {
|
|
333
|
+
node.path
|
|
334
|
+
for node in graph.nodes
|
|
335
|
+
if node.path
|
|
336
|
+
and previous_communities.get(
|
|
337
|
+
(node.id, node.path, node.line, node.kind.value)
|
|
338
|
+
) != node.community
|
|
339
|
+
}
|
|
340
|
+
# Pathless semantic channel/bridge nodes must be replaced as a group; SQLite
|
|
341
|
+
# otherwise copies them from the prior generation as "unaffected".
|
|
342
|
+
persistence_paths = (
|
|
343
|
+
affected
|
|
344
|
+
| liveness_changed_paths
|
|
345
|
+
| community_changed_paths
|
|
346
|
+
| stale_shard_paths
|
|
347
|
+
| {""}
|
|
348
|
+
)
|
|
349
|
+
graph.meta["liveness_changed_paths"] = sorted(liveness_changed_paths)
|
|
350
|
+
graph.meta["community_changed_paths"] = sorted(community_changed_paths)
|
|
351
|
+
from devcouncil.indexing.graph.build import CompatibilityGraphTooLarge
|
|
352
|
+
|
|
353
|
+
generation_before = service.store.current_generation()
|
|
354
|
+
compatibility_reason = ""
|
|
355
|
+
try:
|
|
356
|
+
write_code_graph(
|
|
357
|
+
root,
|
|
358
|
+
graph,
|
|
359
|
+
changed_paths=persistence_paths,
|
|
360
|
+
analysis_shards=shards,
|
|
361
|
+
)
|
|
362
|
+
except CompatibilityGraphTooLarge as exc:
|
|
363
|
+
compatibility_reason = str(exc)
|
|
364
|
+
logger.warning("graph committed but compatibility export was skipped: %s", exc)
|
|
365
|
+
generation_after = service.store.current_generation()
|
|
366
|
+
record_inline_build_status(
|
|
367
|
+
root,
|
|
368
|
+
state="degraded" if compatibility_reason else "complete",
|
|
369
|
+
mode="incremental",
|
|
370
|
+
phase="complete",
|
|
371
|
+
generation_before=generation_before,
|
|
372
|
+
generation_after=generation_after,
|
|
373
|
+
reason=compatibility_reason,
|
|
374
|
+
compatibility_export="degraded" if compatibility_reason else "healthy",
|
|
375
|
+
)
|
|
376
|
+
refresh_repo_map_from_graph(
|
|
377
|
+
root,
|
|
378
|
+
graph,
|
|
379
|
+
affected,
|
|
380
|
+
files,
|
|
381
|
+
mapper=mapper,
|
|
382
|
+
)
|
|
383
|
+
return service.load() or graph
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def _resolution_surface(extraction) -> dict[str, object]: # noqa: ANN001
|
|
387
|
+
"""Fields whose changes can alter resolution outside the edited file."""
|
|
388
|
+
return {
|
|
389
|
+
"language": extraction.language,
|
|
390
|
+
"imports": list(extraction.imports),
|
|
391
|
+
"import_details": [asdict(value) for value in extraction.import_details],
|
|
392
|
+
"symbols": [
|
|
393
|
+
{
|
|
394
|
+
key: value
|
|
395
|
+
for key, value in asdict(symbol).items()
|
|
396
|
+
if key not in {"line", "end_line"}
|
|
397
|
+
}
|
|
398
|
+
for symbol in extraction.symbols
|
|
399
|
+
],
|
|
400
|
+
"all_exports": list(extraction.all_exports),
|
|
401
|
+
"reexports": list(extraction.reexports),
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def _incremental_surface_is_stable(
|
|
406
|
+
changed: set[str],
|
|
407
|
+
shards: dict[str, dict[str, object]],
|
|
408
|
+
selected: dict,
|
|
409
|
+
) -> bool:
|
|
410
|
+
"""Only existing files with an unchanged external resolution surface are safe."""
|
|
411
|
+
for path in changed:
|
|
412
|
+
old_shard = shards.get(path)
|
|
413
|
+
new_extraction = selected.get(path)
|
|
414
|
+
if old_shard is None or new_extraction is None:
|
|
415
|
+
return False
|
|
416
|
+
old_extraction = extraction_from_liveness_shard(old_shard)
|
|
417
|
+
if _resolution_surface(old_extraction) != _resolution_surface(new_extraction):
|
|
418
|
+
return False
|
|
419
|
+
return True
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def _affected_closure(
|
|
423
|
+
graph: CodeGraph,
|
|
424
|
+
changed: set[str],
|
|
425
|
+
service: CodeIntelService,
|
|
426
|
+
) -> set[str]:
|
|
427
|
+
reverse: dict[str, set[str]] = {}
|
|
428
|
+
for edge in graph.edges:
|
|
429
|
+
if edge.kind == "imports" and "::" not in edge.source and "::" not in edge.target:
|
|
430
|
+
reverse.setdefault(edge.target, set()).add(edge.source)
|
|
431
|
+
affected = set(changed)
|
|
432
|
+
queue = list(changed)
|
|
433
|
+
while queue:
|
|
434
|
+
target = queue.pop()
|
|
435
|
+
for importer in reverse.get(target, set()):
|
|
436
|
+
if importer not in affected:
|
|
437
|
+
affected.add(importer)
|
|
438
|
+
queue.append(importer)
|
|
439
|
+
|
|
440
|
+
changed_names = {
|
|
441
|
+
node.name for node in graph.nodes
|
|
442
|
+
if node.path in changed
|
|
443
|
+
and node.name
|
|
444
|
+
and node.kind not in {
|
|
445
|
+
NodeKind.FILE,
|
|
446
|
+
NodeKind.DYNAMIC,
|
|
447
|
+
NodeKind.EVENT,
|
|
448
|
+
NodeKind.ROUTE,
|
|
449
|
+
NodeKind.STATE,
|
|
450
|
+
NodeKind.PROVIDER,
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
affected.update(
|
|
454
|
+
str(row["path"])
|
|
455
|
+
for row in service.store.unresolved_references()
|
|
456
|
+
if row.get("path") and str(row.get("name") or "") in changed_names
|
|
457
|
+
)
|
|
458
|
+
changed_semantic_kinds = {
|
|
459
|
+
node.kind
|
|
460
|
+
for node in graph.nodes
|
|
461
|
+
if node.path in changed
|
|
462
|
+
and node.kind in {NodeKind.ROUTE, NodeKind.EVENT, NodeKind.PROVIDER}
|
|
463
|
+
}
|
|
464
|
+
if changed_semantic_kinds:
|
|
465
|
+
affected.update(
|
|
466
|
+
node.path for node in graph.nodes
|
|
467
|
+
if node.path and node.kind in changed_semantic_kinds
|
|
468
|
+
)
|
|
469
|
+
return affected
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def _symbol_index(nodes) -> dict[str, str]: # noqa: ANN001
|
|
473
|
+
index: dict[str, str] = {}
|
|
474
|
+
for node in nodes:
|
|
475
|
+
if node.kind == NodeKind.FILE or not node.path:
|
|
476
|
+
continue
|
|
477
|
+
qualname = str(node.extras.get("qualname") or node.id.split("::", 1)[-1].split("#", 1)[0])
|
|
478
|
+
index[f"{node.path}::{qualname}"] = node.id
|
|
479
|
+
return index
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _normal(path: str) -> str:
|
|
483
|
+
normalized = path.replace("\\", "/")
|
|
484
|
+
return normalized[2:] if normalized.startswith("./") else normalized
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""Portable best-effort cross-process writer lease."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import BinaryIO, Callable
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class WriterLease:
|
|
12
|
+
def __init__(self, path: Path):
|
|
13
|
+
self.path = path
|
|
14
|
+
self._handle: BinaryIO | None = None
|
|
15
|
+
|
|
16
|
+
def acquire(self) -> bool:
|
|
17
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
handle = self.path.open("a+b")
|
|
19
|
+
try:
|
|
20
|
+
if os.name == "nt":
|
|
21
|
+
import msvcrt
|
|
22
|
+
|
|
23
|
+
handle.seek(0)
|
|
24
|
+
if handle.tell() == 0:
|
|
25
|
+
handle.write(b"\0")
|
|
26
|
+
handle.flush()
|
|
27
|
+
handle.seek(0)
|
|
28
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_NBLCK, 1) # type: ignore[attr-defined]
|
|
29
|
+
else:
|
|
30
|
+
import fcntl
|
|
31
|
+
|
|
32
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
33
|
+
except (OSError, BlockingIOError):
|
|
34
|
+
handle.close()
|
|
35
|
+
return False
|
|
36
|
+
self._handle = handle
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
def acquire_with_retry(
|
|
40
|
+
self,
|
|
41
|
+
*,
|
|
42
|
+
timeout: float = 30.0,
|
|
43
|
+
initial_delay: float = 0.05,
|
|
44
|
+
max_delay: float = 1.0,
|
|
45
|
+
sleep: Callable[[float], None] | None = None,
|
|
46
|
+
) -> bool:
|
|
47
|
+
"""Acquire the lease, retrying with exponential backoff until ``timeout``.
|
|
48
|
+
|
|
49
|
+
Concurrent watchers / MCP writers commonly hold ``writer.lock`` for a short
|
|
50
|
+
window. Callers that would otherwise fail closed on the first busy probe
|
|
51
|
+
should use this so pending work drains instead of oscillating into
|
|
52
|
+
``read_only`` / lean-map fallbacks.
|
|
53
|
+
"""
|
|
54
|
+
sleeper = sleep or time.sleep
|
|
55
|
+
if self.acquire():
|
|
56
|
+
return True
|
|
57
|
+
deadline = time.monotonic() + max(0.0, timeout)
|
|
58
|
+
delay = max(0.0, initial_delay)
|
|
59
|
+
max_delay = max(delay, max_delay)
|
|
60
|
+
while True:
|
|
61
|
+
remaining = deadline - time.monotonic()
|
|
62
|
+
if remaining <= 0:
|
|
63
|
+
return False
|
|
64
|
+
sleeper(min(delay, remaining, max_delay))
|
|
65
|
+
if self.acquire():
|
|
66
|
+
return True
|
|
67
|
+
if delay <= 0:
|
|
68
|
+
delay = max(initial_delay, 0.05)
|
|
69
|
+
else:
|
|
70
|
+
delay = min(max_delay, delay * 2)
|
|
71
|
+
|
|
72
|
+
def release(self) -> None:
|
|
73
|
+
handle = self._handle
|
|
74
|
+
if handle is None:
|
|
75
|
+
return
|
|
76
|
+
try:
|
|
77
|
+
if os.name == "nt":
|
|
78
|
+
import msvcrt
|
|
79
|
+
|
|
80
|
+
handle.seek(0)
|
|
81
|
+
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1) # type: ignore[attr-defined]
|
|
82
|
+
else:
|
|
83
|
+
import fcntl
|
|
84
|
+
|
|
85
|
+
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
|
86
|
+
finally:
|
|
87
|
+
handle.close()
|
|
88
|
+
self._handle = None
|
|
89
|
+
|
|
90
|
+
def __enter__(self) -> "WriterLease":
|
|
91
|
+
if not self.acquire():
|
|
92
|
+
raise BlockingIOError("another code-intelligence writer owns the project lease")
|
|
93
|
+
return self
|
|
94
|
+
|
|
95
|
+
def __exit__(self, *_args: object) -> None:
|
|
96
|
+
self.release()
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""One index/watch scope policy backed by Git ignore semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from devcouncil.codeintel.languages import detect_language
|
|
9
|
+
from devcouncil.indexing.wiring import is_vendored_path
|
|
10
|
+
|
|
11
|
+
_IGNORED_PREFIXES = (
|
|
12
|
+
".git/",
|
|
13
|
+
".devcouncil/",
|
|
14
|
+
"node_modules/",
|
|
15
|
+
"vendor/",
|
|
16
|
+
".venv/",
|
|
17
|
+
"venv/",
|
|
18
|
+
"dist/",
|
|
19
|
+
"build/",
|
|
20
|
+
"target/",
|
|
21
|
+
"__pycache__/",
|
|
22
|
+
".pytest_cache/",
|
|
23
|
+
".mypy_cache/",
|
|
24
|
+
".ruff_cache/",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class IndexScope:
|
|
29
|
+
def __init__(self, root: Path):
|
|
30
|
+
self.root = root.expanduser().resolve()
|
|
31
|
+
|
|
32
|
+
def includes(self, path: str | Path) -> bool:
|
|
33
|
+
try:
|
|
34
|
+
rel = self.relative(path)
|
|
35
|
+
except ValueError:
|
|
36
|
+
return False
|
|
37
|
+
if not self._language_and_prefix_included(rel):
|
|
38
|
+
return False
|
|
39
|
+
return not self._git_ignored(rel)
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def _language_and_prefix_included(rel: str) -> bool:
|
|
43
|
+
if any(rel == prefix.rstrip("/") or rel.startswith(prefix) for prefix in _IGNORED_PREFIXES):
|
|
44
|
+
return False
|
|
45
|
+
# Match graph ingestion: nested ``vendor/`` / ``.min.js`` must stay out of
|
|
46
|
+
# watch/reconcile scope or they remain perpetually "changed".
|
|
47
|
+
if is_vendored_path(rel):
|
|
48
|
+
return False
|
|
49
|
+
return detect_language(rel) is not None
|
|
50
|
+
|
|
51
|
+
def relative(self, path: str | Path) -> str:
|
|
52
|
+
candidate = Path(path)
|
|
53
|
+
if not candidate.is_absolute():
|
|
54
|
+
candidate = self.root / candidate
|
|
55
|
+
return candidate.resolve(strict=False).relative_to(self.root).as_posix()
|
|
56
|
+
|
|
57
|
+
def files(self) -> list[str]:
|
|
58
|
+
try:
|
|
59
|
+
proc = subprocess.run(
|
|
60
|
+
["git", "ls-files", "-co", "--exclude-standard", "-z"],
|
|
61
|
+
cwd=self.root,
|
|
62
|
+
check=False,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
timeout=10,
|
|
65
|
+
)
|
|
66
|
+
if proc.returncode == 0:
|
|
67
|
+
values = proc.stdout.decode("utf-8", errors="replace").split("\0")
|
|
68
|
+
# git ls-files --exclude-standard has already applied repository
|
|
69
|
+
# ignore rules. Calling check-ignore again for every returned path
|
|
70
|
+
# made MCP startup O(files) subprocesses.
|
|
71
|
+
return sorted({
|
|
72
|
+
value
|
|
73
|
+
for value in values
|
|
74
|
+
if value and self._language_and_prefix_included(value)
|
|
75
|
+
})
|
|
76
|
+
except (OSError, subprocess.SubprocessError):
|
|
77
|
+
pass
|
|
78
|
+
return sorted(
|
|
79
|
+
path.relative_to(self.root).as_posix()
|
|
80
|
+
for path in self.root.rglob("*")
|
|
81
|
+
if path.is_file() and self.includes(path)
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def _git_ignored(self, rel: str) -> bool:
|
|
85
|
+
# Fast exclusions cover high-volume directories. Git is the authority for
|
|
86
|
+
# project-specific rules and correctly handles nested .gitignore files.
|
|
87
|
+
try:
|
|
88
|
+
result = subprocess.run(
|
|
89
|
+
["git", "check-ignore", "-q", "--", rel],
|
|
90
|
+
cwd=self.root,
|
|
91
|
+
check=False,
|
|
92
|
+
stdout=subprocess.DEVNULL,
|
|
93
|
+
stderr=subprocess.DEVNULL,
|
|
94
|
+
timeout=2,
|
|
95
|
+
)
|
|
96
|
+
return result.returncode == 0
|
|
97
|
+
except (OSError, subprocess.SubprocessError):
|
|
98
|
+
return False
|