devcouncil 0.3.1 → 0.4.1
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 +92 -33
- 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 +56 -40
- 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 +964 -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 +281 -0
- package/src/devcouncil/integrations/mcp/handlers/graph.py +34 -0
- package/src/devcouncil/integrations/mcp/handlers/handoff.py +53 -0
- package/src/devcouncil/integrations/mcp/handlers/knowledge.py +28 -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 +87 -0
- package/src/devcouncil/integrations/mcp/handlers/read.py +103 -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 +69 -0
- package/src/devcouncil/integrations/mcp/handlers/scope.py +56 -0
- package/src/devcouncil/integrations/mcp/handlers/status.py +199 -0
- package/src/devcouncil/integrations/mcp/handlers/task.py +88 -0
- package/src/devcouncil/integrations/mcp/handlers/tool_specs.py +922 -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 +53 -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 +325 -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/summary.py +21 -3
- 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,1205 @@
|
|
|
1
|
+
"""TS/JS/Go/Rust symbol + call extraction via tree-sitter with regex fallback.
|
|
2
|
+
|
|
3
|
+
devcouncil: allow-unwired — package-private; reached only via graph.cache/build.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import List, Optional, Set
|
|
11
|
+
|
|
12
|
+
from devcouncil.indexing.graph.extract_python import (
|
|
13
|
+
ExtractedCall,
|
|
14
|
+
ExtractedImport,
|
|
15
|
+
ExtractedSymbol,
|
|
16
|
+
FileExtraction,
|
|
17
|
+
extract_rationales,
|
|
18
|
+
_enclosing_qualname,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Regex fallbacks
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_JS_EXPORT_RE = re.compile(
|
|
26
|
+
r"(?m)^\s*export\s+(?:default\s+)?(?:async\s+)?"
|
|
27
|
+
r"(?:function|class|const|let|var)\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
28
|
+
)
|
|
29
|
+
_JS_FUNCTION_RE = re.compile(
|
|
30
|
+
r"(?m)^\s*(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
31
|
+
)
|
|
32
|
+
_JS_CLASS_RE = re.compile(
|
|
33
|
+
r"(?m)^\s*(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)"
|
|
34
|
+
)
|
|
35
|
+
_JS_CALL_RE = re.compile(
|
|
36
|
+
r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
37
|
+
r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
38
|
+
)
|
|
39
|
+
_JS_IMPORT_RE = re.compile(
|
|
40
|
+
r"""(?:import|export)\s[^'"]*?from\s*['"](?P<spec>[^'"]+)['"]"""
|
|
41
|
+
r"""|(?:require|import)\s*\(\s*['"](?P<spec2>[^'"]+)['"]\s*\)"""
|
|
42
|
+
)
|
|
43
|
+
_JS_WORKER_URL_RE = re.compile(
|
|
44
|
+
r"""(?:new\s+(?:Worker|SharedWorker)\s*\(\s*)?"""
|
|
45
|
+
r"""new\s+URL\s*\(\s*['"](?P<spec>[^'"]+)['"]\s*,\s*import\.meta\.url"""
|
|
46
|
+
)
|
|
47
|
+
_JS_BARE_IMPORT_RE = re.compile(r"""^\s*import\s*['"](?P<spec>[^'"]+)['"]""", re.M)
|
|
48
|
+
_JS_EXPORT_LIST_RE = re.compile(
|
|
49
|
+
r"^\s*export\s*\{([^}]+)\}"
|
|
50
|
+
r"|^\s*export\s+default\s+(?:async\s+)?(?:function\s+)?([A-Za-z_][A-Za-z0-9_]*)",
|
|
51
|
+
re.M,
|
|
52
|
+
)
|
|
53
|
+
_JS_RATIONALE_RE = re.compile(
|
|
54
|
+
r"^\s*(?://|/\*)\s*(NOTE|WHY)\s*:\s*(.+?)(?:\*/)?$"
|
|
55
|
+
r"|^\s*(?://|/\*).*\b(ADR[- ]?\d+)\b",
|
|
56
|
+
re.M | re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
_GO_FUNC_RE = re.compile(
|
|
60
|
+
r"(?m)^func\s+(?:\((?P<recv>[^)]+)\)\s+)?(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
61
|
+
)
|
|
62
|
+
_GO_IMPORT_BLOCK_RE = re.compile(r"import\s*\((?P<body>[^)]*)\)", re.DOTALL)
|
|
63
|
+
_GO_IMPORT_SINGLE_RE = re.compile(
|
|
64
|
+
r"""^\s*import\s+(?:[A-Za-z_.]\w*\s+)?['"](?P<spec>[^'"]+)['"]""", re.M
|
|
65
|
+
)
|
|
66
|
+
_GO_IMPORT_SPEC_RE = re.compile(r"""['"](?P<spec>[^'"]+)['"]""")
|
|
67
|
+
_GO_CALL_RE = re.compile(
|
|
68
|
+
r"\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
69
|
+
r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
70
|
+
)
|
|
71
|
+
_GO_RECV_TYPE_RE = re.compile(r"\*?\s*([A-Za-z_][A-Za-z0-9_]*)\s*$")
|
|
72
|
+
|
|
73
|
+
_RUST_FN_RE = re.compile(
|
|
74
|
+
r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?fn\s+([A-Za-z_][A-Za-z0-9_]*)\s*[<(]"
|
|
75
|
+
)
|
|
76
|
+
_RUST_STRUCT_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?struct\s+([A-Za-z_][A-Za-z0-9_]*)")
|
|
77
|
+
_RUST_ENUM_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?enum\s+([A-Za-z_][A-Za-z0-9_]*)")
|
|
78
|
+
_RUST_TRAIT_RE = re.compile(r"(?m)^\s*(?:pub(?:\([^)]*\))?\s+)?trait\s+([A-Za-z_][A-Za-z0-9_]*)")
|
|
79
|
+
_RUST_USE_RE = re.compile(r"(?m)^\s*(?:pub\s+)?use\s+([^;]+);")
|
|
80
|
+
_RUST_MOD_RE = re.compile(r"(?m)^\s*(?:pub\s+)?mod\s+([A-Za-z_][A-Za-z0-9_]*)\s*;")
|
|
81
|
+
_RUST_CALL_RE = re.compile(
|
|
82
|
+
r"\b([A-Za-z_][A-Za-z0-9_]*)\s*::\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
83
|
+
r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
84
|
+
r"|\b([A-Za-z_][A-Za-z0-9_]*)\s*\("
|
|
85
|
+
)
|
|
86
|
+
_RUST_RATIONALE_RE = re.compile(
|
|
87
|
+
r"^\s*//[!/]?\s*(NOTE|WHY)\s*:\s*(.+)$"
|
|
88
|
+
r"|^\s*//[!/]?.*\b(ADR[- ]?\d+)\b",
|
|
89
|
+
re.M | re.IGNORECASE,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
_JS_SUFFIXES = {".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"}
|
|
93
|
+
_JS_CALL_SKIP = {
|
|
94
|
+
"if", "for", "while", "switch", "catch", "function", "return", "await", "new",
|
|
95
|
+
"typeof", "instanceof", "void", "delete", "yield", "import", "super",
|
|
96
|
+
}
|
|
97
|
+
# JSX/TSX component tags used as references (keeps components live without call edges).
|
|
98
|
+
# Member form (``<Foo.Bar``) must win over bare PascalCase so ``Nested`` is not
|
|
99
|
+
# captured alone when ``Nested.Thing`` is written.
|
|
100
|
+
_JSX_TAG_RE = re.compile(
|
|
101
|
+
r"<(?:"
|
|
102
|
+
r"([A-Za-z_][A-Za-z0-9_]*)\s*\.\s*([A-Za-z_][A-Za-z0-9_]*)"
|
|
103
|
+
r"|([A-Z][A-Za-z0-9_]*)"
|
|
104
|
+
r")\b"
|
|
105
|
+
)
|
|
106
|
+
_GO_CALL_SKIP = {
|
|
107
|
+
"if", "for", "switch", "return", "go", "defer", "func", "make", "new",
|
|
108
|
+
"len", "cap", "append", "copy", "delete", "panic", "recover", "range",
|
|
109
|
+
}
|
|
110
|
+
_RUST_CALL_SKIP = {
|
|
111
|
+
"if", "for", "while", "loop", "match", "return", "unsafe", "async", "await",
|
|
112
|
+
"box", "move", "ref", "mut", "self", "Self", "super", "crate",
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _js_import_specs(source: str) -> List[str]:
|
|
117
|
+
specs: List[str] = []
|
|
118
|
+
for m in _JS_IMPORT_RE.finditer(source):
|
|
119
|
+
spec = m.group("spec") or m.group("spec2")
|
|
120
|
+
if spec:
|
|
121
|
+
specs.append(spec)
|
|
122
|
+
for m in _JS_WORKER_URL_RE.finditer(source):
|
|
123
|
+
spec = m.group("spec")
|
|
124
|
+
if spec:
|
|
125
|
+
specs.append(spec)
|
|
126
|
+
for m in _JS_BARE_IMPORT_RE.finditer(source):
|
|
127
|
+
specs.append(m.group("spec"))
|
|
128
|
+
return specs
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _go_import_specs_regex(source: str) -> List[str]:
|
|
132
|
+
specs: List[str] = []
|
|
133
|
+
for m in _GO_IMPORT_BLOCK_RE.finditer(source):
|
|
134
|
+
specs.extend(_GO_IMPORT_SPEC_RE.findall(m.group("body")))
|
|
135
|
+
for m in _GO_IMPORT_SINGLE_RE.finditer(source):
|
|
136
|
+
specs.append(m.group("spec"))
|
|
137
|
+
return specs
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _line_span(node) -> tuple[int, int]:
|
|
141
|
+
return int(node.start_point[0]) + 1, int(node.end_point[0]) + 1
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _node_text(node, source: bytes) -> str:
|
|
145
|
+
return source[node.start_byte : node.end_byte].decode("utf-8", errors="replace")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _first_child(node, types: Set[str]):
|
|
149
|
+
for c in node.children:
|
|
150
|
+
if c.type in types:
|
|
151
|
+
return c
|
|
152
|
+
return None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _first_named_text(node, source: bytes, types: Set[str]) -> str:
|
|
156
|
+
c = _first_child(node, types)
|
|
157
|
+
return _node_text(c, source) if c is not None else ""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _walk(node):
|
|
161
|
+
yield node
|
|
162
|
+
for child in node.children:
|
|
163
|
+
yield from _walk(child)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _parse_lang(language: str, source: str):
|
|
167
|
+
try:
|
|
168
|
+
from tree_sitter_language_pack import get_parser
|
|
169
|
+
except Exception:
|
|
170
|
+
return None, b""
|
|
171
|
+
try:
|
|
172
|
+
parser = get_parser(language)
|
|
173
|
+
raw = source.encode("utf-8")
|
|
174
|
+
return parser.parse(raw), raw
|
|
175
|
+
except Exception:
|
|
176
|
+
return None, b""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ---------------------------------------------------------------------------
|
|
180
|
+
# JS / TS regex fallback
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _js_export_names_regex(source: str) -> Set[str]:
|
|
185
|
+
names: Set[str] = set()
|
|
186
|
+
for m in _JS_EXPORT_RE.finditer(source):
|
|
187
|
+
names.add(m.group(1))
|
|
188
|
+
for m in _JS_EXPORT_LIST_RE.finditer(source):
|
|
189
|
+
if m.group(1):
|
|
190
|
+
for part in m.group(1).split(","):
|
|
191
|
+
part = part.strip()
|
|
192
|
+
if not part:
|
|
193
|
+
continue
|
|
194
|
+
bits = re.split(r"\s+as\s+", part, flags=re.I)
|
|
195
|
+
local = bits[0].strip()
|
|
196
|
+
if local and local != "default" and re.match(r"^[A-Za-z_]", local):
|
|
197
|
+
names.add(local)
|
|
198
|
+
if m.group(2):
|
|
199
|
+
names.add(m.group(2))
|
|
200
|
+
return names
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _extract_js_rationales(source: str, symbols: List[ExtractedSymbol]) -> List[ExtractedSymbol]:
|
|
204
|
+
out: List[ExtractedSymbol] = []
|
|
205
|
+
for m in _JS_RATIONALE_RE.finditer(source):
|
|
206
|
+
line = source[: m.start()].count("\n") + 1
|
|
207
|
+
if m.group(1):
|
|
208
|
+
tag = m.group(1).upper()
|
|
209
|
+
text = (m.group(2) or "").strip().rstrip("*/").strip()
|
|
210
|
+
name = f"{tag}: {text}" if text else tag
|
|
211
|
+
else:
|
|
212
|
+
adr = (m.group(3) or "").strip()
|
|
213
|
+
name = f"ADR: {adr}"
|
|
214
|
+
enclosing = _enclosing_qualname(symbols, line)
|
|
215
|
+
out.append(
|
|
216
|
+
ExtractedSymbol(
|
|
217
|
+
kind="rationale",
|
|
218
|
+
name=name[:120],
|
|
219
|
+
qualname=f"$rationale:{line}",
|
|
220
|
+
line=line,
|
|
221
|
+
end_line=line,
|
|
222
|
+
bases=[enclosing] if enclosing else [],
|
|
223
|
+
)
|
|
224
|
+
)
|
|
225
|
+
return out
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _jsx_component_refs(source: str) -> List[str]:
|
|
229
|
+
"""Bare component names from JSX/TSX tags (``<Foo />``, ``<Foo.Bar />``)."""
|
|
230
|
+
names: List[str] = []
|
|
231
|
+
seen: Set[str] = set()
|
|
232
|
+
for m in _JSX_TAG_RE.finditer(source):
|
|
233
|
+
# Member form: groups 1/2; bare PascalCase: group 3.
|
|
234
|
+
candidates = [m.group(2), m.group(1), m.group(3)]
|
|
235
|
+
for name in candidates:
|
|
236
|
+
if not name or name in seen:
|
|
237
|
+
continue
|
|
238
|
+
# Prefer component-like identifiers (PascalCase or trailing member).
|
|
239
|
+
if name[0].isupper() or name is m.group(2):
|
|
240
|
+
seen.add(name)
|
|
241
|
+
names.append(name)
|
|
242
|
+
return names
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _extract_js_regex(path: str, source: str) -> FileExtraction:
|
|
246
|
+
lang = "typescript" if path.endswith((".ts", ".tsx")) else "javascript"
|
|
247
|
+
out = FileExtraction(path=path, language=lang, imports=_js_import_specs(source))
|
|
248
|
+
exported_names = _js_export_names_regex(source)
|
|
249
|
+
seen: set[str] = set()
|
|
250
|
+
for m in _JS_EXPORT_RE.finditer(source):
|
|
251
|
+
name = m.group(1)
|
|
252
|
+
if name in seen:
|
|
253
|
+
continue
|
|
254
|
+
seen.add(name)
|
|
255
|
+
line = source[: m.start()].count("\n") + 1
|
|
256
|
+
kind = "class" if "class" in m.group(0) else "function"
|
|
257
|
+
out.symbols.append(
|
|
258
|
+
ExtractedSymbol(
|
|
259
|
+
kind=kind,
|
|
260
|
+
name=name,
|
|
261
|
+
qualname=name,
|
|
262
|
+
line=line,
|
|
263
|
+
end_line=line,
|
|
264
|
+
exported=True,
|
|
265
|
+
)
|
|
266
|
+
)
|
|
267
|
+
for m in _JS_FUNCTION_RE.finditer(source):
|
|
268
|
+
name = m.group(1)
|
|
269
|
+
if name in seen:
|
|
270
|
+
continue
|
|
271
|
+
seen.add(name)
|
|
272
|
+
line = source[: m.start()].count("\n") + 1
|
|
273
|
+
out.symbols.append(
|
|
274
|
+
ExtractedSymbol(
|
|
275
|
+
kind="function",
|
|
276
|
+
name=name,
|
|
277
|
+
qualname=name,
|
|
278
|
+
line=line,
|
|
279
|
+
end_line=line,
|
|
280
|
+
exported=name in exported_names,
|
|
281
|
+
)
|
|
282
|
+
)
|
|
283
|
+
for m in _JS_CLASS_RE.finditer(source):
|
|
284
|
+
name = m.group(1)
|
|
285
|
+
if name in seen:
|
|
286
|
+
continue
|
|
287
|
+
seen.add(name)
|
|
288
|
+
line = source[: m.start()].count("\n") + 1
|
|
289
|
+
out.symbols.append(
|
|
290
|
+
ExtractedSymbol(
|
|
291
|
+
kind="class",
|
|
292
|
+
name=name,
|
|
293
|
+
qualname=name,
|
|
294
|
+
line=line,
|
|
295
|
+
end_line=line,
|
|
296
|
+
exported=name in exported_names,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
for m in _JS_CALL_RE.finditer(source):
|
|
300
|
+
if m.group(1) and m.group(2):
|
|
301
|
+
out.calls.append(
|
|
302
|
+
ExtractedCall(
|
|
303
|
+
name=m.group(2),
|
|
304
|
+
line=source[: m.start()].count("\n") + 1,
|
|
305
|
+
receiver=m.group(1),
|
|
306
|
+
qualname_hint=f"{m.group(1)}.{m.group(2)}",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
elif m.group(3):
|
|
310
|
+
name = m.group(3)
|
|
311
|
+
if name in _JS_CALL_SKIP:
|
|
312
|
+
continue
|
|
313
|
+
out.calls.append(
|
|
314
|
+
ExtractedCall(
|
|
315
|
+
name=name,
|
|
316
|
+
line=source[: m.start()].count("\n") + 1,
|
|
317
|
+
qualname_hint=name,
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
# Mark export-list names
|
|
321
|
+
for sym in out.symbols:
|
|
322
|
+
if sym.name in exported_names:
|
|
323
|
+
sym.exported = True
|
|
324
|
+
out.all_exports = sorted(exported_names)
|
|
325
|
+
out.references = _jsx_component_refs(source)
|
|
326
|
+
out.symbols.extend(_extract_js_rationales(source, out.symbols))
|
|
327
|
+
return out
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
# ---------------------------------------------------------------------------
|
|
331
|
+
# JS / TS tree-sitter
|
|
332
|
+
# ---------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _js_heritage(node, source: bytes) -> tuple[List[str], List[str]]:
|
|
336
|
+
bases: List[str] = []
|
|
337
|
+
implements: List[str] = []
|
|
338
|
+
heritage = _first_child(node, {"class_heritage"})
|
|
339
|
+
if heritage is None:
|
|
340
|
+
return bases, implements
|
|
341
|
+
for clause in heritage.children:
|
|
342
|
+
if clause.type == "extends_clause":
|
|
343
|
+
for c in clause.children:
|
|
344
|
+
if c.type in {"identifier", "type_identifier", "member_expression"}:
|
|
345
|
+
bases.append(_node_text(c, source))
|
|
346
|
+
elif clause.type == "implements_clause":
|
|
347
|
+
for c in clause.children:
|
|
348
|
+
if c.type in {"identifier", "type_identifier", "generic_type"}:
|
|
349
|
+
text = _node_text(c, source)
|
|
350
|
+
# strip type args: Foo<T> → Foo
|
|
351
|
+
implements.append(text.split("<", 1)[0].strip())
|
|
352
|
+
return bases, implements
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _collect_js_exports(root, source: bytes) -> tuple[Set[str], List[str], List[ExtractedImport]]:
|
|
356
|
+
"""Return (exported local names, import specs, import details / reexports)."""
|
|
357
|
+
exported: Set[str] = set()
|
|
358
|
+
specs: List[str] = []
|
|
359
|
+
details: List[ExtractedImport] = []
|
|
360
|
+
reexports: List[str] = []
|
|
361
|
+
|
|
362
|
+
for node in root.children:
|
|
363
|
+
t = node.type
|
|
364
|
+
if t == "import_statement":
|
|
365
|
+
detail = ExtractedImport(module="")
|
|
366
|
+
for c in _walk(node):
|
|
367
|
+
if c.type == "string":
|
|
368
|
+
spec = _node_text(c, source).strip("'\"")
|
|
369
|
+
if spec:
|
|
370
|
+
specs.append(spec)
|
|
371
|
+
detail.module = spec
|
|
372
|
+
elif c.type == "import_specifier":
|
|
373
|
+
ids = [x for x in c.children if x.type == "identifier"]
|
|
374
|
+
if ids:
|
|
375
|
+
local = _node_text(ids[-1], source)
|
|
376
|
+
remote = _node_text(ids[0], source)
|
|
377
|
+
detail.names.append(remote)
|
|
378
|
+
detail.alias_map[local] = remote
|
|
379
|
+
elif c.type == "identifier" and c.parent is not None and c.parent.type == "import_clause":
|
|
380
|
+
name = _node_text(c, source)
|
|
381
|
+
detail.names.append(name)
|
|
382
|
+
detail.alias_map[name] = "default"
|
|
383
|
+
if detail.module or detail.names:
|
|
384
|
+
details.append(detail)
|
|
385
|
+
elif t == "export_statement":
|
|
386
|
+
# export { X } / export { X } from '...' / export default ...
|
|
387
|
+
has_from = any(c.type == "string" for c in node.children)
|
|
388
|
+
from_spec = ""
|
|
389
|
+
stmt_reexports: List[str] = [] # names re-exported by THIS statement
|
|
390
|
+
for c in node.children:
|
|
391
|
+
if c.type == "string":
|
|
392
|
+
from_spec = _node_text(c, source).strip("'\"")
|
|
393
|
+
if from_spec:
|
|
394
|
+
specs.append(from_spec)
|
|
395
|
+
for c in node.children:
|
|
396
|
+
if c.type == "export_clause":
|
|
397
|
+
for spec in c.children:
|
|
398
|
+
if spec.type != "export_specifier":
|
|
399
|
+
continue
|
|
400
|
+
ids = [x for x in spec.children if x.type == "identifier"]
|
|
401
|
+
if not ids:
|
|
402
|
+
continue
|
|
403
|
+
local = _node_text(ids[0], source)
|
|
404
|
+
exported.add(local)
|
|
405
|
+
if has_from:
|
|
406
|
+
reexports.append(local)
|
|
407
|
+
stmt_reexports.append(local)
|
|
408
|
+
elif c.type in {
|
|
409
|
+
"function_declaration",
|
|
410
|
+
"class_declaration",
|
|
411
|
+
"interface_declaration",
|
|
412
|
+
"type_alias_declaration",
|
|
413
|
+
"lexical_declaration",
|
|
414
|
+
"variable_declaration",
|
|
415
|
+
}:
|
|
416
|
+
name = _first_named_text(
|
|
417
|
+
c, source, {"identifier", "type_identifier", "property_identifier"}
|
|
418
|
+
)
|
|
419
|
+
if c.type == "lexical_declaration":
|
|
420
|
+
for vd in c.children:
|
|
421
|
+
if vd.type == "variable_declarator":
|
|
422
|
+
name = _first_named_text(vd, source, {"identifier"}) or name
|
|
423
|
+
if name:
|
|
424
|
+
exported.add(name)
|
|
425
|
+
elif c.type == "identifier" and any(x.type == "default" for x in node.children):
|
|
426
|
+
exported.add(_node_text(c, source))
|
|
427
|
+
if has_from and from_spec:
|
|
428
|
+
# Only names from THIS statement (the old accumulated-list slice
|
|
429
|
+
# cross-contaminated re-export names across statements).
|
|
430
|
+
details.append(
|
|
431
|
+
ExtractedImport(module=from_spec, names=stmt_reexports)
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
# Fix reexport details: rebuild from export-from statements cleanly
|
|
435
|
+
details = [d for d in details if d.module or d.names]
|
|
436
|
+
return exported, specs, details
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _extract_js_calls(root, source: bytes) -> List[ExtractedCall]:
|
|
440
|
+
calls: List[ExtractedCall] = []
|
|
441
|
+
for node in _walk(root):
|
|
442
|
+
if node.type != "call_expression":
|
|
443
|
+
continue
|
|
444
|
+
func = node.child_by_field_name("function") if hasattr(node, "child_by_field_name") else None
|
|
445
|
+
if func is None and node.children:
|
|
446
|
+
func = node.children[0]
|
|
447
|
+
if func is None:
|
|
448
|
+
continue
|
|
449
|
+
line = int(node.start_point[0]) + 1
|
|
450
|
+
if func.type == "identifier":
|
|
451
|
+
name = _node_text(func, source)
|
|
452
|
+
if name in _JS_CALL_SKIP:
|
|
453
|
+
continue
|
|
454
|
+
calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
|
|
455
|
+
elif func.type == "member_expression":
|
|
456
|
+
prop = _first_child(func, {"property_identifier"}) or _first_child(
|
|
457
|
+
func, {"identifier"}
|
|
458
|
+
)
|
|
459
|
+
if prop is None:
|
|
460
|
+
continue
|
|
461
|
+
name = _node_text(prop, source)
|
|
462
|
+
obj = func.children[0] if func.children else None
|
|
463
|
+
receiver = ""
|
|
464
|
+
if obj is not None and obj.type in {"identifier", "this", "super"}:
|
|
465
|
+
receiver = _node_text(obj, source)
|
|
466
|
+
elif obj is not None:
|
|
467
|
+
receiver = _node_text(obj, source).split(".")[-1]
|
|
468
|
+
hint = f"{receiver}.{name}" if receiver else name
|
|
469
|
+
calls.append(
|
|
470
|
+
ExtractedCall(name=name, line=line, receiver=receiver, qualname_hint=hint)
|
|
471
|
+
)
|
|
472
|
+
return calls
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _extract_js_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
|
|
476
|
+
from devcouncil.indexing.ts_imports import language_for_suffix
|
|
477
|
+
|
|
478
|
+
suffix = Path(path).suffix.lower()
|
|
479
|
+
lang_key = language_for_suffix(suffix)
|
|
480
|
+
if not lang_key:
|
|
481
|
+
return None
|
|
482
|
+
tree, raw = _parse_lang(lang_key, source)
|
|
483
|
+
if tree is None and lang_key == "typescript":
|
|
484
|
+
tree, raw = _parse_lang("tsx", source)
|
|
485
|
+
if tree is None:
|
|
486
|
+
return None
|
|
487
|
+
|
|
488
|
+
lang = "typescript" if suffix in {".ts", ".tsx"} else "javascript"
|
|
489
|
+
out = FileExtraction(path=path, language=lang)
|
|
490
|
+
root = tree.root_node
|
|
491
|
+
exported, specs, details = _collect_js_exports(root, raw)
|
|
492
|
+
# Also pick up require/import() via regex
|
|
493
|
+
for spec in _js_import_specs(source):
|
|
494
|
+
if spec not in specs:
|
|
495
|
+
specs.append(spec)
|
|
496
|
+
out.imports = specs
|
|
497
|
+
out.import_details = details
|
|
498
|
+
out.all_exports = sorted(exported)
|
|
499
|
+
out.reexports = []
|
|
500
|
+
|
|
501
|
+
# Re-scan export-from for reexports list
|
|
502
|
+
for node in root.children:
|
|
503
|
+
if node.type != "export_statement":
|
|
504
|
+
continue
|
|
505
|
+
if not any(c.type == "string" for c in node.children):
|
|
506
|
+
continue
|
|
507
|
+
for c in node.children:
|
|
508
|
+
if c.type != "export_clause":
|
|
509
|
+
continue
|
|
510
|
+
for spec in c.children:
|
|
511
|
+
if spec.type != "export_specifier":
|
|
512
|
+
continue
|
|
513
|
+
ids = [x for x in spec.children if x.type == "identifier"]
|
|
514
|
+
if ids:
|
|
515
|
+
out.reexports.append(_node_text(ids[0], raw))
|
|
516
|
+
|
|
517
|
+
seen_qual: Set[str] = set()
|
|
518
|
+
|
|
519
|
+
def add_sym(
|
|
520
|
+
kind: str,
|
|
521
|
+
name: str,
|
|
522
|
+
qualname: str,
|
|
523
|
+
node,
|
|
524
|
+
*,
|
|
525
|
+
bases: Optional[List[str]] = None,
|
|
526
|
+
implements: Optional[List[str]] = None,
|
|
527
|
+
force_exported: Optional[bool] = None,
|
|
528
|
+
) -> None:
|
|
529
|
+
if not name or qualname in seen_qual:
|
|
530
|
+
return
|
|
531
|
+
seen_qual.add(qualname)
|
|
532
|
+
start, end = _line_span(node)
|
|
533
|
+
if force_exported is not None:
|
|
534
|
+
exp = force_exported
|
|
535
|
+
else:
|
|
536
|
+
exp = name in exported
|
|
537
|
+
out.symbols.append(
|
|
538
|
+
ExtractedSymbol(
|
|
539
|
+
kind=kind,
|
|
540
|
+
name=name,
|
|
541
|
+
qualname=qualname,
|
|
542
|
+
line=start,
|
|
543
|
+
end_line=end,
|
|
544
|
+
bases=bases or [],
|
|
545
|
+
implements=implements or [],
|
|
546
|
+
exported=exp,
|
|
547
|
+
)
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
def walk_decl(node, under_export: bool = False) -> None:
|
|
551
|
+
t = node.type
|
|
552
|
+
if t == "export_statement":
|
|
553
|
+
for c in node.children:
|
|
554
|
+
walk_decl(c, under_export=True)
|
|
555
|
+
return
|
|
556
|
+
if t == "function_declaration":
|
|
557
|
+
name = _first_named_text(node, raw, {"identifier"})
|
|
558
|
+
add_sym("function", name, name, node, force_exported=True if under_export else None)
|
|
559
|
+
return
|
|
560
|
+
if t == "class_declaration":
|
|
561
|
+
name = _first_named_text(node, raw, {"type_identifier", "identifier"})
|
|
562
|
+
bases, implements = _js_heritage(node, raw)
|
|
563
|
+
add_sym(
|
|
564
|
+
"class",
|
|
565
|
+
name,
|
|
566
|
+
name,
|
|
567
|
+
node,
|
|
568
|
+
bases=bases,
|
|
569
|
+
implements=implements,
|
|
570
|
+
force_exported=True if under_export else None,
|
|
571
|
+
)
|
|
572
|
+
body = _first_child(node, {"class_body"})
|
|
573
|
+
if body is not None and name:
|
|
574
|
+
for child in body.children:
|
|
575
|
+
if child.type != "method_definition":
|
|
576
|
+
continue
|
|
577
|
+
# skip constructors as methods with name constructor still recorded
|
|
578
|
+
mname = _first_named_text(
|
|
579
|
+
child, raw, {"property_identifier", "identifier"}
|
|
580
|
+
)
|
|
581
|
+
if not mname:
|
|
582
|
+
continue
|
|
583
|
+
add_sym("method", mname, f"{name}.{mname}", child, force_exported=False)
|
|
584
|
+
return
|
|
585
|
+
if t == "interface_declaration":
|
|
586
|
+
name = _first_named_text(node, raw, {"type_identifier", "identifier"})
|
|
587
|
+
bases, _impl = _js_heritage(node, raw)
|
|
588
|
+
# interfaces use extends_type_clause sometimes
|
|
589
|
+
for c in node.children:
|
|
590
|
+
if c.type in {"extends_type_clause", "extends_clause"}:
|
|
591
|
+
for gc in c.children:
|
|
592
|
+
if gc.type in {"identifier", "type_identifier"}:
|
|
593
|
+
bases.append(_node_text(gc, raw))
|
|
594
|
+
add_sym(
|
|
595
|
+
"interface",
|
|
596
|
+
name,
|
|
597
|
+
name,
|
|
598
|
+
node,
|
|
599
|
+
bases=bases,
|
|
600
|
+
force_exported=True if under_export else None,
|
|
601
|
+
)
|
|
602
|
+
return
|
|
603
|
+
if t == "type_alias_declaration":
|
|
604
|
+
name = _first_named_text(node, raw, {"type_identifier", "identifier"})
|
|
605
|
+
add_sym(
|
|
606
|
+
"type",
|
|
607
|
+
name,
|
|
608
|
+
name,
|
|
609
|
+
node,
|
|
610
|
+
force_exported=True if under_export else None,
|
|
611
|
+
)
|
|
612
|
+
return
|
|
613
|
+
if t in {"lexical_declaration", "variable_declaration"}:
|
|
614
|
+
for c in node.children:
|
|
615
|
+
if c.type != "variable_declarator":
|
|
616
|
+
continue
|
|
617
|
+
name = _first_named_text(c, raw, {"identifier"})
|
|
618
|
+
if not name:
|
|
619
|
+
continue
|
|
620
|
+
# Only count if value looks like a function/arrow, or is exported
|
|
621
|
+
val = None
|
|
622
|
+
for gc in c.children:
|
|
623
|
+
if gc.type not in {"identifier", "=", "type_annotation"}:
|
|
624
|
+
val = gc
|
|
625
|
+
break
|
|
626
|
+
is_fn = val is not None and val.type in {
|
|
627
|
+
"arrow_function",
|
|
628
|
+
"function",
|
|
629
|
+
"function_expression",
|
|
630
|
+
}
|
|
631
|
+
if is_fn or under_export or name in exported:
|
|
632
|
+
add_sym(
|
|
633
|
+
"function",
|
|
634
|
+
name,
|
|
635
|
+
name,
|
|
636
|
+
c,
|
|
637
|
+
force_exported=True if under_export or name in exported else False,
|
|
638
|
+
)
|
|
639
|
+
return
|
|
640
|
+
|
|
641
|
+
for child in root.children:
|
|
642
|
+
walk_decl(child)
|
|
643
|
+
|
|
644
|
+
# Mark any remaining exported names that were only listed via export { X }
|
|
645
|
+
by_name = {s.name: s for s in out.symbols if s.kind != "rationale"}
|
|
646
|
+
for name in exported:
|
|
647
|
+
if name in by_name:
|
|
648
|
+
by_name[name].exported = True
|
|
649
|
+
|
|
650
|
+
out.calls = _extract_js_calls(root, raw)
|
|
651
|
+
# JSX component tags as non-call references (regex covers TSX even when
|
|
652
|
+
# tree-sitter language was plain typescript).
|
|
653
|
+
refs = set(_jsx_component_refs(source))
|
|
654
|
+
for node in _walk(root):
|
|
655
|
+
if node.type not in {"jsx_opening_element", "jsx_self_closing_element"}:
|
|
656
|
+
continue
|
|
657
|
+
name_node = node.child_by_field_name("name") if hasattr(node, "child_by_field_name") else None
|
|
658
|
+
if name_node is None:
|
|
659
|
+
for c in node.children:
|
|
660
|
+
if c.type in {
|
|
661
|
+
"identifier",
|
|
662
|
+
"jsx_identifier",
|
|
663
|
+
"nested_identifier",
|
|
664
|
+
"member_expression",
|
|
665
|
+
}:
|
|
666
|
+
name_node = c
|
|
667
|
+
break
|
|
668
|
+
if name_node is None:
|
|
669
|
+
continue
|
|
670
|
+
text = _node_text(name_node, raw)
|
|
671
|
+
if "." in text:
|
|
672
|
+
text = text.rsplit(".", 1)[-1]
|
|
673
|
+
if text and text[0].isupper():
|
|
674
|
+
refs.add(text)
|
|
675
|
+
out.references = sorted(refs)
|
|
676
|
+
out.symbols.extend(_extract_js_rationales(source, out.symbols))
|
|
677
|
+
return out
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
# ---------------------------------------------------------------------------
|
|
681
|
+
# Go
|
|
682
|
+
# ---------------------------------------------------------------------------
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _go_receiver_type(recv_list, source: bytes) -> str:
|
|
686
|
+
"""Extract type name from method receiver parameter_list."""
|
|
687
|
+
if recv_list is None:
|
|
688
|
+
return ""
|
|
689
|
+
for c in recv_list.children:
|
|
690
|
+
if c.type != "parameter_declaration":
|
|
691
|
+
continue
|
|
692
|
+
for gc in c.children:
|
|
693
|
+
if gc.type == "type_identifier":
|
|
694
|
+
return _node_text(gc, source)
|
|
695
|
+
if gc.type == "pointer_type":
|
|
696
|
+
tid = _first_child(gc, {"type_identifier"})
|
|
697
|
+
if tid is not None:
|
|
698
|
+
return _node_text(tid, source)
|
|
699
|
+
return _node_text(gc, source).lstrip("*").strip()
|
|
700
|
+
return ""
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
def _extract_go_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
|
|
704
|
+
tree, raw = _parse_lang("go", source)
|
|
705
|
+
if tree is None:
|
|
706
|
+
return None
|
|
707
|
+
out = FileExtraction(path=path, language="go")
|
|
708
|
+
try:
|
|
709
|
+
from devcouncil.indexing.ts_imports import extract_go_import_specs
|
|
710
|
+
|
|
711
|
+
specs = extract_go_import_specs(source)
|
|
712
|
+
if specs is not None:
|
|
713
|
+
out.imports = specs
|
|
714
|
+
else:
|
|
715
|
+
out.imports = _go_import_specs_regex(source)
|
|
716
|
+
except Exception:
|
|
717
|
+
out.imports = _go_import_specs_regex(source)
|
|
718
|
+
|
|
719
|
+
for node in _walk(tree.root_node):
|
|
720
|
+
if node.type == "function_declaration":
|
|
721
|
+
name = _first_named_text(node, raw, {"identifier"})
|
|
722
|
+
if not name:
|
|
723
|
+
continue
|
|
724
|
+
start, end = _line_span(node)
|
|
725
|
+
out.symbols.append(
|
|
726
|
+
ExtractedSymbol(
|
|
727
|
+
kind="function",
|
|
728
|
+
name=name,
|
|
729
|
+
qualname=name,
|
|
730
|
+
line=start,
|
|
731
|
+
end_line=end,
|
|
732
|
+
exported=name[:1].isupper(),
|
|
733
|
+
)
|
|
734
|
+
)
|
|
735
|
+
elif node.type == "method_declaration":
|
|
736
|
+
name = _first_named_text(node, raw, {"field_identifier"})
|
|
737
|
+
if not name:
|
|
738
|
+
continue
|
|
739
|
+
# First parameter_list is the receiver
|
|
740
|
+
recv_type = ""
|
|
741
|
+
for c in node.children:
|
|
742
|
+
if c.type == "parameter_list":
|
|
743
|
+
recv_type = _go_receiver_type(c, raw)
|
|
744
|
+
break
|
|
745
|
+
qual = f"{recv_type}.{name}" if recv_type else name
|
|
746
|
+
start, end = _line_span(node)
|
|
747
|
+
out.symbols.append(
|
|
748
|
+
ExtractedSymbol(
|
|
749
|
+
kind="method",
|
|
750
|
+
name=name,
|
|
751
|
+
qualname=qual,
|
|
752
|
+
line=start,
|
|
753
|
+
end_line=end,
|
|
754
|
+
exported=name[:1].isupper(),
|
|
755
|
+
)
|
|
756
|
+
)
|
|
757
|
+
elif node.type == "type_spec":
|
|
758
|
+
name = _first_named_text(node, raw, {"type_identifier", "identifier"})
|
|
759
|
+
if not name:
|
|
760
|
+
continue
|
|
761
|
+
kind = "type"
|
|
762
|
+
for c in node.children:
|
|
763
|
+
if c.type == "struct_type":
|
|
764
|
+
kind = "struct"
|
|
765
|
+
elif c.type == "interface_type":
|
|
766
|
+
kind = "interface"
|
|
767
|
+
start, end = _line_span(node)
|
|
768
|
+
out.symbols.append(
|
|
769
|
+
ExtractedSymbol(
|
|
770
|
+
kind=kind,
|
|
771
|
+
name=name,
|
|
772
|
+
qualname=name,
|
|
773
|
+
line=start,
|
|
774
|
+
end_line=end,
|
|
775
|
+
exported=name[:1].isupper(),
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
elif node.type == "call_expression":
|
|
779
|
+
func = node.children[0] if node.children else None
|
|
780
|
+
if func is None:
|
|
781
|
+
continue
|
|
782
|
+
line = int(node.start_point[0]) + 1
|
|
783
|
+
if func.type == "identifier":
|
|
784
|
+
name = _node_text(func, raw)
|
|
785
|
+
if name in _GO_CALL_SKIP:
|
|
786
|
+
continue
|
|
787
|
+
out.calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
|
|
788
|
+
elif func.type == "selector_expression":
|
|
789
|
+
field = _first_child(func, {"field_identifier"})
|
|
790
|
+
if field is None:
|
|
791
|
+
continue
|
|
792
|
+
name = _node_text(field, raw)
|
|
793
|
+
recv_node = func.children[0] if func.children else None
|
|
794
|
+
receiver = _node_text(recv_node, raw) if recv_node else ""
|
|
795
|
+
out.calls.append(
|
|
796
|
+
ExtractedCall(
|
|
797
|
+
name=name,
|
|
798
|
+
line=line,
|
|
799
|
+
receiver=receiver,
|
|
800
|
+
qualname_hint=f"{receiver}.{name}" if receiver else name,
|
|
801
|
+
)
|
|
802
|
+
)
|
|
803
|
+
out.symbols.extend(extract_rationales(source, out.symbols)) # # style rare in Go
|
|
804
|
+
return out
|
|
805
|
+
|
|
806
|
+
|
|
807
|
+
def _extract_go_regex(path: str, source: str) -> FileExtraction:
|
|
808
|
+
out = FileExtraction(path=path, language="go", imports=_go_import_specs_regex(source))
|
|
809
|
+
try:
|
|
810
|
+
from devcouncil.indexing.ts_imports import extract_go_import_specs
|
|
811
|
+
|
|
812
|
+
ts_specs = extract_go_import_specs(source)
|
|
813
|
+
if ts_specs is not None:
|
|
814
|
+
out.imports = ts_specs
|
|
815
|
+
except Exception:
|
|
816
|
+
pass
|
|
817
|
+
for m in _GO_FUNC_RE.finditer(source):
|
|
818
|
+
name = m.group("name")
|
|
819
|
+
recv = (m.group("recv") or "").strip()
|
|
820
|
+
line = source[: m.start()].count("\n") + 1
|
|
821
|
+
if recv:
|
|
822
|
+
rt = _GO_RECV_TYPE_RE.search(recv)
|
|
823
|
+
rtype = rt.group(1) if rt else ""
|
|
824
|
+
qual = f"{rtype}.{name}" if rtype else name
|
|
825
|
+
out.symbols.append(
|
|
826
|
+
ExtractedSymbol(
|
|
827
|
+
kind="method",
|
|
828
|
+
name=name,
|
|
829
|
+
qualname=qual,
|
|
830
|
+
line=line,
|
|
831
|
+
end_line=line,
|
|
832
|
+
exported=name[:1].isupper() if name else False,
|
|
833
|
+
)
|
|
834
|
+
)
|
|
835
|
+
else:
|
|
836
|
+
out.symbols.append(
|
|
837
|
+
ExtractedSymbol(
|
|
838
|
+
kind="function",
|
|
839
|
+
name=name,
|
|
840
|
+
qualname=name,
|
|
841
|
+
line=line,
|
|
842
|
+
end_line=line,
|
|
843
|
+
exported=name[:1].isupper() if name else False,
|
|
844
|
+
)
|
|
845
|
+
)
|
|
846
|
+
for m in _GO_CALL_RE.finditer(source):
|
|
847
|
+
if m.group(1) and m.group(2):
|
|
848
|
+
out.calls.append(
|
|
849
|
+
ExtractedCall(
|
|
850
|
+
name=m.group(2),
|
|
851
|
+
line=source[: m.start()].count("\n") + 1,
|
|
852
|
+
receiver=m.group(1),
|
|
853
|
+
qualname_hint=f"{m.group(1)}.{m.group(2)}",
|
|
854
|
+
)
|
|
855
|
+
)
|
|
856
|
+
elif m.group(3):
|
|
857
|
+
name = m.group(3)
|
|
858
|
+
if name in _GO_CALL_SKIP:
|
|
859
|
+
continue
|
|
860
|
+
out.calls.append(
|
|
861
|
+
ExtractedCall(
|
|
862
|
+
name=name,
|
|
863
|
+
line=source[: m.start()].count("\n") + 1,
|
|
864
|
+
qualname_hint=name,
|
|
865
|
+
)
|
|
866
|
+
)
|
|
867
|
+
return out
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
# ---------------------------------------------------------------------------
|
|
871
|
+
# Rust
|
|
872
|
+
# ---------------------------------------------------------------------------
|
|
873
|
+
|
|
874
|
+
|
|
875
|
+
def _extract_rust_rationales(source: str, symbols: List[ExtractedSymbol]) -> List[ExtractedSymbol]:
|
|
876
|
+
out: List[ExtractedSymbol] = []
|
|
877
|
+
for m in _RUST_RATIONALE_RE.finditer(source):
|
|
878
|
+
line = source[: m.start()].count("\n") + 1
|
|
879
|
+
if m.group(1):
|
|
880
|
+
tag = m.group(1).upper()
|
|
881
|
+
text = (m.group(2) or "").strip()
|
|
882
|
+
name = f"{tag}: {text}" if text else tag
|
|
883
|
+
else:
|
|
884
|
+
name = f"ADR: {(m.group(3) or '').strip()}"
|
|
885
|
+
enclosing = _enclosing_qualname(symbols, line)
|
|
886
|
+
out.append(
|
|
887
|
+
ExtractedSymbol(
|
|
888
|
+
kind="rationale",
|
|
889
|
+
name=name[:120],
|
|
890
|
+
qualname=f"$rationale:{line}",
|
|
891
|
+
line=line,
|
|
892
|
+
end_line=line,
|
|
893
|
+
bases=[enclosing] if enclosing else [],
|
|
894
|
+
)
|
|
895
|
+
)
|
|
896
|
+
return out
|
|
897
|
+
|
|
898
|
+
|
|
899
|
+
def _rust_is_pub(node) -> bool:
|
|
900
|
+
return any(c.type == "visibility_modifier" for c in node.children)
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _extract_rust_tree_sitter(path: str, source: str) -> Optional[FileExtraction]:
|
|
904
|
+
tree, raw = _parse_lang("rust", source)
|
|
905
|
+
if tree is None:
|
|
906
|
+
return None
|
|
907
|
+
out = FileExtraction(path=path, language="rust")
|
|
908
|
+
try:
|
|
909
|
+
from devcouncil.indexing.ts_imports import extract_rust_import_refs
|
|
910
|
+
|
|
911
|
+
refs = extract_rust_import_refs(source)
|
|
912
|
+
if refs is not None:
|
|
913
|
+
for ref in refs:
|
|
914
|
+
if ref.get("kind") == "mod":
|
|
915
|
+
name = ref.get("name") or ""
|
|
916
|
+
if name:
|
|
917
|
+
out.imports.append(f"mod:{name}")
|
|
918
|
+
elif ref.get("kind") == "use":
|
|
919
|
+
segs = ref.get("segments") or []
|
|
920
|
+
if segs:
|
|
921
|
+
out.imports.append("::".join(segs))
|
|
922
|
+
out.import_details.append(
|
|
923
|
+
ExtractedImport(
|
|
924
|
+
module="::".join(segs[:-1]) if len(segs) > 1 else "::".join(segs),
|
|
925
|
+
names=[segs[-1]] if segs else [],
|
|
926
|
+
alias_map={segs[-1]: "::".join(segs)} if segs else {},
|
|
927
|
+
)
|
|
928
|
+
)
|
|
929
|
+
except Exception:
|
|
930
|
+
pass
|
|
931
|
+
|
|
932
|
+
for node in tree.root_node.children:
|
|
933
|
+
t = node.type
|
|
934
|
+
if t == "function_item":
|
|
935
|
+
name = _first_named_text(node, raw, {"identifier"})
|
|
936
|
+
if not name:
|
|
937
|
+
continue
|
|
938
|
+
start, end = _line_span(node)
|
|
939
|
+
out.symbols.append(
|
|
940
|
+
ExtractedSymbol(
|
|
941
|
+
kind="function",
|
|
942
|
+
name=name,
|
|
943
|
+
qualname=name,
|
|
944
|
+
line=start,
|
|
945
|
+
end_line=end,
|
|
946
|
+
exported=_rust_is_pub(node),
|
|
947
|
+
)
|
|
948
|
+
)
|
|
949
|
+
elif t in {"struct_item", "enum_item", "trait_item", "type_item"}:
|
|
950
|
+
kind = {
|
|
951
|
+
"struct_item": "struct",
|
|
952
|
+
"enum_item": "enum",
|
|
953
|
+
"trait_item": "trait",
|
|
954
|
+
"type_item": "type",
|
|
955
|
+
}[t]
|
|
956
|
+
name = _first_named_text(node, raw, {"type_identifier", "identifier"})
|
|
957
|
+
if not name:
|
|
958
|
+
continue
|
|
959
|
+
start, end = _line_span(node)
|
|
960
|
+
out.symbols.append(
|
|
961
|
+
ExtractedSymbol(
|
|
962
|
+
kind=kind,
|
|
963
|
+
name=name,
|
|
964
|
+
qualname=name,
|
|
965
|
+
line=start,
|
|
966
|
+
end_line=end,
|
|
967
|
+
exported=_rust_is_pub(node),
|
|
968
|
+
)
|
|
969
|
+
)
|
|
970
|
+
if t == "trait_item":
|
|
971
|
+
body = _first_child(node, {"declaration_list"})
|
|
972
|
+
if body is not None:
|
|
973
|
+
for child in body.children:
|
|
974
|
+
if child.type not in {"function_item", "function_signature_item"}:
|
|
975
|
+
continue
|
|
976
|
+
mname = _first_named_text(child, raw, {"identifier"})
|
|
977
|
+
if not mname:
|
|
978
|
+
continue
|
|
979
|
+
ms, me = _line_span(child)
|
|
980
|
+
out.symbols.append(
|
|
981
|
+
ExtractedSymbol(
|
|
982
|
+
kind="method",
|
|
983
|
+
name=mname,
|
|
984
|
+
qualname=f"{name}.{mname}",
|
|
985
|
+
line=ms,
|
|
986
|
+
end_line=me,
|
|
987
|
+
exported=_rust_is_pub(node),
|
|
988
|
+
)
|
|
989
|
+
)
|
|
990
|
+
elif t == "impl_item":
|
|
991
|
+
# impl Type { ... } or impl Trait for Type { ... }
|
|
992
|
+
type_ids = [c for c in node.children if c.type == "type_identifier"]
|
|
993
|
+
trait_name = ""
|
|
994
|
+
type_name = ""
|
|
995
|
+
if any(c.type == "for" for c in node.children):
|
|
996
|
+
if len(type_ids) >= 2:
|
|
997
|
+
trait_name = _node_text(type_ids[0], raw)
|
|
998
|
+
type_name = _node_text(type_ids[1], raw)
|
|
999
|
+
elif len(type_ids) == 1:
|
|
1000
|
+
type_name = _node_text(type_ids[0], raw)
|
|
1001
|
+
elif type_ids:
|
|
1002
|
+
type_name = _node_text(type_ids[0], raw)
|
|
1003
|
+
if type_name and trait_name:
|
|
1004
|
+
# Record implements on a synthetic note via a type-level symbol update later
|
|
1005
|
+
# Ensure type exists; attach implements list by appending a marker symbol update
|
|
1006
|
+
existing = next((s for s in out.symbols if s.qualname == type_name), None)
|
|
1007
|
+
if existing is not None:
|
|
1008
|
+
if trait_name not in existing.implements:
|
|
1009
|
+
existing.implements.append(trait_name)
|
|
1010
|
+
else:
|
|
1011
|
+
start, end = _line_span(node)
|
|
1012
|
+
out.symbols.append(
|
|
1013
|
+
ExtractedSymbol(
|
|
1014
|
+
kind="struct",
|
|
1015
|
+
name=type_name,
|
|
1016
|
+
qualname=type_name,
|
|
1017
|
+
line=start,
|
|
1018
|
+
end_line=end,
|
|
1019
|
+
implements=[trait_name],
|
|
1020
|
+
exported=False,
|
|
1021
|
+
)
|
|
1022
|
+
)
|
|
1023
|
+
body = _first_child(node, {"declaration_list"})
|
|
1024
|
+
if body is not None and type_name:
|
|
1025
|
+
for child in body.children:
|
|
1026
|
+
if child.type != "function_item":
|
|
1027
|
+
continue
|
|
1028
|
+
mname = _first_named_text(child, raw, {"identifier"})
|
|
1029
|
+
if not mname:
|
|
1030
|
+
continue
|
|
1031
|
+
ms, me = _line_span(child)
|
|
1032
|
+
out.symbols.append(
|
|
1033
|
+
ExtractedSymbol(
|
|
1034
|
+
kind="method",
|
|
1035
|
+
name=mname,
|
|
1036
|
+
qualname=f"{type_name}.{mname}",
|
|
1037
|
+
line=ms,
|
|
1038
|
+
end_line=me,
|
|
1039
|
+
exported=_rust_is_pub(child),
|
|
1040
|
+
bases=[trait_name] if trait_name else [],
|
|
1041
|
+
)
|
|
1042
|
+
)
|
|
1043
|
+
|
|
1044
|
+
for node in _walk(tree.root_node):
|
|
1045
|
+
if node.type != "call_expression":
|
|
1046
|
+
continue
|
|
1047
|
+
func = node.children[0] if node.children else None
|
|
1048
|
+
if func is None:
|
|
1049
|
+
continue
|
|
1050
|
+
line = int(node.start_point[0]) + 1
|
|
1051
|
+
if func.type == "identifier":
|
|
1052
|
+
name = _node_text(func, raw)
|
|
1053
|
+
if name in _RUST_CALL_SKIP:
|
|
1054
|
+
continue
|
|
1055
|
+
out.calls.append(ExtractedCall(name=name, line=line, qualname_hint=name))
|
|
1056
|
+
elif func.type == "field_expression":
|
|
1057
|
+
field = _first_child(func, {"field_identifier"}) or _first_child(
|
|
1058
|
+
func, {"identifier"}
|
|
1059
|
+
)
|
|
1060
|
+
if field is None:
|
|
1061
|
+
continue
|
|
1062
|
+
name = _node_text(field, raw)
|
|
1063
|
+
recv = func.children[0] if func.children else None
|
|
1064
|
+
receiver = _node_text(recv, raw) if recv is not None else ""
|
|
1065
|
+
out.calls.append(
|
|
1066
|
+
ExtractedCall(
|
|
1067
|
+
name=name,
|
|
1068
|
+
line=line,
|
|
1069
|
+
receiver=receiver,
|
|
1070
|
+
qualname_hint=f"{receiver}.{name}" if receiver else name,
|
|
1071
|
+
)
|
|
1072
|
+
)
|
|
1073
|
+
elif func.type in {"scoped_identifier", "scoped_type_identifier"}:
|
|
1074
|
+
# Type::method / path::func
|
|
1075
|
+
name_node = _first_child(func, {"identifier"})
|
|
1076
|
+
# Prefer the rightmost identifier as the called name
|
|
1077
|
+
ids = [c for c in func.children if c.type in {"identifier", "type_identifier"}]
|
|
1078
|
+
if not ids:
|
|
1079
|
+
continue
|
|
1080
|
+
name = _node_text(ids[-1], raw)
|
|
1081
|
+
if name in _RUST_CALL_SKIP:
|
|
1082
|
+
continue
|
|
1083
|
+
receiver = _node_text(ids[-2], raw) if len(ids) >= 2 else ""
|
|
1084
|
+
if not receiver and name_node is not None:
|
|
1085
|
+
name = _node_text(name_node, raw)
|
|
1086
|
+
out.calls.append(
|
|
1087
|
+
ExtractedCall(
|
|
1088
|
+
name=name,
|
|
1089
|
+
line=line,
|
|
1090
|
+
receiver=receiver,
|
|
1091
|
+
qualname_hint=f"{receiver}.{name}" if receiver else name,
|
|
1092
|
+
)
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
out.symbols.extend(_extract_rust_rationales(source, out.symbols))
|
|
1096
|
+
return out
|
|
1097
|
+
|
|
1098
|
+
|
|
1099
|
+
def _extract_rust_regex(path: str, source: str) -> FileExtraction:
|
|
1100
|
+
out = FileExtraction(path=path, language="rust")
|
|
1101
|
+
for m in _RUST_USE_RE.finditer(source):
|
|
1102
|
+
path_s = m.group(1).strip()
|
|
1103
|
+
out.imports.append(path_s.replace(" ", ""))
|
|
1104
|
+
for m in _RUST_MOD_RE.finditer(source):
|
|
1105
|
+
out.imports.append(f"mod:{m.group(1)}")
|
|
1106
|
+
for kind, rx in (
|
|
1107
|
+
("struct", _RUST_STRUCT_RE),
|
|
1108
|
+
("enum", _RUST_ENUM_RE),
|
|
1109
|
+
("trait", _RUST_TRAIT_RE),
|
|
1110
|
+
("function", _RUST_FN_RE),
|
|
1111
|
+
):
|
|
1112
|
+
for m in rx.finditer(source):
|
|
1113
|
+
name = m.group(1)
|
|
1114
|
+
line = source[: m.start()].count("\n") + 1
|
|
1115
|
+
exported = "pub" in m.group(0)
|
|
1116
|
+
out.symbols.append(
|
|
1117
|
+
ExtractedSymbol(
|
|
1118
|
+
kind=kind,
|
|
1119
|
+
name=name,
|
|
1120
|
+
qualname=name,
|
|
1121
|
+
line=line,
|
|
1122
|
+
end_line=line,
|
|
1123
|
+
exported=exported,
|
|
1124
|
+
)
|
|
1125
|
+
)
|
|
1126
|
+
for m in _RUST_CALL_RE.finditer(source):
|
|
1127
|
+
if m.group(1) and m.group(2):
|
|
1128
|
+
# Type::method
|
|
1129
|
+
out.calls.append(
|
|
1130
|
+
ExtractedCall(
|
|
1131
|
+
name=m.group(2),
|
|
1132
|
+
line=source[: m.start()].count("\n") + 1,
|
|
1133
|
+
receiver=m.group(1),
|
|
1134
|
+
qualname_hint=f"{m.group(1)}.{m.group(2)}",
|
|
1135
|
+
)
|
|
1136
|
+
)
|
|
1137
|
+
elif m.group(3) and m.group(4):
|
|
1138
|
+
out.calls.append(
|
|
1139
|
+
ExtractedCall(
|
|
1140
|
+
name=m.group(4),
|
|
1141
|
+
line=source[: m.start()].count("\n") + 1,
|
|
1142
|
+
receiver=m.group(3),
|
|
1143
|
+
qualname_hint=f"{m.group(3)}.{m.group(4)}",
|
|
1144
|
+
)
|
|
1145
|
+
)
|
|
1146
|
+
elif m.group(5):
|
|
1147
|
+
name = m.group(5)
|
|
1148
|
+
if name in _RUST_CALL_SKIP:
|
|
1149
|
+
continue
|
|
1150
|
+
out.calls.append(
|
|
1151
|
+
ExtractedCall(
|
|
1152
|
+
name=name,
|
|
1153
|
+
line=source[: m.start()].count("\n") + 1,
|
|
1154
|
+
qualname_hint=name,
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
out.symbols.extend(_extract_rust_rationales(source, out.symbols))
|
|
1158
|
+
return out
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
# ---------------------------------------------------------------------------
|
|
1162
|
+
# Public API
|
|
1163
|
+
# ---------------------------------------------------------------------------
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def extract_ts_js(path: str, source: str) -> FileExtraction:
|
|
1167
|
+
"""Extract from a TS/JS file (tree-sitter when available, else regex)."""
|
|
1168
|
+
ts = _extract_js_tree_sitter(path, source)
|
|
1169
|
+
if ts is not None:
|
|
1170
|
+
return ts
|
|
1171
|
+
return _extract_js_regex(path, source)
|
|
1172
|
+
|
|
1173
|
+
|
|
1174
|
+
def extract_go(path: str, source: str) -> FileExtraction:
|
|
1175
|
+
"""Extract from a Go file (tree-sitter preferred)."""
|
|
1176
|
+
ts = _extract_go_tree_sitter(path, source)
|
|
1177
|
+
if ts is not None:
|
|
1178
|
+
return ts
|
|
1179
|
+
return _extract_go_regex(path, source)
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def extract_rust(path: str, source: str) -> FileExtraction:
|
|
1183
|
+
"""Extract from a Rust file (tree-sitter preferred)."""
|
|
1184
|
+
ts = _extract_rust_tree_sitter(path, source)
|
|
1185
|
+
if ts is not None:
|
|
1186
|
+
return ts
|
|
1187
|
+
return _extract_rust_regex(path, source)
|
|
1188
|
+
|
|
1189
|
+
|
|
1190
|
+
def extract_file(path: str, source: str) -> FileExtraction:
|
|
1191
|
+
"""Dispatch extraction by file suffix."""
|
|
1192
|
+
lower = path.lower()
|
|
1193
|
+
if lower.endswith(".py"):
|
|
1194
|
+
from devcouncil.indexing.graph.extract_python import extract_python
|
|
1195
|
+
|
|
1196
|
+
return extract_python(path, source)
|
|
1197
|
+
if any(lower.endswith(s) for s in _JS_SUFFIXES):
|
|
1198
|
+
return extract_ts_js(path, source)
|
|
1199
|
+
if lower.endswith(".go"):
|
|
1200
|
+
return extract_go(path, source)
|
|
1201
|
+
if lower.endswith(".rs"):
|
|
1202
|
+
return extract_rust(path, source)
|
|
1203
|
+
from devcouncil.codeintel.languages.generic_extractor import extract_generic
|
|
1204
|
+
|
|
1205
|
+
return extract_generic(path, source)
|