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,289 @@
|
|
|
1
|
+
"""Generation-consistent search, exploration, path, and impact queries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections import defaultdict, deque
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Iterable
|
|
8
|
+
|
|
9
|
+
from devcouncil.codeintel.service import CodeIntelService, get_codeintel_service
|
|
10
|
+
from devcouncil.indexing.graph.schema import CodeGraph, Confidence, GraphEdge, GraphNode
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CodeIntelQueryEngine:
|
|
14
|
+
def __init__(self, root: Path | CodeIntelService):
|
|
15
|
+
self.service = root if isinstance(root, CodeIntelService) else get_codeintel_service(root)
|
|
16
|
+
|
|
17
|
+
def _graph(self) -> CodeGraph:
|
|
18
|
+
graph = self.service.load()
|
|
19
|
+
if graph is None:
|
|
20
|
+
raise FileNotFoundError("no code-intelligence index; run `dev graph init`")
|
|
21
|
+
# Fingerprinting shells out to git (diff + untracked hashing) — only pay
|
|
22
|
+
# that per-query cost when runtime evidence actually exists to match.
|
|
23
|
+
if not self.service.store.has_runtime_observations():
|
|
24
|
+
return graph
|
|
25
|
+
from devcouncil.codeintel.debug.fingerprint import source_fingerprint
|
|
26
|
+
|
|
27
|
+
fingerprint = source_fingerprint(self.service.project_root)
|
|
28
|
+
runtime = self.service.store.runtime_observations(source_fingerprint=fingerprint)
|
|
29
|
+
existing = {(edge.source, edge.target, edge.kind) for edge in graph.edges}
|
|
30
|
+
for observation in runtime:
|
|
31
|
+
kind = str(observation["kind"])
|
|
32
|
+
key = (str(observation["source"]), str(observation["target"]), kind)
|
|
33
|
+
if key in existing:
|
|
34
|
+
continue
|
|
35
|
+
sampled = kind in {"sampled_calls", "sampled_stack"}
|
|
36
|
+
graph.edges.append(GraphEdge(
|
|
37
|
+
source=key[0],
|
|
38
|
+
target=key[1],
|
|
39
|
+
kind=kind,
|
|
40
|
+
confidence=Confidence.INFERRED if sampled else Confidence.EXTRACTED,
|
|
41
|
+
reason="fingerprint-matched runtime observation",
|
|
42
|
+
extras={
|
|
43
|
+
"provenance": "runtime",
|
|
44
|
+
"confidence_score": 0.75 if sampled else 1.0,
|
|
45
|
+
"source_fingerprint": fingerprint,
|
|
46
|
+
"runtime_session": observation["session_id"],
|
|
47
|
+
"count": observation["count"],
|
|
48
|
+
"evidence": observation["evidence"],
|
|
49
|
+
},
|
|
50
|
+
))
|
|
51
|
+
existing.add(key)
|
|
52
|
+
return graph
|
|
53
|
+
|
|
54
|
+
def _envelope(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
55
|
+
from devcouncil.codeintel.sync import get_sync_coordinator
|
|
56
|
+
|
|
57
|
+
status = self.service.status()
|
|
58
|
+
sync = get_sync_coordinator(self.service.project_root).status().as_dict()
|
|
59
|
+
graph_degraded = False
|
|
60
|
+
graph_degraded_reason = ""
|
|
61
|
+
try:
|
|
62
|
+
from devcouncil.utils.json_persist import read_json
|
|
63
|
+
|
|
64
|
+
map_path = self.service.project_root / ".devcouncil" / "repo_map.json"
|
|
65
|
+
if map_path.is_file():
|
|
66
|
+
data = read_json(map_path)
|
|
67
|
+
if isinstance(data, dict) and data.get("graph_degraded"):
|
|
68
|
+
graph_degraded = True
|
|
69
|
+
graph_degraded_reason = str(data.get("graph_degraded_reason") or "")
|
|
70
|
+
except Exception:
|
|
71
|
+
pass
|
|
72
|
+
return {
|
|
73
|
+
"ok": True,
|
|
74
|
+
"project_root": str(self.service.project_root),
|
|
75
|
+
"generation": status.get("generation"),
|
|
76
|
+
"schema_version": status.get("schema_version"),
|
|
77
|
+
"analyzer_version": status.get("analyzer_version"),
|
|
78
|
+
"sync": sync,
|
|
79
|
+
"graph_degraded": graph_degraded,
|
|
80
|
+
"graph_degraded_reason": graph_degraded_reason,
|
|
81
|
+
**payload,
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
def search(self, query: str, *, limit: int = 50) -> dict[str, Any]:
|
|
85
|
+
rows = self.service.cached_query(
|
|
86
|
+
"search", f"{query}\0{limit}", lambda: self.service.store.search(query, limit=limit)
|
|
87
|
+
)
|
|
88
|
+
return self._envelope({"query": query, "matches": rows})
|
|
89
|
+
|
|
90
|
+
def explore(self, query: str, *, limit: int = 20) -> dict[str, Any]:
|
|
91
|
+
graph = self._graph()
|
|
92
|
+
matches = self._match(graph, query)[:limit]
|
|
93
|
+
inbound, outbound = self._relations(graph.edges)
|
|
94
|
+
definitions: list[dict[str, Any]] = []
|
|
95
|
+
for node in matches:
|
|
96
|
+
content = self.service.store.content_for_path(node.path)
|
|
97
|
+
snippet = self._snippet(content, node.line, node.end_line)
|
|
98
|
+
callers = [self._edge_dict(edge) for edge in inbound.get(node.id, [])[:50]]
|
|
99
|
+
callees = [self._edge_dict(edge) for edge in outbound.get(node.id, [])[:50]]
|
|
100
|
+
definitions.append({
|
|
101
|
+
"id": node.id,
|
|
102
|
+
"kind": node.kind.value if hasattr(node.kind, "value") else str(node.kind),
|
|
103
|
+
"path": node.path,
|
|
104
|
+
"name": node.name,
|
|
105
|
+
"line": node.line,
|
|
106
|
+
"end_line": node.end_line,
|
|
107
|
+
"language": node.language,
|
|
108
|
+
"source": snippet,
|
|
109
|
+
"callers": callers,
|
|
110
|
+
"callees": callees,
|
|
111
|
+
})
|
|
112
|
+
impacted = self._impact_for_ids(graph, [node.id for node in matches], max_depth=3)
|
|
113
|
+
return self._envelope({
|
|
114
|
+
"query": query,
|
|
115
|
+
"match_count": len(matches),
|
|
116
|
+
"definitions": definitions,
|
|
117
|
+
"blast_radius": impacted,
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
def path(self, start: str, end: str, *, max_depth: int = 32) -> dict[str, Any]:
|
|
121
|
+
graph = self._graph()
|
|
122
|
+
starts = {node.id for node in self._match(graph, start)}
|
|
123
|
+
ends = {node.id for node in self._match(graph, end)}
|
|
124
|
+
if not starts or not ends:
|
|
125
|
+
return self._envelope({"from": start, "to": end, "found": False, "path": [], "reason": "endpoint not found"})
|
|
126
|
+
adjacency: dict[str, list[tuple[str, GraphEdge]]] = defaultdict(list)
|
|
127
|
+
for edge in graph.edges:
|
|
128
|
+
adjacency[edge.source].append((edge.target, edge))
|
|
129
|
+
if edge.kind in {"imports", "contains", "defines", "inherits", "implements", "overrides"}:
|
|
130
|
+
adjacency[edge.target].append((edge.source, edge))
|
|
131
|
+
queue = deque((node_id, 0) for node_id in starts)
|
|
132
|
+
parent: dict[str, tuple[str | None, GraphEdge | None]] = {node_id: (None, None) for node_id in starts}
|
|
133
|
+
found: str | None = None
|
|
134
|
+
while queue:
|
|
135
|
+
current, depth = queue.popleft()
|
|
136
|
+
if current in ends:
|
|
137
|
+
found = current
|
|
138
|
+
break
|
|
139
|
+
if depth >= max_depth:
|
|
140
|
+
continue
|
|
141
|
+
for target, edge in adjacency.get(current, []):
|
|
142
|
+
if target not in parent:
|
|
143
|
+
parent[target] = (current, edge)
|
|
144
|
+
queue.append((target, depth + 1))
|
|
145
|
+
if found is None:
|
|
146
|
+
return self._envelope({"from": start, "to": end, "found": False, "path": []})
|
|
147
|
+
steps: list[dict[str, Any]] = []
|
|
148
|
+
walk = found
|
|
149
|
+
while True:
|
|
150
|
+
previous, path_edge = parent[walk]
|
|
151
|
+
steps.append({"node": walk, "via": self._edge_dict(path_edge) if path_edge else None})
|
|
152
|
+
if previous is None:
|
|
153
|
+
break
|
|
154
|
+
walk = previous
|
|
155
|
+
steps.reverse()
|
|
156
|
+
return self._envelope({"from": start, "to": end, "found": True, "length": len(steps) - 1, "path": steps})
|
|
157
|
+
|
|
158
|
+
def impact(self, targets: Iterable[str], *, max_depth: int = 3) -> dict[str, Any]:
|
|
159
|
+
graph = self._graph()
|
|
160
|
+
ids: list[str] = []
|
|
161
|
+
for target in targets:
|
|
162
|
+
ids.extend(node.id for node in self._match(graph, target))
|
|
163
|
+
return self._envelope({"targets": list(targets), "blast_radius": self._impact_for_ids(graph, ids, max_depth=max_depth)})
|
|
164
|
+
|
|
165
|
+
def dead(self, *, minimum_confidence: str = "inferred") -> dict[str, Any]:
|
|
166
|
+
graph = self._graph()
|
|
167
|
+
ranks = {"ambiguous": 0, "inferred": 1, "extracted": 2}
|
|
168
|
+
floor = ranks.get(minimum_confidence, 1)
|
|
169
|
+
runtime_live = {
|
|
170
|
+
edge.target
|
|
171
|
+
for edge in graph.edges
|
|
172
|
+
if edge.extras.get("provenance") == "runtime"
|
|
173
|
+
}
|
|
174
|
+
rows = []
|
|
175
|
+
for entry in graph.dead_code:
|
|
176
|
+
confidence = entry.confidence.value if hasattr(entry.confidence, "value") else str(entry.confidence)
|
|
177
|
+
if ranks.get(confidence, 0) < floor:
|
|
178
|
+
continue
|
|
179
|
+
if entry.id in runtime_live:
|
|
180
|
+
continue
|
|
181
|
+
tier = "high-confidence dead candidate" if confidence == "extracted" else "unconfirmed/unwired"
|
|
182
|
+
rows.append({**entry.model_dump(mode="json"), "tier": tier})
|
|
183
|
+
return self._envelope({
|
|
184
|
+
"minimum_confidence": minimum_confidence,
|
|
185
|
+
"dead_code": rows,
|
|
186
|
+
"runtime_proven_live": sorted(runtime_live),
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
def affected_tests(self, targets: Iterable[str], *, max_depth: int = 3) -> dict[str, Any]:
|
|
190
|
+
graph = self._graph()
|
|
191
|
+
target_list = list(targets)
|
|
192
|
+
ids = [node.id for target in target_list for node in self._match(graph, target)]
|
|
193
|
+
impact = self._impact_for_ids(graph, ids, max_depth=max_depth)
|
|
194
|
+
impacted_ids = {node_id for layer in impact["layers"] for node_id in layer["nodes"]} | set(ids)
|
|
195
|
+
nodes = graph.node_by_id()
|
|
196
|
+
tests = sorted({
|
|
197
|
+
node.path
|
|
198
|
+
for node_id in impacted_ids
|
|
199
|
+
if (node := nodes.get(node_id)) is not None and self._is_test_path(node.path)
|
|
200
|
+
})
|
|
201
|
+
return self._envelope({"targets": target_list, "tests": tests, "blast_radius": impact})
|
|
202
|
+
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _match(graph: CodeGraph, query: str) -> list[GraphNode]:
|
|
205
|
+
normalized = query.replace("\\", "/").lower()
|
|
206
|
+
exact: list[GraphNode] = []
|
|
207
|
+
partial: list[GraphNode] = []
|
|
208
|
+
for node in graph.nodes:
|
|
209
|
+
values = (node.id.lower(), node.path.lower(), node.name.lower())
|
|
210
|
+
if normalized in values or node.id.lower().endswith(f"::{normalized}"):
|
|
211
|
+
exact.append(node)
|
|
212
|
+
elif any(normalized in value for value in values):
|
|
213
|
+
partial.append(node)
|
|
214
|
+
return exact + partial
|
|
215
|
+
|
|
216
|
+
@staticmethod
|
|
217
|
+
def _relations(edges: Iterable[GraphEdge]) -> tuple[dict[str, list[GraphEdge]], dict[str, list[GraphEdge]]]:
|
|
218
|
+
inbound: dict[str, list[GraphEdge]] = defaultdict(list)
|
|
219
|
+
outbound: dict[str, list[GraphEdge]] = defaultdict(list)
|
|
220
|
+
for edge in edges:
|
|
221
|
+
inbound[edge.target].append(edge)
|
|
222
|
+
outbound[edge.source].append(edge)
|
|
223
|
+
return inbound, outbound
|
|
224
|
+
|
|
225
|
+
@staticmethod
|
|
226
|
+
def _edge_dict(edge: GraphEdge | None) -> dict[str, Any]:
|
|
227
|
+
if edge is None:
|
|
228
|
+
return {}
|
|
229
|
+
confidence = edge.confidence.value if hasattr(edge.confidence, "value") else str(edge.confidence)
|
|
230
|
+
return {
|
|
231
|
+
"source": edge.source,
|
|
232
|
+
"target": edge.target,
|
|
233
|
+
"kind": edge.kind,
|
|
234
|
+
"confidence": confidence,
|
|
235
|
+
"confidence_score": edge.extras.get("confidence_score", {"extracted": 1.0, "inferred": 0.7, "ambiguous": 0.4}.get(confidence, 0.4)),
|
|
236
|
+
"provenance": edge.extras.get("provenance", "extracted" if confidence == "extracted" else "inferred"),
|
|
237
|
+
"reason": edge.reason,
|
|
238
|
+
"evidence": edge.extras.get("evidence", []),
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
@staticmethod
|
|
242
|
+
def _snippet(content: bytes | None, start: int, end: int) -> str:
|
|
243
|
+
if content is None:
|
|
244
|
+
return ""
|
|
245
|
+
lines = content.decode("utf-8", errors="replace").splitlines()
|
|
246
|
+
if start <= 0:
|
|
247
|
+
return ""
|
|
248
|
+
final = end if end >= start else start
|
|
249
|
+
return "\n".join(f"{line_no}: {lines[line_no - 1]}" for line_no in range(start, min(final, len(lines)) + 1))
|
|
250
|
+
|
|
251
|
+
def _impact_for_ids(self, graph: CodeGraph, ids: Iterable[str], *, max_depth: int) -> dict[str, Any]:
|
|
252
|
+
seeds = list(dict.fromkeys(ids))
|
|
253
|
+
inbound, _ = self._relations(graph.edges)
|
|
254
|
+
visited = set(seeds)
|
|
255
|
+
frontier = set(seeds)
|
|
256
|
+
layers: list[dict[str, Any]] = []
|
|
257
|
+
for depth in range(1, max(1, min(8, max_depth)) + 1):
|
|
258
|
+
next_frontier: set[str] = set()
|
|
259
|
+
confidences: list[str] = []
|
|
260
|
+
for node_id in frontier:
|
|
261
|
+
for edge in inbound.get(node_id, []):
|
|
262
|
+
if edge.source in visited:
|
|
263
|
+
continue
|
|
264
|
+
next_frontier.add(edge.source)
|
|
265
|
+
confidence = edge.confidence.value if hasattr(edge.confidence, "value") else str(edge.confidence)
|
|
266
|
+
confidences.append(confidence)
|
|
267
|
+
visited.update(next_frontier)
|
|
268
|
+
layers.append({
|
|
269
|
+
"depth": depth,
|
|
270
|
+
"nodes": sorted(next_frontier),
|
|
271
|
+
"confidence": self._lowest_confidence(confidences),
|
|
272
|
+
"count": len(next_frontier),
|
|
273
|
+
})
|
|
274
|
+
frontier = next_frontier
|
|
275
|
+
if not frontier:
|
|
276
|
+
break
|
|
277
|
+
return {"seeds": seeds, "layers": layers, "total_impacted": len(visited - set(seeds))}
|
|
278
|
+
|
|
279
|
+
@staticmethod
|
|
280
|
+
def _lowest_confidence(values: Iterable[str]) -> str:
|
|
281
|
+
ranks = {"extracted": 2, "inferred": 1, "ambiguous": 0}
|
|
282
|
+
values = list(values)
|
|
283
|
+
return min(values, key=lambda value: ranks.get(value, 0)) if values else "extracted"
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def _is_test_path(path: str) -> bool:
|
|
287
|
+
normalized = f"/{path.lower().replace('\\', '/')}"
|
|
288
|
+
name = Path(path).name.lower()
|
|
289
|
+
return "/test" in normalized or "/tests/" in normalized or name.startswith("test_") or ".test." in name or ".spec." in name
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"""Semantic synthesis beyond direct syntax edges."""
|
|
2
|
+
|
|
3
|
+
from devcouncil.codeintel.resolution.abstract_state import AbstractState, AbstractValue
|
|
4
|
+
from devcouncil.codeintel.resolution.semantic import enrich_semantic_edges
|
|
5
|
+
|
|
6
|
+
__all__ = ["AbstractState", "AbstractValue", "enrich_semantic_edges"]
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Bounded abstract values for computed names, imports, and paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ast
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Iterable, Mapping
|
|
10
|
+
|
|
11
|
+
MAX_STRINGS = 32
|
|
12
|
+
MAX_CALLABLES = 32
|
|
13
|
+
MAX_TYPES = 16
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AbstractValue:
|
|
18
|
+
scalars: frozenset[str | int | float | bool | None] = frozenset()
|
|
19
|
+
callables: frozenset[str] = frozenset()
|
|
20
|
+
types: frozenset[str] = frozenset()
|
|
21
|
+
unknown: bool = False
|
|
22
|
+
|
|
23
|
+
@classmethod
|
|
24
|
+
def unknown_value(cls) -> "AbstractValue":
|
|
25
|
+
return cls(unknown=True)
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def scalar(cls, value: str | int | float | bool | None) -> "AbstractValue":
|
|
29
|
+
return cls(scalars=frozenset({value}))
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def callable(cls, name: str) -> "AbstractValue":
|
|
33
|
+
return cls(callables=frozenset({name}))
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def type_name(cls, name: str) -> "AbstractValue":
|
|
37
|
+
return cls(types=frozenset({name}))
|
|
38
|
+
|
|
39
|
+
def merge(self, other: "AbstractValue") -> "AbstractValue":
|
|
40
|
+
scalars = self.scalars | other.scalars
|
|
41
|
+
callables = self.callables | other.callables
|
|
42
|
+
types = self.types | other.types
|
|
43
|
+
overflow = len(scalars) > MAX_STRINGS or len(callables) > MAX_CALLABLES or len(types) > MAX_TYPES
|
|
44
|
+
if overflow:
|
|
45
|
+
return AbstractValue.unknown_value()
|
|
46
|
+
return AbstractValue(scalars, callables, types, self.unknown or other.unknown)
|
|
47
|
+
|
|
48
|
+
def strings(self) -> frozenset[str]:
|
|
49
|
+
return frozenset(value for value in self.scalars if isinstance(value, str))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class AbstractState:
|
|
54
|
+
values: dict[str, AbstractValue] = field(default_factory=dict)
|
|
55
|
+
functions: dict[str, tuple[tuple[str, ...], str]] = field(default_factory=dict)
|
|
56
|
+
|
|
57
|
+
def assign(self, name: str, value: AbstractValue) -> None:
|
|
58
|
+
self.values[name] = value
|
|
59
|
+
|
|
60
|
+
def get(self, name: str) -> AbstractValue:
|
|
61
|
+
return self.values.get(name, AbstractValue.unknown_value())
|
|
62
|
+
|
|
63
|
+
def analyze(
|
|
64
|
+
self,
|
|
65
|
+
source: str,
|
|
66
|
+
*,
|
|
67
|
+
callable_names: Iterable[str] = (),
|
|
68
|
+
type_names: Iterable[str] = (),
|
|
69
|
+
callable_aliases: Mapping[str, str] | None = None,
|
|
70
|
+
type_aliases: Mapping[str, str] | None = None,
|
|
71
|
+
) -> "AbstractState":
|
|
72
|
+
"""Collect bounded constants, aliases, object properties, and wrappers."""
|
|
73
|
+
for name in callable_names:
|
|
74
|
+
self.assign(name, AbstractValue.callable(name))
|
|
75
|
+
for name in type_names:
|
|
76
|
+
self.assign(name, AbstractValue.type_name(name))
|
|
77
|
+
for local, remote in (callable_aliases or {}).items():
|
|
78
|
+
self.assign(local, AbstractValue.callable(remote))
|
|
79
|
+
for local, remote in (type_aliases or {}).items():
|
|
80
|
+
self.assign(local, AbstractValue.type_name(remote))
|
|
81
|
+
|
|
82
|
+
for match in re.finditer(
|
|
83
|
+
r"^\s*function\s+([A-Za-z_$][\w$]*)\s*\(([^)]*)\)\s*"
|
|
84
|
+
r"\{\s*return\s+(.+)\s*;\s*\}\s*$",
|
|
85
|
+
source,
|
|
86
|
+
re.MULTILINE,
|
|
87
|
+
):
|
|
88
|
+
params = tuple(
|
|
89
|
+
value.strip()
|
|
90
|
+
for value in match.group(2).split(",")
|
|
91
|
+
if value.strip()
|
|
92
|
+
)
|
|
93
|
+
self.functions[match.group(1)] = (params, match.group(3).rstrip("; "))
|
|
94
|
+
|
|
95
|
+
for line in source.splitlines():
|
|
96
|
+
assignment_match = re.match(
|
|
97
|
+
r"^\s*(?:(?:const|let|var)\s+)?"
|
|
98
|
+
r"([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)?)\s*=\s*(.+?)\s*;?\s*$",
|
|
99
|
+
line,
|
|
100
|
+
)
|
|
101
|
+
if assignment_match is None or "==" in line or "=>" in line:
|
|
102
|
+
continue
|
|
103
|
+
name, expression = assignment_match.groups()
|
|
104
|
+
expression = expression.rstrip("; ")
|
|
105
|
+
if expression.startswith("{") and expression.endswith("}"):
|
|
106
|
+
for key, value in re.findall(
|
|
107
|
+
r"([A-Za-z_$][\w$]*)\s*:\s*([^,}]+)", expression
|
|
108
|
+
):
|
|
109
|
+
self.assign(f"{name}.{key}", self.evaluate(value.strip()))
|
|
110
|
+
continue
|
|
111
|
+
self.assign(name, self.evaluate(expression))
|
|
112
|
+
return self
|
|
113
|
+
|
|
114
|
+
def evaluate(self, expression: str) -> AbstractValue:
|
|
115
|
+
"""Evaluate Python or JS-like scalar/callable/type expressions."""
|
|
116
|
+
expression = expression.strip().rstrip(";")
|
|
117
|
+
python_value = self.evaluate_python(expression)
|
|
118
|
+
if not python_value.unknown:
|
|
119
|
+
return python_value
|
|
120
|
+
return self._eval_text(expression)
|
|
121
|
+
|
|
122
|
+
def evaluate_python(self, expression: str | ast.AST) -> AbstractValue:
|
|
123
|
+
try:
|
|
124
|
+
node = ast.parse(expression, mode="eval").body if isinstance(expression, str) else expression
|
|
125
|
+
except SyntaxError:
|
|
126
|
+
return AbstractValue.unknown_value()
|
|
127
|
+
return self._eval(node)
|
|
128
|
+
|
|
129
|
+
def _eval(self, node: ast.AST) -> AbstractValue:
|
|
130
|
+
if isinstance(node, ast.Constant) and isinstance(node.value, (str, int, float, bool, type(None))):
|
|
131
|
+
return AbstractValue.scalar(node.value)
|
|
132
|
+
if isinstance(node, ast.Name):
|
|
133
|
+
return self.get(node.id)
|
|
134
|
+
if isinstance(node, ast.IfExp):
|
|
135
|
+
return self._eval(node.body).merge(self._eval(node.orelse))
|
|
136
|
+
if isinstance(node, ast.BinOp) and isinstance(node.op, ast.Add):
|
|
137
|
+
return self._combine_strings(self._eval(node.left).strings(), self._eval(node.right).strings(), "")
|
|
138
|
+
if isinstance(node, ast.JoinedStr):
|
|
139
|
+
choices: set[str] = {""}
|
|
140
|
+
for part in node.values:
|
|
141
|
+
if isinstance(part, ast.Constant) and isinstance(part.value, str):
|
|
142
|
+
values = {part.value}
|
|
143
|
+
elif isinstance(part, ast.FormattedValue):
|
|
144
|
+
values = set(self._eval(part.value).strings())
|
|
145
|
+
else:
|
|
146
|
+
return AbstractValue.unknown_value()
|
|
147
|
+
choices = {left + right for left in choices for right in values}
|
|
148
|
+
if len(choices) > MAX_STRINGS:
|
|
149
|
+
return AbstractValue.unknown_value()
|
|
150
|
+
return AbstractValue(scalars=frozenset(choices))
|
|
151
|
+
if isinstance(node, (ast.Tuple, ast.List, ast.Set)):
|
|
152
|
+
value = AbstractValue()
|
|
153
|
+
for item in node.elts:
|
|
154
|
+
value = value.merge(self._eval(item))
|
|
155
|
+
return value
|
|
156
|
+
if isinstance(node, ast.Call):
|
|
157
|
+
name = self._call_name(node.func)
|
|
158
|
+
if name in {"str", "os.fspath"} and node.args:
|
|
159
|
+
return self._eval(node.args[0])
|
|
160
|
+
if name in {"os.path.join", "pathlib.Path", "Path"}:
|
|
161
|
+
parts = [self._eval(arg).strings() for arg in node.args]
|
|
162
|
+
if not parts or any(not part for part in parts):
|
|
163
|
+
return AbstractValue.unknown_value()
|
|
164
|
+
path_values: set[str] = {""}
|
|
165
|
+
for path_part in parts:
|
|
166
|
+
path_values = {
|
|
167
|
+
os.path.join(left, right) if left else right
|
|
168
|
+
for left in path_values
|
|
169
|
+
for right in path_part
|
|
170
|
+
}
|
|
171
|
+
if len(path_values) > MAX_STRINGS:
|
|
172
|
+
return AbstractValue.unknown_value()
|
|
173
|
+
return AbstractValue(scalars=frozenset(value.replace("\\", "/") for value in path_values))
|
|
174
|
+
return AbstractValue.unknown_value()
|
|
175
|
+
|
|
176
|
+
def _eval_text(self, expression: str) -> AbstractValue:
|
|
177
|
+
expression = expression.strip().rstrip(";")
|
|
178
|
+
while expression.startswith("(") and expression.endswith(")"):
|
|
179
|
+
expression = expression[1:-1].strip()
|
|
180
|
+
if not expression:
|
|
181
|
+
return AbstractValue.unknown_value()
|
|
182
|
+
if re.fullmatch(r"[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*", expression):
|
|
183
|
+
return self.get(expression)
|
|
184
|
+
if (
|
|
185
|
+
len(expression) >= 2
|
|
186
|
+
and expression[0] == expression[-1]
|
|
187
|
+
and expression[0] in {"'", '"'}
|
|
188
|
+
):
|
|
189
|
+
try:
|
|
190
|
+
return AbstractValue.scalar(ast.literal_eval(expression))
|
|
191
|
+
except (SyntaxError, ValueError):
|
|
192
|
+
return AbstractValue.unknown_value()
|
|
193
|
+
if expression.startswith("`") and expression.endswith("`"):
|
|
194
|
+
values = {expression[1:-1]}
|
|
195
|
+
for nested in re.findall(r"\$\{([^}]+)\}", expression):
|
|
196
|
+
replacements = self.evaluate(nested).strings()
|
|
197
|
+
if not replacements:
|
|
198
|
+
return AbstractValue.unknown_value()
|
|
199
|
+
values = {
|
|
200
|
+
value.replace(f"${{{nested}}}", replacement)
|
|
201
|
+
for value in values
|
|
202
|
+
for replacement in replacements
|
|
203
|
+
}
|
|
204
|
+
if len(values) > MAX_STRINGS:
|
|
205
|
+
return AbstractValue.unknown_value()
|
|
206
|
+
return AbstractValue(scalars=frozenset(values))
|
|
207
|
+
conditional = re.match(r"^.+?\?\s*(.+?)\s*:\s*(.+)$", expression)
|
|
208
|
+
if conditional is not None:
|
|
209
|
+
return self.evaluate(conditional.group(1)).merge(
|
|
210
|
+
self.evaluate(conditional.group(2))
|
|
211
|
+
)
|
|
212
|
+
parts = self._split_top_level(expression, "+")
|
|
213
|
+
if len(parts) > 1:
|
|
214
|
+
value = self.evaluate(parts[0])
|
|
215
|
+
for part in parts[1:]:
|
|
216
|
+
value = self._combine_strings(
|
|
217
|
+
value.strings(), self.evaluate(part).strings(), ""
|
|
218
|
+
)
|
|
219
|
+
return value
|
|
220
|
+
if expression.startswith("[") and expression.endswith("]"):
|
|
221
|
+
value = AbstractValue()
|
|
222
|
+
for part in self._split_top_level(expression[1:-1], ","):
|
|
223
|
+
value = value.merge(self.evaluate(part))
|
|
224
|
+
return value
|
|
225
|
+
call = re.match(r"^([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\((.*)\)$", expression)
|
|
226
|
+
if call is not None:
|
|
227
|
+
name, raw_args = call.groups()
|
|
228
|
+
args = [self.evaluate(part) for part in self._split_top_level(raw_args, ",")]
|
|
229
|
+
if name in {"path.join", "path.resolve", "os.path.join", "Path", "pathlib.Path"}:
|
|
230
|
+
choices = [arg.strings() for arg in args]
|
|
231
|
+
if not choices or any(not choice for choice in choices):
|
|
232
|
+
return AbstractValue.unknown_value()
|
|
233
|
+
values = {""}
|
|
234
|
+
for choice in choices:
|
|
235
|
+
values = {
|
|
236
|
+
os.path.join(left, right) if left else right
|
|
237
|
+
for left in values
|
|
238
|
+
for right in choice
|
|
239
|
+
}
|
|
240
|
+
if len(values) > MAX_STRINGS:
|
|
241
|
+
return AbstractValue.unknown_value()
|
|
242
|
+
return AbstractValue(
|
|
243
|
+
scalars=frozenset(value.replace("\\", "/") for value in values)
|
|
244
|
+
)
|
|
245
|
+
bound = self.get(name)
|
|
246
|
+
if bound.types or bound.callables:
|
|
247
|
+
return bound
|
|
248
|
+
wrapper = self.functions.get(name)
|
|
249
|
+
if wrapper is not None:
|
|
250
|
+
params, body = wrapper
|
|
251
|
+
previous = {param: self.values.get(param) for param in params}
|
|
252
|
+
try:
|
|
253
|
+
for param, value in zip(params, args):
|
|
254
|
+
self.assign(param, value)
|
|
255
|
+
return self.evaluate(body)
|
|
256
|
+
finally:
|
|
257
|
+
for param, previous_value in previous.items():
|
|
258
|
+
if previous_value is None:
|
|
259
|
+
self.values.pop(param, None)
|
|
260
|
+
else:
|
|
261
|
+
self.values[param] = previous_value
|
|
262
|
+
return AbstractValue.unknown_value()
|
|
263
|
+
|
|
264
|
+
@staticmethod
|
|
265
|
+
def _split_top_level(expression: str, separator: str) -> list[str]:
|
|
266
|
+
parts: list[str] = []
|
|
267
|
+
start = 0
|
|
268
|
+
depth = 0
|
|
269
|
+
quote = ""
|
|
270
|
+
for index, char in enumerate(expression):
|
|
271
|
+
if quote:
|
|
272
|
+
if char == quote and (index == 0 or expression[index - 1] != "\\"):
|
|
273
|
+
quote = ""
|
|
274
|
+
continue
|
|
275
|
+
if char in {"'", '"', "`"}:
|
|
276
|
+
quote = char
|
|
277
|
+
elif char in "([{":
|
|
278
|
+
depth += 1
|
|
279
|
+
elif char in ")]}":
|
|
280
|
+
depth = max(0, depth - 1)
|
|
281
|
+
elif char == separator and depth == 0:
|
|
282
|
+
parts.append(expression[start:index].strip())
|
|
283
|
+
start = index + 1
|
|
284
|
+
parts.append(expression[start:].strip())
|
|
285
|
+
return [part for part in parts if part]
|
|
286
|
+
|
|
287
|
+
@staticmethod
|
|
288
|
+
def _call_name(node: ast.AST) -> str:
|
|
289
|
+
if isinstance(node, ast.Name):
|
|
290
|
+
return node.id
|
|
291
|
+
if isinstance(node, ast.Attribute):
|
|
292
|
+
prefix = AbstractState._call_name(node.value)
|
|
293
|
+
return f"{prefix}.{node.attr}" if prefix else node.attr
|
|
294
|
+
return ""
|
|
295
|
+
|
|
296
|
+
@staticmethod
|
|
297
|
+
def _combine_strings(left: Iterable[str], right: Iterable[str], separator: str) -> AbstractValue:
|
|
298
|
+
values = {f"{a}{separator}{b}" for a in left for b in right}
|
|
299
|
+
if not values or len(values) > MAX_STRINGS:
|
|
300
|
+
return AbstractValue.unknown_value()
|
|
301
|
+
return AbstractValue(scalars=frozenset(values))
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Manifest for independently testable framework semantic augmenters."""
|
|
2
|
+
|
|
3
|
+
from devcouncil.codeintel.resolution.frameworks.base import FrameworkSpec
|
|
4
|
+
from devcouncil.codeintel.resolution.frameworks.di import (
|
|
5
|
+
DI_SPECS,
|
|
6
|
+
iter_provider_matches,
|
|
7
|
+
)
|
|
8
|
+
from devcouncil.codeintel.resolution.frameworks.events import (
|
|
9
|
+
EVENT_SPECS,
|
|
10
|
+
iter_event_matches,
|
|
11
|
+
)
|
|
12
|
+
from devcouncil.codeintel.resolution.frameworks.routes import (
|
|
13
|
+
COMPUTED_ROUTE_PATTERN,
|
|
14
|
+
ROUTE_SPECS,
|
|
15
|
+
iter_route_matches,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
FRAMEWORK_MANIFEST: tuple[FrameworkSpec, ...] = (
|
|
19
|
+
*ROUTE_SPECS,
|
|
20
|
+
*DI_SPECS,
|
|
21
|
+
*EVENT_SPECS,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"COMPUTED_ROUTE_PATTERN",
|
|
26
|
+
"DI_SPECS",
|
|
27
|
+
"EVENT_SPECS",
|
|
28
|
+
"FRAMEWORK_MANIFEST",
|
|
29
|
+
"ROUTE_SPECS",
|
|
30
|
+
"iter_event_matches",
|
|
31
|
+
"iter_provider_matches",
|
|
32
|
+
"iter_route_matches",
|
|
33
|
+
]
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Shared records for isolated framework semantic augmenters."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Pattern
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(frozen=True)
|
|
11
|
+
class FrameworkSpec:
|
|
12
|
+
"""A framework matcher advertised by the semantic resolver."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
family: str
|
|
16
|
+
pattern: Pattern[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class RouteMatch:
|
|
21
|
+
framework: str
|
|
22
|
+
verb: str
|
|
23
|
+
path: str
|
|
24
|
+
handler_expression: str
|
|
25
|
+
start: int
|
|
26
|
+
end: int
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class EventMatch:
|
|
31
|
+
operation: str
|
|
32
|
+
event: str
|
|
33
|
+
callback_expression: str
|
|
34
|
+
start: int
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ProviderMatch:
|
|
39
|
+
framework: str
|
|
40
|
+
target_expression: str
|
|
41
|
+
start: int
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def compile_pattern(pattern: str, *, multiline: bool = False) -> Pattern[str]:
|
|
45
|
+
flags = re.IGNORECASE | (re.MULTILINE if multiline else 0)
|
|
46
|
+
return re.compile(pattern, flags)
|