devcouncil 0.3.1 → 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,329 @@
|
|
|
1
|
+
"""Extraction cache v5 — symbols, calls, import_details, modules/specs keyed by sha256."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Dict, List, Optional, Set, cast
|
|
10
|
+
|
|
11
|
+
from devcouncil.indexing.graph.extract_python import (
|
|
12
|
+
ExtractedCall,
|
|
13
|
+
ExtractedImport,
|
|
14
|
+
ExtractedSymbol,
|
|
15
|
+
FileExtraction,
|
|
16
|
+
)
|
|
17
|
+
from devcouncil.indexing.graph.extract_ts import extract_file
|
|
18
|
+
from devcouncil.utils.fsio import atomic_write_json
|
|
19
|
+
from devcouncil.utils.json_persist import read_json
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# v1 = Python modules; v2 = + JS/TS specs; v3 = + symbols + calls;
|
|
24
|
+
# v4 = + import_details (named imports / alias maps) for warm/cold edge parity;
|
|
25
|
+
# v5 = + references (non-call name/attribute loads) for callback-aware liveness;
|
|
26
|
+
# v6 = generic-extractor callee-style qualname hints (owner hints poisoned
|
|
27
|
+
# same-file call resolution with self-loops).
|
|
28
|
+
# v8 = declarator-chain name recovery for C-family definitions (the language
|
|
29
|
+
# pack returns name=None, dropping every C/C++/ObjC symbol) + grammar
|
|
30
|
+
# availability folded into the cache identity.
|
|
31
|
+
PARSE_CACHE_VERSION = 8
|
|
32
|
+
|
|
33
|
+
# Keys preserved across partial updates (RepoMapper modules/specs vs graph symbols).
|
|
34
|
+
_PRESERVE_KEYS = (
|
|
35
|
+
"symbols",
|
|
36
|
+
"calls",
|
|
37
|
+
"all_exports",
|
|
38
|
+
"reexports",
|
|
39
|
+
"references",
|
|
40
|
+
"language",
|
|
41
|
+
"import_details",
|
|
42
|
+
"modules",
|
|
43
|
+
"specs",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def cache_path(root: Path) -> Path:
|
|
48
|
+
return root / ".devcouncil" / "cache" / "repo_map_parse.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_parse_cache(root: Path) -> Dict[str, Dict[str, Any]]:
|
|
52
|
+
"""Load cache. Version mismatch → empty dict (caller rebuilds in place)."""
|
|
53
|
+
try:
|
|
54
|
+
data = read_json(cache_path(root))
|
|
55
|
+
if data.get("version") == PARSE_CACHE_VERSION and isinstance(data.get("files"), dict):
|
|
56
|
+
return cast(Dict[str, Dict[str, Any]], data["files"])
|
|
57
|
+
except Exception:
|
|
58
|
+
pass
|
|
59
|
+
return {}
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def save_parse_cache(root: Path, files: Dict[str, Dict[str, Any]]) -> None:
|
|
63
|
+
try:
|
|
64
|
+
path = cache_path(root)
|
|
65
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
atomic_write_json(path, {"version": PARSE_CACHE_VERSION, "files": files})
|
|
67
|
+
except Exception:
|
|
68
|
+
logger.debug("Failed to write graph parse cache", exc_info=True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def merge_parse_cache(
|
|
72
|
+
root: Path,
|
|
73
|
+
updates: Dict[str, Dict[str, Any]],
|
|
74
|
+
managed: Set[str],
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Merge updates for ``managed`` paths; preserve sibling-language / sibling-field keys.
|
|
77
|
+
|
|
78
|
+
Paths in ``managed`` but absent from ``updates`` are pruned. When an update
|
|
79
|
+
omits graph or import fields, existing values from the prior entry are kept
|
|
80
|
+
so RepoMapper modules/specs passes and graph extract passes share one store.
|
|
81
|
+
"""
|
|
82
|
+
cache = load_parse_cache(root)
|
|
83
|
+
merged = {k: v for k, v in cache.items() if k not in managed}
|
|
84
|
+
for path, entry in updates.items():
|
|
85
|
+
prev = cache.get(path)
|
|
86
|
+
if isinstance(prev, dict) and isinstance(entry, dict):
|
|
87
|
+
for key in _PRESERVE_KEYS:
|
|
88
|
+
if key not in entry and key in prev:
|
|
89
|
+
entry = {**entry, key: prev[key]}
|
|
90
|
+
merged[path] = entry
|
|
91
|
+
if merged != cache:
|
|
92
|
+
save_parse_cache(root, merged)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _symbol_to_dict(s: ExtractedSymbol) -> Dict[str, Any]:
|
|
96
|
+
return {
|
|
97
|
+
"kind": s.kind,
|
|
98
|
+
"name": s.name,
|
|
99
|
+
"qualname": s.qualname,
|
|
100
|
+
"line": s.line,
|
|
101
|
+
"end_line": s.end_line,
|
|
102
|
+
"bases": list(s.bases),
|
|
103
|
+
"implements": list(getattr(s, "implements", []) or []),
|
|
104
|
+
"decorators": list(s.decorators),
|
|
105
|
+
"exported": s.exported,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _call_to_dict(c: ExtractedCall) -> Dict[str, Any]:
|
|
110
|
+
return {
|
|
111
|
+
"name": c.name,
|
|
112
|
+
"line": c.line,
|
|
113
|
+
"receiver": c.receiver,
|
|
114
|
+
"qualname_hint": c.qualname_hint,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _import_to_dict(imp: ExtractedImport) -> Dict[str, Any]:
|
|
119
|
+
return {
|
|
120
|
+
"module": imp.module,
|
|
121
|
+
"names": list(imp.names),
|
|
122
|
+
"alias_map": dict(imp.alias_map),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _symbol_from_dict(d: Dict[str, Any]) -> ExtractedSymbol:
|
|
127
|
+
return ExtractedSymbol(
|
|
128
|
+
kind=str(d.get("kind") or "function"),
|
|
129
|
+
name=str(d.get("name") or ""),
|
|
130
|
+
qualname=str(d.get("qualname") or d.get("name") or ""),
|
|
131
|
+
line=int(d.get("line") or 0),
|
|
132
|
+
end_line=int(d.get("end_line") or d.get("line") or 0),
|
|
133
|
+
bases=list(d.get("bases") or []),
|
|
134
|
+
implements=list(d.get("implements") or []),
|
|
135
|
+
decorators=list(d.get("decorators") or []),
|
|
136
|
+
exported=bool(d.get("exported", False)),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _call_from_dict(d: Dict[str, Any]) -> ExtractedCall:
|
|
141
|
+
return ExtractedCall(
|
|
142
|
+
name=str(d.get("name") or ""),
|
|
143
|
+
line=int(d.get("line") or 0),
|
|
144
|
+
receiver=str(d.get("receiver") or ""),
|
|
145
|
+
qualname_hint=str(d.get("qualname_hint") or ""),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _import_from_dict(d: Dict[str, Any]) -> ExtractedImport:
|
|
150
|
+
alias_raw = d.get("alias_map") or {}
|
|
151
|
+
alias_map = {
|
|
152
|
+
str(k): str(v) for k, v in alias_raw.items()
|
|
153
|
+
} if isinstance(alias_raw, dict) else {}
|
|
154
|
+
return ExtractedImport(
|
|
155
|
+
module=str(d.get("module") or ""),
|
|
156
|
+
names=[n for n in (d.get("names") or []) if isinstance(n, str)],
|
|
157
|
+
alias_map=alias_map,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def extraction_to_cache_entry(ext: FileExtraction, digest: str) -> Dict[str, Any]:
|
|
162
|
+
entry: Dict[str, Any] = {
|
|
163
|
+
"sha256": digest,
|
|
164
|
+
"symbols": [_symbol_to_dict(s) for s in ext.symbols],
|
|
165
|
+
"calls": [_call_to_dict(c) for c in ext.calls],
|
|
166
|
+
"import_details": [_import_to_dict(i) for i in ext.import_details],
|
|
167
|
+
"all_exports": list(ext.all_exports),
|
|
168
|
+
"reexports": list(ext.reexports),
|
|
169
|
+
"references": list(ext.references),
|
|
170
|
+
"language": ext.language,
|
|
171
|
+
}
|
|
172
|
+
if ext.path.endswith(".py"):
|
|
173
|
+
entry["modules"] = list(ext.imports)
|
|
174
|
+
elif ext.path.endswith(".go"):
|
|
175
|
+
entry["specs"] = list(ext.imports)
|
|
176
|
+
else:
|
|
177
|
+
entry["specs"] = list(ext.imports)
|
|
178
|
+
return entry
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def extraction_from_cache_entry(path: str, entry: Dict[str, Any]) -> FileExtraction:
|
|
182
|
+
imports: List[str] = []
|
|
183
|
+
if isinstance(entry.get("modules"), list):
|
|
184
|
+
imports = [m for m in entry["modules"] if isinstance(m, str)]
|
|
185
|
+
elif isinstance(entry.get("specs"), list):
|
|
186
|
+
imports = [m for m in entry["specs"] if isinstance(m, str)]
|
|
187
|
+
symbols = [
|
|
188
|
+
_symbol_from_dict(s)
|
|
189
|
+
for s in (entry.get("symbols") or [])
|
|
190
|
+
if isinstance(s, dict)
|
|
191
|
+
]
|
|
192
|
+
calls = [
|
|
193
|
+
_call_from_dict(c)
|
|
194
|
+
for c in (entry.get("calls") or [])
|
|
195
|
+
if isinstance(c, dict)
|
|
196
|
+
]
|
|
197
|
+
import_details = [
|
|
198
|
+
_import_from_dict(i)
|
|
199
|
+
for i in (entry.get("import_details") or [])
|
|
200
|
+
if isinstance(i, dict)
|
|
201
|
+
]
|
|
202
|
+
return FileExtraction(
|
|
203
|
+
path=path,
|
|
204
|
+
language=str(entry.get("language") or ""),
|
|
205
|
+
imports=imports,
|
|
206
|
+
import_details=import_details,
|
|
207
|
+
symbols=symbols,
|
|
208
|
+
calls=calls,
|
|
209
|
+
all_exports=[x for x in (entry.get("all_exports") or []) if isinstance(x, str)],
|
|
210
|
+
reexports=[x for x in (entry.get("reexports") or []) if isinstance(x, str)],
|
|
211
|
+
references=[x for x in (entry.get("references") or []) if isinstance(x, str)],
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def extract_cached(
|
|
216
|
+
root: Path,
|
|
217
|
+
path: str,
|
|
218
|
+
*,
|
|
219
|
+
cache: Optional[Dict[str, Dict[str, Any]]] = None,
|
|
220
|
+
force: bool = False,
|
|
221
|
+
) -> tuple[FileExtraction, Dict[str, Any]]:
|
|
222
|
+
"""Extract one file, using cache when sha256 matches.
|
|
223
|
+
|
|
224
|
+
Returns ``(extraction, cache_entry)``.
|
|
225
|
+
"""
|
|
226
|
+
cache = cache if cache is not None else load_parse_cache(root)
|
|
227
|
+
try:
|
|
228
|
+
raw = (root / path).read_bytes()
|
|
229
|
+
except OSError:
|
|
230
|
+
empty = FileExtraction(path=path, language="")
|
|
231
|
+
return empty, {
|
|
232
|
+
"sha256": "",
|
|
233
|
+
"modules": [],
|
|
234
|
+
"symbols": [],
|
|
235
|
+
"calls": [],
|
|
236
|
+
"import_details": [],
|
|
237
|
+
}
|
|
238
|
+
digest = hashlib.sha256(raw).hexdigest()
|
|
239
|
+
grammar = _grammar_identity(path)
|
|
240
|
+
config_hash = f"parse-cache-v{PARSE_CACHE_VERSION}"
|
|
241
|
+
try:
|
|
242
|
+
from devcouncil.codeintel import get_codeintel_service
|
|
243
|
+
|
|
244
|
+
store = get_codeintel_service(root).store
|
|
245
|
+
payload = store.get_extraction(
|
|
246
|
+
content_hash=digest,
|
|
247
|
+
language=grammar[0],
|
|
248
|
+
grammar_version=grammar[1],
|
|
249
|
+
config_hash=config_hash,
|
|
250
|
+
)
|
|
251
|
+
if payload is not None and not force:
|
|
252
|
+
stored_entry = json.loads(payload.decode("utf-8"))
|
|
253
|
+
if isinstance(stored_entry, dict):
|
|
254
|
+
return extraction_from_cache_entry(path, stored_entry), stored_entry
|
|
255
|
+
except Exception:
|
|
256
|
+
logger.debug("SQLite extraction-cache read failed", exc_info=True)
|
|
257
|
+
entry = cache.get(path)
|
|
258
|
+
if (
|
|
259
|
+
not force
|
|
260
|
+
and isinstance(entry, dict)
|
|
261
|
+
and entry.get("sha256") == digest
|
|
262
|
+
and isinstance(entry.get("symbols"), list)
|
|
263
|
+
and isinstance(entry.get("calls"), list)
|
|
264
|
+
and isinstance(entry.get("import_details"), list)
|
|
265
|
+
):
|
|
266
|
+
return extraction_from_cache_entry(path, entry), entry
|
|
267
|
+
source = raw.decode("utf-8", errors="replace")
|
|
268
|
+
try:
|
|
269
|
+
ext = extract_file(path, source)
|
|
270
|
+
except Exception:
|
|
271
|
+
# One pathological file (e.g. RecursionError on absurd nesting) must not
|
|
272
|
+
# fail every subsequent full build. Index it as opaque; sha256 stays ""
|
|
273
|
+
# so the failure is never treated as a valid warm cache entry.
|
|
274
|
+
logger.warning("extraction failed for %s; indexing as opaque file", path, exc_info=True)
|
|
275
|
+
empty = FileExtraction(path=path, language="")
|
|
276
|
+
return empty, {
|
|
277
|
+
"sha256": "",
|
|
278
|
+
"modules": [],
|
|
279
|
+
"symbols": [],
|
|
280
|
+
"calls": [],
|
|
281
|
+
"import_details": [],
|
|
282
|
+
}
|
|
283
|
+
fresh = extraction_to_cache_entry(ext, digest)
|
|
284
|
+
try:
|
|
285
|
+
from devcouncil.codeintel import get_codeintel_service
|
|
286
|
+
|
|
287
|
+
get_codeintel_service(root).store.put_extraction(
|
|
288
|
+
content_hash=digest,
|
|
289
|
+
language=grammar[0],
|
|
290
|
+
grammar_version=grammar[1],
|
|
291
|
+
config_hash=config_hash,
|
|
292
|
+
payload=json.dumps(fresh, sort_keys=True, separators=(",", ":")).encode("utf-8"),
|
|
293
|
+
)
|
|
294
|
+
except Exception:
|
|
295
|
+
logger.debug("SQLite extraction-cache write failed", exc_info=True)
|
|
296
|
+
return ext, fresh
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
_GRAMMAR_PROBE_CACHE: Dict[str, bool] = {}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _grammar_identity(path: str) -> tuple[str, str]:
|
|
303
|
+
from devcouncil.codeintel.languages import detect_language
|
|
304
|
+
|
|
305
|
+
spec = detect_language(path)
|
|
306
|
+
language = spec.grammar if spec is not None else Path(path).suffix.lower().lstrip(".")
|
|
307
|
+
if language == "python":
|
|
308
|
+
return language, "stdlib-ast"
|
|
309
|
+
try:
|
|
310
|
+
import tree_sitter_language_pack as pack
|
|
311
|
+
except ImportError:
|
|
312
|
+
return language, "unavailable"
|
|
313
|
+
version = str(getattr(pack, "__version__", "unknown"))
|
|
314
|
+
# Per-grammar availability must be part of the identity: installing or
|
|
315
|
+
# removing the grammar companion changes extraction output without
|
|
316
|
+
# changing the pack version, and a version-only key would keep serving
|
|
317
|
+
# pre-grammar (empty/regex) extractions forever.
|
|
318
|
+
loadable = _GRAMMAR_PROBE_CACHE.get(language)
|
|
319
|
+
if loadable is None:
|
|
320
|
+
try:
|
|
321
|
+
from devcouncil.codeintel.languages.workers import _activate_companion_once
|
|
322
|
+
|
|
323
|
+
_activate_companion_once()
|
|
324
|
+
pack.get_language(language)
|
|
325
|
+
loadable = True
|
|
326
|
+
except Exception:
|
|
327
|
+
loadable = False
|
|
328
|
+
_GRAMMAR_PROBE_CACHE[language] = loadable
|
|
329
|
+
return language, f"{version}:{'native' if loadable else 'fallback'}"
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"""Community labeling helpers (leaf module — no intel/verifier imports)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import Counter
|
|
6
|
+
|
|
7
|
+
from devcouncil.indexing.graph.schema import CodeGraph, GraphNode, NodeKind
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _is_file_node(n: GraphNode) -> bool:
|
|
11
|
+
kind = n.kind.value if hasattr(n.kind, "value") else str(n.kind)
|
|
12
|
+
return kind == NodeKind.FILE.value or ("::" not in n.id and not n.name)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def community_label_for_area(graph: CodeGraph, area: str) -> str:
|
|
16
|
+
"""Dominant community label among file nodes under ``area`` (generic subsystems)."""
|
|
17
|
+
counts: Counter[str] = Counter()
|
|
18
|
+
prefix = area.replace("\\", "/").rstrip("/") + "/"
|
|
19
|
+
for n in graph.nodes:
|
|
20
|
+
if not _is_file_node(n):
|
|
21
|
+
continue
|
|
22
|
+
path = (n.path or n.id).replace("\\", "/")
|
|
23
|
+
if path == area or path.startswith(prefix):
|
|
24
|
+
if n.community:
|
|
25
|
+
counts[n.community] += 1
|
|
26
|
+
if not counts:
|
|
27
|
+
return ""
|
|
28
|
+
return counts.most_common(1)[0][0]
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Minimal openCypher-style subset over the native in-memory code graph."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
_SUPPORTED_REL = frozenset({
|
|
10
|
+
"CALLS", "IMPORTS", "CONTAINS", "REFERENCES", "IMPLEMENTS", "EXTENDS", "DECORATES",
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
_MATCH_RE = re.compile(
|
|
14
|
+
r"^\s*MATCH\s+\(a(?::(?P<alabel>[\w]+))?\)"
|
|
15
|
+
r"(?:-\[r:(?P<rel>[\w|]+)\]->\(b(?::(?P<blabel>[\w]+))?\))?"
|
|
16
|
+
r"(?:\s+WHERE\s+(?P<where>.+?))?"
|
|
17
|
+
r"\s+RETURN\s+(?P<ret>.+?)(?:\s+LIMIT\s+(?P<limit>\d+))?\s*$",
|
|
18
|
+
re.IGNORECASE | re.DOTALL,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _parse_where(clause: str) -> Tuple[Optional[str], Optional[str]]:
|
|
23
|
+
if not clause:
|
|
24
|
+
return None, None
|
|
25
|
+
lowered = clause.strip().lower()
|
|
26
|
+
if "a.name" in lowered and "contains" in lowered:
|
|
27
|
+
m = re.search(r"contains\s*\(\s*a\.name\s*,\s*['\"]([^'\"]+)['\"]\s*\)", clause, re.I)
|
|
28
|
+
if m:
|
|
29
|
+
return m.group(1), None
|
|
30
|
+
if "b.path" in lowered and "starts with" in lowered:
|
|
31
|
+
m = re.search(r"starts\s+with\s*\(\s*b\.path\s*,\s*['\"]([^'\"]+)['\"]\s*\)", clause, re.I)
|
|
32
|
+
if m:
|
|
33
|
+
return None, m.group(1)
|
|
34
|
+
return None, None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_cypher(project_root: Path, query: str, *, default_limit: int = 50) -> Dict[str, Any]:
|
|
38
|
+
"""Execute a supported MATCH … RETURN subset; reject unsupported clauses."""
|
|
39
|
+
normalized = " ".join(query.split())
|
|
40
|
+
if re.search(r"\b(CREATE|DELETE|MERGE|SET|REMOVE|DETACH)\b", normalized, re.I):
|
|
41
|
+
return {"ok": False, "error": "Mutating Cypher clauses are not supported."}
|
|
42
|
+
m = _MATCH_RE.match(normalized)
|
|
43
|
+
if not m:
|
|
44
|
+
return {
|
|
45
|
+
"ok": False,
|
|
46
|
+
"error": (
|
|
47
|
+
"Unsupported Cypher. Supported: "
|
|
48
|
+
"MATCH (a)-[r:CALLS|IMPORTS|...]->(b) WHERE … RETURN a,b LIMIT n"
|
|
49
|
+
),
|
|
50
|
+
}
|
|
51
|
+
rels = m.group("rel") or ""
|
|
52
|
+
rel_types = {r.strip().upper() for r in rels.split("|") if r.strip()}
|
|
53
|
+
for rel in rel_types:
|
|
54
|
+
if rel not in _SUPPORTED_REL:
|
|
55
|
+
return {"ok": False, "error": f"Unsupported relationship type: {rel}"}
|
|
56
|
+
limit = int(m.group("limit") or default_limit)
|
|
57
|
+
name_filter, path_prefix = _parse_where(m.group("where") or "")
|
|
58
|
+
|
|
59
|
+
from devcouncil.codeintel.query.engine import CodeIntelQueryEngine
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
graph = CodeIntelQueryEngine(project_root)._graph()
|
|
63
|
+
except FileNotFoundError:
|
|
64
|
+
return {"ok": False, "error": "No committed graph generation."}
|
|
65
|
+
|
|
66
|
+
nodes_by_id = {n.id: n for n in graph.nodes}
|
|
67
|
+
rows: List[Dict[str, Any]] = []
|
|
68
|
+
|
|
69
|
+
if rel_types:
|
|
70
|
+
for edge in graph.edges:
|
|
71
|
+
if edge.kind.upper() not in rel_types:
|
|
72
|
+
continue
|
|
73
|
+
a = nodes_by_id.get(edge.source)
|
|
74
|
+
b = nodes_by_id.get(edge.target)
|
|
75
|
+
if a is None or b is None:
|
|
76
|
+
continue
|
|
77
|
+
if name_filter and name_filter.lower() not in a.name.lower():
|
|
78
|
+
continue
|
|
79
|
+
if path_prefix and not b.path.startswith(path_prefix):
|
|
80
|
+
continue
|
|
81
|
+
rows.append({
|
|
82
|
+
"a_id": a.id,
|
|
83
|
+
"a_name": a.name,
|
|
84
|
+
"a_path": a.path,
|
|
85
|
+
"a_kind": a.kind,
|
|
86
|
+
"rel": edge.kind,
|
|
87
|
+
"b_id": b.id,
|
|
88
|
+
"b_name": b.name,
|
|
89
|
+
"b_path": b.path,
|
|
90
|
+
"b_kind": b.kind,
|
|
91
|
+
})
|
|
92
|
+
if len(rows) >= limit:
|
|
93
|
+
break
|
|
94
|
+
else:
|
|
95
|
+
for node in graph.nodes:
|
|
96
|
+
if name_filter and name_filter.lower() not in node.name.lower():
|
|
97
|
+
continue
|
|
98
|
+
rows.append({
|
|
99
|
+
"a_id": node.id,
|
|
100
|
+
"a_name": node.name,
|
|
101
|
+
"a_path": node.path,
|
|
102
|
+
"a_kind": node.kind,
|
|
103
|
+
})
|
|
104
|
+
if len(rows) >= limit:
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
return {"ok": True, "rows": rows, "count": len(rows)}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Opt-in local embeddings for semantic graph search (hash fallback)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
import math
|
|
9
|
+
import re
|
|
10
|
+
import sqlite3
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
_EMBEDDINGS_TABLE = "symbol_embeddings"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _index_db(project_root: Path) -> Path:
|
|
20
|
+
return project_root / ".devcouncil" / "codeintel" / "index.sqlite"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _tokenize(text: str) -> List[str]:
|
|
24
|
+
return [t for t in re.split(r"[^a-zA-Z0-9_]+", text.lower()) if len(t) > 1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _hash_vector(tokens: List[str], dims: int = 64) -> List[float]:
|
|
28
|
+
vec = [0.0] * dims
|
|
29
|
+
for token in tokens:
|
|
30
|
+
digest = hashlib.sha256(token.encode("utf-8")).digest()
|
|
31
|
+
for i in range(dims):
|
|
32
|
+
vec[i] += (digest[i % len(digest)] - 128) / 128.0
|
|
33
|
+
norm = math.sqrt(sum(v * v for v in vec)) or 1.0
|
|
34
|
+
return [v / norm for v in vec]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _cosine(a: List[float], b: List[float]) -> float:
|
|
38
|
+
return sum(x * y for x, y in zip(a, b))
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _current_generation(project_root: Path) -> int | None:
|
|
42
|
+
try:
|
|
43
|
+
from devcouncil.codeintel import get_codeintel_service
|
|
44
|
+
|
|
45
|
+
return get_codeintel_service(project_root).store.current_generation()
|
|
46
|
+
except Exception:
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def ensure_embeddings_schema(project_root: Path) -> None:
|
|
51
|
+
db = _index_db(project_root)
|
|
52
|
+
db.parent.mkdir(parents=True, exist_ok=True)
|
|
53
|
+
with sqlite3.connect(db) as conn:
|
|
54
|
+
conn.execute(
|
|
55
|
+
f"""
|
|
56
|
+
CREATE TABLE IF NOT EXISTS {_EMBEDDINGS_TABLE} (
|
|
57
|
+
node_id TEXT NOT NULL,
|
|
58
|
+
path TEXT NOT NULL,
|
|
59
|
+
label TEXT NOT NULL,
|
|
60
|
+
model TEXT NOT NULL,
|
|
61
|
+
vector_json TEXT NOT NULL,
|
|
62
|
+
generation_id INTEGER NOT NULL DEFAULT 0,
|
|
63
|
+
PRIMARY KEY (generation_id, node_id)
|
|
64
|
+
)
|
|
65
|
+
"""
|
|
66
|
+
)
|
|
67
|
+
cols = {
|
|
68
|
+
str(row[1])
|
|
69
|
+
for row in conn.execute(f"PRAGMA table_info({_EMBEDDINGS_TABLE})").fetchall()
|
|
70
|
+
}
|
|
71
|
+
if "generation_id" not in cols:
|
|
72
|
+
# Legacy single-generation table: rebuild on next build_embeddings.
|
|
73
|
+
conn.execute(f"ALTER TABLE {_EMBEDDINGS_TABLE} ADD COLUMN generation_id INTEGER NOT NULL DEFAULT 0")
|
|
74
|
+
conn.execute(
|
|
75
|
+
f"CREATE INDEX IF NOT EXISTS idx_{_EMBEDDINGS_TABLE}_gen "
|
|
76
|
+
f"ON {_EMBEDDINGS_TABLE}(generation_id)"
|
|
77
|
+
)
|
|
78
|
+
conn.commit()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def embeddings_enabled(project_root: Path) -> bool:
|
|
82
|
+
try:
|
|
83
|
+
from devcouncil.app.config import load_config
|
|
84
|
+
|
|
85
|
+
return bool(load_config(project_root).indexing.embeddings.enabled)
|
|
86
|
+
except Exception:
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def build_embeddings(project_root: Path) -> int:
|
|
91
|
+
"""Rebuild symbol embeddings for the committed codeintel generation."""
|
|
92
|
+
if not embeddings_enabled(project_root):
|
|
93
|
+
return 0
|
|
94
|
+
ensure_embeddings_schema(project_root)
|
|
95
|
+
generation = _current_generation(project_root)
|
|
96
|
+
if generation is None:
|
|
97
|
+
return 0
|
|
98
|
+
from devcouncil.codeintel.query.engine import CodeIntelQueryEngine
|
|
99
|
+
|
|
100
|
+
try:
|
|
101
|
+
graph = CodeIntelQueryEngine(project_root)._graph()
|
|
102
|
+
except FileNotFoundError:
|
|
103
|
+
return 0
|
|
104
|
+
model = "hash-v1"
|
|
105
|
+
try:
|
|
106
|
+
from devcouncil.app.config import load_config
|
|
107
|
+
|
|
108
|
+
model = load_config(project_root).indexing.embeddings.model_name or model
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
count = 0
|
|
112
|
+
try:
|
|
113
|
+
with sqlite3.connect(_index_db(project_root)) as conn:
|
|
114
|
+
conn.execute(
|
|
115
|
+
f"DELETE FROM {_EMBEDDINGS_TABLE} WHERE generation_id=?",
|
|
116
|
+
(generation,),
|
|
117
|
+
)
|
|
118
|
+
# Drop older generations; keep only current + previous.
|
|
119
|
+
conn.execute(
|
|
120
|
+
f"DELETE FROM {_EMBEDDINGS_TABLE} WHERE generation_id NOT IN (?, ?)",
|
|
121
|
+
(generation, max(0, generation - 1)),
|
|
122
|
+
)
|
|
123
|
+
for node in graph.nodes:
|
|
124
|
+
label = f"{node.path} {node.name} {node.kind}"
|
|
125
|
+
vec = _hash_vector(_tokenize(label))
|
|
126
|
+
conn.execute(
|
|
127
|
+
f"INSERT OR REPLACE INTO {_EMBEDDINGS_TABLE} "
|
|
128
|
+
f"(node_id, path, label, model, vector_json, generation_id) "
|
|
129
|
+
f"VALUES (?, ?, ?, ?, ?, ?)",
|
|
130
|
+
(node.id, node.path, node.name, model, json.dumps(vec), generation),
|
|
131
|
+
)
|
|
132
|
+
count += 1
|
|
133
|
+
conn.commit()
|
|
134
|
+
except sqlite3.Error:
|
|
135
|
+
logger.warning("embedding rebuild failed; continuing without embeddings", exc_info=True)
|
|
136
|
+
return 0
|
|
137
|
+
return count
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def semantic_search(
|
|
141
|
+
project_root: Path,
|
|
142
|
+
query: str,
|
|
143
|
+
*,
|
|
144
|
+
limit: int = 50,
|
|
145
|
+
) -> Dict[str, Any]:
|
|
146
|
+
"""Semantic search over stored embeddings; empty when disabled."""
|
|
147
|
+
if not embeddings_enabled(project_root):
|
|
148
|
+
return {"ok": False, "reason": "embeddings disabled", "matches": []}
|
|
149
|
+
ensure_embeddings_schema(project_root)
|
|
150
|
+
generation = _current_generation(project_root)
|
|
151
|
+
qvec = _hash_vector(_tokenize(query))
|
|
152
|
+
matches: List[Dict[str, Any]] = []
|
|
153
|
+
scanned = 0
|
|
154
|
+
max_scan = max(500, limit * 200)
|
|
155
|
+
try:
|
|
156
|
+
with sqlite3.connect(_index_db(project_root)) as conn:
|
|
157
|
+
if generation is None:
|
|
158
|
+
rows = conn.execute(
|
|
159
|
+
f"SELECT node_id, path, label, model, vector_json FROM {_EMBEDDINGS_TABLE}"
|
|
160
|
+
)
|
|
161
|
+
else:
|
|
162
|
+
rows = conn.execute(
|
|
163
|
+
f"SELECT node_id, path, label, model, vector_json FROM {_EMBEDDINGS_TABLE} "
|
|
164
|
+
f"WHERE generation_id=?",
|
|
165
|
+
(generation,),
|
|
166
|
+
)
|
|
167
|
+
for node_id, path, label, model, vector_json in rows:
|
|
168
|
+
scanned += 1
|
|
169
|
+
if scanned > max_scan:
|
|
170
|
+
break
|
|
171
|
+
try:
|
|
172
|
+
vec = json.loads(vector_json)
|
|
173
|
+
except json.JSONDecodeError:
|
|
174
|
+
continue
|
|
175
|
+
score = _cosine(qvec, vec)
|
|
176
|
+
matches.append({
|
|
177
|
+
"id": node_id,
|
|
178
|
+
"path": path,
|
|
179
|
+
"label": label,
|
|
180
|
+
"score": round(score, 4),
|
|
181
|
+
"model": model,
|
|
182
|
+
})
|
|
183
|
+
except sqlite3.Error as exc:
|
|
184
|
+
logger.warning("semantic search failed: %s", exc)
|
|
185
|
+
return {"ok": False, "reason": f"embeddings store error: {exc}", "matches": []}
|
|
186
|
+
matches.sort(key=lambda item: item["score"], reverse=True)
|
|
187
|
+
return {
|
|
188
|
+
"ok": True,
|
|
189
|
+
"matches": matches[: max(1, limit)],
|
|
190
|
+
"scanned": scanned,
|
|
191
|
+
"truncated": scanned > max_scan,
|
|
192
|
+
"generation": generation,
|
|
193
|
+
"backend": "hash-v1",
|
|
194
|
+
}
|