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
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
"""Semantic snapshots and diff classification.
|
|
1
|
+
"""Semantic snapshots and diff classification.
|
|
2
|
+
|
|
3
|
+
NOTE (Phase 4 legacy audit): task-scoped semantic diffs for verification — distinct
|
|
4
|
+
from the code symbol graph in ``indexing/graph/``. Keep; not overlapping dead-code.
|
|
5
|
+
"""
|
|
2
6
|
|
|
3
7
|
from __future__ import annotations
|
|
4
8
|
|
|
@@ -11,8 +15,10 @@ from pathlib import Path
|
|
|
11
15
|
|
|
12
16
|
from devcouncil.indexing.ast_matcher import AstMatcher
|
|
13
17
|
from devcouncil.indexing.lsp import LspInspector
|
|
18
|
+
from devcouncil.indexing.walk import iter_project_files
|
|
14
19
|
from devcouncil.storage.db import get_db
|
|
15
20
|
from devcouncil.storage.native import SemanticDiffRepository
|
|
21
|
+
from devcouncil.utils.json_persist import read_json, write_json
|
|
16
22
|
|
|
17
23
|
_CONFIG_FILES = {
|
|
18
24
|
"pyproject.toml",
|
|
@@ -39,7 +45,7 @@ class SemanticIndex:
|
|
|
39
45
|
# matching, source-file hashing, import extraction, and LSP language detection);
|
|
40
46
|
# each collector now filters this single in-memory list with its own predicate, so
|
|
41
47
|
# the on-disk output is unchanged while the filesystem is walked a single time.
|
|
42
|
-
all_files =
|
|
48
|
+
all_files = list(iter_project_files(self.project_root))
|
|
43
49
|
rel_files = [str(path.relative_to(self.project_root)) for path in all_files]
|
|
44
50
|
symbols = self._collect_symbols(all_files)
|
|
45
51
|
source_files, imports = self._collect_source_data(all_files)
|
|
@@ -56,7 +62,7 @@ class SemanticIndex:
|
|
|
56
62
|
}
|
|
57
63
|
path = self.snapshot_path(task_id, stage)
|
|
58
64
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
-
path
|
|
65
|
+
write_json(path, payload)
|
|
60
66
|
return path
|
|
61
67
|
|
|
62
68
|
def diff(self, task_id: str) -> dict:
|
|
@@ -64,8 +70,8 @@ class SemanticIndex:
|
|
|
64
70
|
after_path = self.snapshot_path(task_id, "after")
|
|
65
71
|
if not after_path.exists():
|
|
66
72
|
self.create_snapshot(task_id, "after")
|
|
67
|
-
before =
|
|
68
|
-
after =
|
|
73
|
+
before = read_json(before_path) if before_path.exists() else {}
|
|
74
|
+
after = read_json(after_path) if after_path.exists() else {}
|
|
69
75
|
classifications = self._classify(before, after)
|
|
70
76
|
summary = ", ".join(item["type"] for item in classifications) or "no semantic changes"
|
|
71
77
|
db = get_db(self.project_root)
|
|
@@ -107,7 +113,7 @@ class SemanticIndex:
|
|
|
107
113
|
exactly — source entries are still sorted by path, imports stay in traversal order.
|
|
108
114
|
"""
|
|
109
115
|
if files is None:
|
|
110
|
-
files =
|
|
116
|
+
files = list(iter_project_files(self.project_root))
|
|
111
117
|
source_entries: list[dict] = []
|
|
112
118
|
imports: list[dict] = []
|
|
113
119
|
for path in files:
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Pure helpers for reasoning about subsystem areas from a loaded ``repo_map.json``.
|
|
2
|
+
|
|
3
|
+
Shared by the prompt builder's impact block and the subsystem-boundary verification
|
|
4
|
+
gate so both agree on how a path maps to an area, which area is which area's neighbor,
|
|
5
|
+
and what a change "touches". Dependency-free and side-effect-free: everything takes an
|
|
6
|
+
already-parsed repo-map dict (or its ``subsystems``/``dependents`` slices) and returns
|
|
7
|
+
plain data. Never raises on malformed input — a missing/odd map degrades to "unknown".
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Iterable, Mapping, Sequence
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _norm(path: str) -> str:
|
|
16
|
+
return str(path).replace("\\", "/")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def subsystem_areas(data: Mapping | None) -> list[str]:
|
|
20
|
+
"""All subsystem ``area`` prefixes declared in the map (longest first)."""
|
|
21
|
+
subs = (data or {}).get("subsystems") or []
|
|
22
|
+
areas = [str(s.get("area")) for s in subs if isinstance(s, dict) and s.get("area")]
|
|
23
|
+
return sorted(set(areas), key=len, reverse=True)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def area_for_path(path: str, data: Mapping | None) -> str | None:
|
|
27
|
+
"""The subsystem area a path belongs to (longest matching area prefix), or ``None``.
|
|
28
|
+
|
|
29
|
+
Falls back to the per-file ``area`` recorded in the map's ``files`` list when no
|
|
30
|
+
subsystem prefix matches, so files outside a declared subsystem still resolve.
|
|
31
|
+
"""
|
|
32
|
+
norm = _norm(path)
|
|
33
|
+
for area in subsystem_areas(data):
|
|
34
|
+
if norm == area or norm.startswith(area + "/"):
|
|
35
|
+
return area
|
|
36
|
+
files = (data or {}).get("files") or []
|
|
37
|
+
for entry in files:
|
|
38
|
+
if isinstance(entry, dict) and _norm(entry.get("path", "")) == norm:
|
|
39
|
+
file_area = entry.get("area")
|
|
40
|
+
return str(file_area) if file_area else None
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def neighbors_for_area(area: str | None, data: Mapping | None) -> list[str]:
|
|
45
|
+
"""Declared neighboring areas for ``area`` (empty when unknown)."""
|
|
46
|
+
if not area:
|
|
47
|
+
return []
|
|
48
|
+
for sub in (data or {}).get("subsystems") or []:
|
|
49
|
+
if isinstance(sub, dict) and str(sub.get("area")) == area:
|
|
50
|
+
return [str(n) for n in (sub.get("neighbors") or [])]
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def are_neighbors(area_a: str | None, area_b: str | None, data: Mapping | None) -> bool:
|
|
55
|
+
"""True when the two areas are the same or declared neighbors (in either direction)."""
|
|
56
|
+
if not area_a or not area_b:
|
|
57
|
+
return True # unknown side → don't flag
|
|
58
|
+
if area_a == area_b:
|
|
59
|
+
return True
|
|
60
|
+
if area_b in neighbors_for_area(area_a, data):
|
|
61
|
+
return True
|
|
62
|
+
if area_a in neighbors_for_area(area_b, data):
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def dependents_of(path: str, data: Mapping | None) -> list[str]:
|
|
68
|
+
"""Files that import ``path`` (reverse-import blast radius), from the map.
|
|
69
|
+
|
|
70
|
+
The list may be truncated by the mapper's ``_DEPENDENTS_MAX``; see
|
|
71
|
+
:func:`dependents_total_of` for the full importer count when truncated.
|
|
72
|
+
"""
|
|
73
|
+
dependents = (data or {}).get("dependents") or {}
|
|
74
|
+
if not isinstance(dependents, dict):
|
|
75
|
+
return []
|
|
76
|
+
return [str(p) for p in (dependents.get(_norm(path)) or [])]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dependents_total_of(path: str, data: Mapping | None) -> int | None:
|
|
80
|
+
"""Full importer count when ``dependents`` was truncated; else ``None``.
|
|
81
|
+
|
|
82
|
+
When this returns an int greater than ``len(dependents_of(...))``, the listed
|
|
83
|
+
dependents are a sample — not the complete blast radius.
|
|
84
|
+
"""
|
|
85
|
+
totals = (data or {}).get("dependents_total") or {}
|
|
86
|
+
if not isinstance(totals, dict):
|
|
87
|
+
return None
|
|
88
|
+
raw = totals.get(_norm(path))
|
|
89
|
+
if raw is None:
|
|
90
|
+
return None
|
|
91
|
+
try:
|
|
92
|
+
n = int(raw)
|
|
93
|
+
except (TypeError, ValueError):
|
|
94
|
+
return None
|
|
95
|
+
return n if n > 0 else None
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def unwired_candidates_of(data: Mapping | None) -> list[str]:
|
|
99
|
+
"""Files with zero inbound edges that aren't entry roots/exempt (capped list)."""
|
|
100
|
+
vals = (data or {}).get("unwired_candidates") or []
|
|
101
|
+
if not isinstance(vals, list):
|
|
102
|
+
return []
|
|
103
|
+
return [str(p) for p in vals]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def unreachable_of(data: Mapping | None) -> list[str]:
|
|
107
|
+
"""Files not reachable by BFS from any entry root (capped list)."""
|
|
108
|
+
vals = (data or {}).get("unreachable_files") or []
|
|
109
|
+
if not isinstance(vals, list):
|
|
110
|
+
return []
|
|
111
|
+
return [str(p) for p in vals]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def dead_symbol_candidates_of(data: Mapping | None) -> list[str]:
|
|
115
|
+
"""``path:line name`` entries for unused public top-level symbols (capped)."""
|
|
116
|
+
vals = (data or {}).get("dead_symbol_candidates") or []
|
|
117
|
+
if not isinstance(vals, list):
|
|
118
|
+
return []
|
|
119
|
+
return [str(p) for p in vals]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def is_entry_root(path: str, data: Mapping | None) -> bool:
|
|
123
|
+
"""True when ``path`` is listed in the map's ``entry_roots``."""
|
|
124
|
+
roots = (data or {}).get("entry_roots") or []
|
|
125
|
+
if not isinstance(roots, list):
|
|
126
|
+
return False
|
|
127
|
+
norm = _norm(path)
|
|
128
|
+
return any(_norm(str(r)) == norm for r in roots)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def areas_touched(paths: Iterable[str], data: Mapping | None) -> list[str]:
|
|
132
|
+
"""The distinct subsystem areas the given paths live in (sorted)."""
|
|
133
|
+
seen: set[str] = set()
|
|
134
|
+
for path in paths:
|
|
135
|
+
area = area_for_path(path, data)
|
|
136
|
+
if area:
|
|
137
|
+
seen.add(area)
|
|
138
|
+
return sorted(seen)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def impact_targets(path: str, data: Mapping | None) -> tuple[list[str], list[str]]:
|
|
142
|
+
"""Return ``(dependent_files, neighbor_areas)`` a change to ``path`` touches."""
|
|
143
|
+
area = area_for_path(path, data)
|
|
144
|
+
return dependents_of(path, data), neighbors_for_area(area, data)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def cross_boundary_pairs(
|
|
148
|
+
paths: Sequence[str],
|
|
149
|
+
data: Mapping | None,
|
|
150
|
+
) -> list[tuple[str, str]]:
|
|
151
|
+
"""Pairs of touched areas that are neither identical nor declared neighbors.
|
|
152
|
+
|
|
153
|
+
Each pair ``(a, b)`` is emitted once (sorted) and signals an edit that spans two
|
|
154
|
+
subsystems the map does not consider adjacent — a candidate architecture-drift
|
|
155
|
+
boundary crossing.
|
|
156
|
+
"""
|
|
157
|
+
areas = areas_touched(paths, data)
|
|
158
|
+
crossings: set[tuple[str, str]] = set()
|
|
159
|
+
for i, area_a in enumerate(areas):
|
|
160
|
+
for area_b in areas[i + 1:]:
|
|
161
|
+
if not are_neighbors(area_a, area_b, data):
|
|
162
|
+
crossings.add(tuple(sorted((area_a, area_b)))) # type: ignore[arg-type]
|
|
163
|
+
return sorted(crossings)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"""Tree-sitter import / symbol extraction for Rust, Go, and JS/TS.
|
|
2
|
+
|
|
3
|
+
``tree-sitter`` and ``tree-sitter-language-pack`` are core dependencies. Helpers
|
|
4
|
+
still return ``None`` on parse failure so callers can keep regex/AST fallbacks.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from typing import Iterable, List, Optional, Sequence, Tuple
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# language name for tree-sitter-language-pack → file suffixes that use it
|
|
16
|
+
_LANG_BY_SUFFIX = {
|
|
17
|
+
".rs": "rust",
|
|
18
|
+
".go": "go",
|
|
19
|
+
".ts": "typescript",
|
|
20
|
+
".tsx": "tsx",
|
|
21
|
+
".js": "javascript",
|
|
22
|
+
".jsx": "javascript",
|
|
23
|
+
".mjs": "javascript",
|
|
24
|
+
".cjs": "javascript",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_SymbolHit = Tuple[str, str, int, str] # kind, name, line, text
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def tree_sitter_available() -> bool:
|
|
31
|
+
"""True when both the core binding and language pack import cleanly."""
|
|
32
|
+
try:
|
|
33
|
+
import tree_sitter # noqa: F401
|
|
34
|
+
from tree_sitter_language_pack import get_parser # noqa: F401
|
|
35
|
+
except Exception:
|
|
36
|
+
return False
|
|
37
|
+
return True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@lru_cache(maxsize=16)
|
|
41
|
+
def _parser_for(language: str):
|
|
42
|
+
if not tree_sitter_available():
|
|
43
|
+
return None
|
|
44
|
+
try:
|
|
45
|
+
from tree_sitter_language_pack import get_parser
|
|
46
|
+
|
|
47
|
+
return get_parser(language)
|
|
48
|
+
except Exception:
|
|
49
|
+
logger.debug("tree-sitter parser unavailable for %s", language, exc_info=True)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse(language: str, source: bytes):
|
|
54
|
+
parser = _parser_for(language)
|
|
55
|
+
if parser is None:
|
|
56
|
+
return None
|
|
57
|
+
try:
|
|
58
|
+
return parser.parse(source)
|
|
59
|
+
except Exception:
|
|
60
|
+
logger.debug("tree-sitter parse failed for %s", language, exc_info=True)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _node_text(node, source: bytes) -> str:
|
|
65
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _walk(node) -> Iterable:
|
|
69
|
+
yield node
|
|
70
|
+
for child in node.children:
|
|
71
|
+
yield from _walk(child)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# ---------------------------------------------------------------------------
|
|
75
|
+
# Go imports
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def extract_go_import_specs(source: str | bytes) -> Optional[List[str]]:
|
|
80
|
+
"""File-level Go import path strings, or None when tree-sitter is unavailable."""
|
|
81
|
+
if not tree_sitter_available():
|
|
82
|
+
return None
|
|
83
|
+
raw = source.encode("utf-8") if isinstance(source, str) else source
|
|
84
|
+
tree = _parse("go", raw)
|
|
85
|
+
if tree is None:
|
|
86
|
+
return None
|
|
87
|
+
specs: List[str] = []
|
|
88
|
+
for node in _walk(tree.root_node):
|
|
89
|
+
if node.type != "interpreted_string_literal":
|
|
90
|
+
continue
|
|
91
|
+
# Only count string literals under import_spec.
|
|
92
|
+
parent = node.parent
|
|
93
|
+
while parent is not None and parent.type not in {"import_spec", "source_file"}:
|
|
94
|
+
parent = parent.parent
|
|
95
|
+
if parent is None or parent.type != "import_spec":
|
|
96
|
+
continue
|
|
97
|
+
text = _node_text(node, raw).strip()
|
|
98
|
+
if len(text) >= 2 and text[0] in "\"`" and text[-1] == text[0]:
|
|
99
|
+
text = text[1:-1]
|
|
100
|
+
if text:
|
|
101
|
+
specs.append(text)
|
|
102
|
+
return specs
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Rust mod / use
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _rust_path_segments(node, source: bytes) -> List[str]:
|
|
111
|
+
"""Flatten a use/mod path node into identifier segments (skip crate/super/self)."""
|
|
112
|
+
segs: List[str] = []
|
|
113
|
+
|
|
114
|
+
def collect(n) -> None:
|
|
115
|
+
t = n.type
|
|
116
|
+
if t in {"identifier", "type_identifier", "field_identifier"}:
|
|
117
|
+
segs.append(_node_text(n, source))
|
|
118
|
+
return
|
|
119
|
+
if t in {"crate", "super", "self"}:
|
|
120
|
+
segs.append(t)
|
|
121
|
+
return
|
|
122
|
+
if t == "use_wildcard":
|
|
123
|
+
for c in n.children:
|
|
124
|
+
collect(c)
|
|
125
|
+
return
|
|
126
|
+
if t in {"scoped_identifier", "scoped_type_identifier", "scoped_use_list", "use_list", "use_as_clause"}:
|
|
127
|
+
for c in n.children:
|
|
128
|
+
if c.type in {",", "{", "}", "::", "as", "*"}:
|
|
129
|
+
continue
|
|
130
|
+
collect(c)
|
|
131
|
+
return
|
|
132
|
+
for c in n.children:
|
|
133
|
+
collect(c)
|
|
134
|
+
|
|
135
|
+
collect(node)
|
|
136
|
+
return segs
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def extract_rust_import_refs(source: str | bytes) -> Optional[List[dict]]:
|
|
140
|
+
"""Extract Rust ``mod`` / ``use`` references.
|
|
141
|
+
|
|
142
|
+
Returns a list of dicts:
|
|
143
|
+
``{"kind": "mod", "name": "foo"}`` for ``mod foo;`` (file-level, no body)
|
|
144
|
+
``{"kind": "use", "segments": ["crate", "foo", "Bar"]}`` for use paths
|
|
145
|
+
|
|
146
|
+
``None`` when tree-sitter is unavailable.
|
|
147
|
+
"""
|
|
148
|
+
if not tree_sitter_available():
|
|
149
|
+
return None
|
|
150
|
+
raw = source.encode("utf-8") if isinstance(source, str) else source
|
|
151
|
+
tree = _parse("rust", raw)
|
|
152
|
+
if tree is None:
|
|
153
|
+
return None
|
|
154
|
+
refs: List[dict] = []
|
|
155
|
+
for node in tree.root_node.children:
|
|
156
|
+
if node.type == "mod_item":
|
|
157
|
+
# File-level mod only when there is no declaration_list body.
|
|
158
|
+
has_body = any(c.type == "declaration_list" for c in node.children)
|
|
159
|
+
name = None
|
|
160
|
+
for c in node.children:
|
|
161
|
+
if c.type == "identifier":
|
|
162
|
+
name = _node_text(c, raw)
|
|
163
|
+
break
|
|
164
|
+
if name and not has_body:
|
|
165
|
+
refs.append({"kind": "mod", "name": name})
|
|
166
|
+
elif node.type == "use_declaration":
|
|
167
|
+
# Path is the non-keyword child after `use` / visibility.
|
|
168
|
+
path_node = None
|
|
169
|
+
for c in node.children:
|
|
170
|
+
if c.type in {"use", "visibility_modifier", ";"}:
|
|
171
|
+
continue
|
|
172
|
+
path_node = c
|
|
173
|
+
break
|
|
174
|
+
if path_node is None:
|
|
175
|
+
continue
|
|
176
|
+
segments = _rust_path_segments(path_node, raw)
|
|
177
|
+
if segments:
|
|
178
|
+
refs.append({"kind": "use", "segments": segments})
|
|
179
|
+
return refs
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# Symbol matching (AstMatcher)
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def extract_symbols(language: str, source: str | bytes) -> Optional[List[_SymbolHit]]:
|
|
188
|
+
"""Named top-level (and nested) definitions for ``language``.
|
|
189
|
+
|
|
190
|
+
``language`` is an AstMatcher language id: typescript, javascript, go, rust.
|
|
191
|
+
Returns ``None`` when tree-sitter cannot parse (caller should fall back).
|
|
192
|
+
"""
|
|
193
|
+
if not tree_sitter_available():
|
|
194
|
+
return None
|
|
195
|
+
lang_key = {
|
|
196
|
+
"typescript": "typescript",
|
|
197
|
+
"javascript": "javascript",
|
|
198
|
+
"go": "go",
|
|
199
|
+
"rust": "rust",
|
|
200
|
+
}.get(language)
|
|
201
|
+
if lang_key is None:
|
|
202
|
+
return None
|
|
203
|
+
raw = source.encode("utf-8") if isinstance(source, str) else source
|
|
204
|
+
# Prefer tsx grammar when JSX-looking; callers pass typescript for .tsx too.
|
|
205
|
+
tree = _parse(lang_key, raw)
|
|
206
|
+
if tree is None and language == "typescript":
|
|
207
|
+
tree = _parse("tsx", raw)
|
|
208
|
+
if tree is None:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
if language in {"typescript", "javascript"}:
|
|
212
|
+
return _symbols_js(tree.root_node, raw)
|
|
213
|
+
if language == "go":
|
|
214
|
+
return _symbols_go(tree.root_node, raw)
|
|
215
|
+
if language == "rust":
|
|
216
|
+
return _symbols_rust(tree.root_node, raw)
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _line_of(node) -> int:
|
|
221
|
+
return int(node.start_point[0]) + 1
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _first_named(node, types: Sequence[str]) -> Optional[str]:
|
|
225
|
+
for c in node.children:
|
|
226
|
+
if c.type in types:
|
|
227
|
+
return str(c.text.decode("utf-8", errors="replace"))
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _symbols_js(root, source: bytes) -> List[_SymbolHit]:
|
|
232
|
+
hits: List[_SymbolHit] = []
|
|
233
|
+
|
|
234
|
+
def consider(node, kind: str, name_types: Sequence[str] = ("identifier", "type_identifier")) -> None:
|
|
235
|
+
name = _first_named(node, name_types)
|
|
236
|
+
if not name:
|
|
237
|
+
return
|
|
238
|
+
line = _line_of(node)
|
|
239
|
+
text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip() if line else name
|
|
240
|
+
hits.append((kind, name, line, text))
|
|
241
|
+
|
|
242
|
+
def walk(node) -> None:
|
|
243
|
+
t = node.type
|
|
244
|
+
if t == "function_declaration":
|
|
245
|
+
consider(node, "function")
|
|
246
|
+
elif t == "class_declaration":
|
|
247
|
+
consider(node, "class", ("type_identifier", "identifier"))
|
|
248
|
+
elif t == "interface_declaration":
|
|
249
|
+
consider(node, "interface", ("type_identifier", "identifier"))
|
|
250
|
+
elif t == "type_alias_declaration":
|
|
251
|
+
consider(node, "type", ("type_identifier", "identifier"))
|
|
252
|
+
elif t == "lexical_declaration":
|
|
253
|
+
for c in node.children:
|
|
254
|
+
if c.type != "variable_declarator":
|
|
255
|
+
continue
|
|
256
|
+
name = _first_named(c, ("identifier",))
|
|
257
|
+
if not name:
|
|
258
|
+
continue
|
|
259
|
+
line = _line_of(c)
|
|
260
|
+
text = (
|
|
261
|
+
source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
|
|
262
|
+
if line
|
|
263
|
+
else name
|
|
264
|
+
)
|
|
265
|
+
hits.append(("function", name, line, text))
|
|
266
|
+
elif t == "export_statement":
|
|
267
|
+
for c in node.children:
|
|
268
|
+
walk(c)
|
|
269
|
+
return
|
|
270
|
+
for c in node.children:
|
|
271
|
+
walk(c)
|
|
272
|
+
|
|
273
|
+
walk(root)
|
|
274
|
+
return hits
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _symbols_go(root, source: bytes) -> List[_SymbolHit]:
|
|
278
|
+
hits: List[_SymbolHit] = []
|
|
279
|
+
for node in _walk(root):
|
|
280
|
+
if node.type == "function_declaration":
|
|
281
|
+
name = _first_named(node, ("identifier",))
|
|
282
|
+
if not name:
|
|
283
|
+
continue
|
|
284
|
+
line = _line_of(node)
|
|
285
|
+
text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
|
|
286
|
+
hits.append(("function", name, line, text))
|
|
287
|
+
elif node.type == "method_declaration":
|
|
288
|
+
name = None
|
|
289
|
+
for c in node.children:
|
|
290
|
+
if c.type == "field_identifier":
|
|
291
|
+
name = c.text.decode("utf-8", errors="replace")
|
|
292
|
+
break
|
|
293
|
+
if not name:
|
|
294
|
+
continue
|
|
295
|
+
line = _line_of(node)
|
|
296
|
+
text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
|
|
297
|
+
hits.append(("function", name, line, text))
|
|
298
|
+
elif node.type == "type_declaration":
|
|
299
|
+
for c in node.children:
|
|
300
|
+
if c.type != "type_spec":
|
|
301
|
+
continue
|
|
302
|
+
name = _first_named(c, ("type_identifier", "identifier"))
|
|
303
|
+
if not name:
|
|
304
|
+
continue
|
|
305
|
+
kind = "struct"
|
|
306
|
+
for gc in c.children:
|
|
307
|
+
if gc.type == "struct_type":
|
|
308
|
+
kind = "struct"
|
|
309
|
+
elif gc.type == "interface_type":
|
|
310
|
+
kind = "interface"
|
|
311
|
+
elif gc.type == "type_identifier":
|
|
312
|
+
continue
|
|
313
|
+
line = _line_of(c)
|
|
314
|
+
text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
|
|
315
|
+
hits.append((kind, name, line, text))
|
|
316
|
+
return hits
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _symbols_rust(root, source: bytes) -> List[_SymbolHit]:
|
|
320
|
+
hits: List[_SymbolHit] = []
|
|
321
|
+
kind_map = {
|
|
322
|
+
"function_item": "function",
|
|
323
|
+
"struct_item": "struct",
|
|
324
|
+
"enum_item": "enum",
|
|
325
|
+
"trait_item": "trait",
|
|
326
|
+
"type_item": "type",
|
|
327
|
+
}
|
|
328
|
+
for node in _walk(root):
|
|
329
|
+
kind = kind_map.get(node.type)
|
|
330
|
+
if not kind:
|
|
331
|
+
continue
|
|
332
|
+
name = _first_named(node, ("identifier", "type_identifier"))
|
|
333
|
+
if not name:
|
|
334
|
+
continue
|
|
335
|
+
line = _line_of(node)
|
|
336
|
+
text = source.splitlines()[line - 1].decode("utf-8", errors="replace").strip()
|
|
337
|
+
hits.append((kind, name, line, text))
|
|
338
|
+
return hits
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def language_for_suffix(suffix: str) -> Optional[str]:
|
|
342
|
+
"""Map a file suffix to a language-pack name, or None."""
|
|
343
|
+
return _LANG_BY_SUFFIX.get(suffix.lower())
|