From 05f60106aacce5de99604effb362dc17106370c9 Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:42:18 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- .beads/.gitignore | 72 + .beads/README.md | 81 + .beads/config.yaml | 54 + .beads/hooks/post-checkout | 24 + .beads/hooks/post-merge | 24 + .beads/hooks/pre-commit | 24 + .beads/hooks/pre-push | 24 + .beads/hooks/prepare-commit-msg | 24 + .beads/metadata.json | 7 + .github/ISSUE_TEMPLATE/bug_report.yml | 94 + .github/ISSUE_TEMPLATE/config.yml | 8 + .github/ISSUE_TEMPLATE/feature_request.yml | 54 + .github/ISSUE_TEMPLATE/platform_request.yml | 62 + .github/PULL_REQUEST_TEMPLATE.md | 34 + .github/code-review-graph.instruction.md | 43 + .github/copilot-instructions.md | 38 + .github/dependabot.yml | 20 + .github/workflows/ci.yml | 88 + .github/workflows/eval.yml | 65 + .github/workflows/pr-review.yml | 21 + .github/workflows/publish.yml | 32 + .gitignore | 94 + .mcp.json | 8 + .serena/.gitignore | 2 + .serena/project.yml | 154 + AGENTS.md | 123 + CHANGELOG.md | 821 ++ CLAUDE.md | 206 + CODE_OF_CONDUCT.md | 16 + CONTRIBUTING.md | 91 + GEMINI.md | 38 + LICENSE | 21 + README.hi-IN.md | 326 + README.ja-JP.md | 328 + README.ko-KR.md | 328 + README.md | 650 ++ README.wehub.md | 7 + README.zh-CN.md | 326 + SECURITY.md | 62 + action.yml | 127 + code-review-graph-vscode/.gitignore | 4 + code-review-graph-vscode/.vscodeignore | 10 + code-review-graph-vscode/CHANGELOG.md | 47 + code-review-graph-vscode/LICENSE | 21 + code-review-graph-vscode/README.md | 94 + code-review-graph-vscode/esbuild.mjs | 51 + .../media/icons/graph.svg | 12 + code-review-graph-vscode/media/icons/icon.png | Bin 0 -> 37267 bytes .../media/walkthrough/build.md | 7 + .../media/walkthrough/explore.md | 10 + .../media/walkthrough/install.md | 17 + code-review-graph-vscode/package-lock.json | 4022 ++++++++++ code-review-graph-vscode/package.json | 311 + code-review-graph-vscode/src/backend/cli.ts | 220 + .../src/backend/sqlite.ts | 595 ++ .../src/backend/watcher.ts | 60 + code-review-graph-vscode/src/extension.ts | 983 +++ .../src/features/blastRadius.ts | 70 + .../src/features/cursorResolver.ts | 37 + .../src/features/navigation.ts | 196 + .../src/features/reviewAssistant.ts | 117 + .../src/features/scmDecorations.ts | 160 + .../src/features/search.ts | 131 + .../src/onboarding/installer.ts | 114 + .../src/onboarding/welcome.ts | 113 + .../src/views/graphWebview.ts | 633 ++ .../src/views/statusBar.ts | 89 + .../src/views/treeItems.ts | 234 + .../src/views/treeView.ts | 237 + code-review-graph-vscode/src/webview/graph.ts | 990 +++ code-review-graph-vscode/test/sqlite.test.ts | 499 ++ code-review-graph-vscode/tsconfig.json | 21 + code_review_graph/__init__.py | 20 + code_review_graph/__main__.py | 4 + code_review_graph/analysis.py | 410 + code_review_graph/changes.py | 409 + code_review_graph/cli.py | 1243 +++ code_review_graph/communities.py | 874 +++ code_review_graph/constants.py | 23 + code_review_graph/context_savings.py | 317 + code_review_graph/custom_languages.py | 322 + code_review_graph/daemon.py | 1009 +++ code_review_graph/daemon_cli.py | 320 + code_review_graph/embeddings.py | 1006 +++ code_review_graph/enrich.py | 303 + code_review_graph/eval/__init__.py | 33 + code_review_graph/eval/benchmarks/__init__.py | 1 + .../eval/benchmarks/agent_baseline.py | 193 + .../eval/benchmarks/build_performance.py | 60 + .../eval/benchmarks/flow_completeness.py | 36 + .../eval/benchmarks/impact_accuracy.py | 220 + .../eval/benchmarks/multi_hop_retrieval.py | 125 + .../eval/benchmarks/search_quality.py | 59 + .../eval/benchmarks/token_efficiency.py | 143 + .../eval/configs/code-review-graph.yaml | 50 + code_review_graph/eval/configs/express.yaml | 45 + code_review_graph/eval/configs/fastapi.yaml | 48 + code_review_graph/eval/configs/flask.yaml | 50 + code_review_graph/eval/configs/gin.yaml | 51 + code_review_graph/eval/configs/httpx.yaml | 48 + code_review_graph/eval/reporter.py | 301 + code_review_graph/eval/runner.py | 211 + code_review_graph/eval/scorer.py | 85 + code_review_graph/eval/token_benchmark.py | 182 + code_review_graph/exports.py | 409 + code_review_graph/flows.py | 698 ++ code_review_graph/graph.py | 1373 ++++ code_review_graph/graph_diff.py | 122 + code_review_graph/hints.py | 384 + code_review_graph/incremental.py | 1234 +++ code_review_graph/jedi_resolver.py | 303 + code_review_graph/main.py | 1079 +++ code_review_graph/memory.py | 142 + code_review_graph/migrations.py | 284 + code_review_graph/parser.py | 6957 +++++++++++++++++ code_review_graph/postprocessing.py | 134 + code_review_graph/prompts.py | 159 + code_review_graph/refactor.py | 852 ++ code_review_graph/registry.py | 319 + code_review_graph/rescript_resolver.py | 206 + code_review_graph/search.py | 447 ++ code_review_graph/skills.py | 1481 ++++ code_review_graph/spring_resolver.py | 200 + code_review_graph/temporal_resolver.py | 199 + code_review_graph/token_benchmark.py | 125 + code_review_graph/tools/__init__.py | 156 + code_review_graph/tools/_common.py | 176 + code_review_graph/tools/analysis_tools.py | 184 + code_review_graph/tools/build.py | 541 ++ code_review_graph/tools/community_tools.py | 246 + code_review_graph/tools/context.py | 152 + code_review_graph/tools/docs.py | 274 + code_review_graph/tools/flows_tools.py | 176 + code_review_graph/tools/query.py | 692 ++ code_review_graph/tools/refactor_tools.py | 168 + code_review_graph/tools/registry_tools.py | 125 + code_review_graph/tools/review.py | 477 ++ code_review_graph/tsconfig_resolver.py | 257 + code_review_graph/visualization.py | 2184 ++++++ code_review_graph/wiki.py | 305 + diagrams/context-savings-demo.gif | Bin 0 -> 1338859 bytes diagrams/context-savings-demo.tape | 62 + diagrams/diagram1_before_vs_after.png | Bin 0 -> 482995 bytes diagrams/diagram2_architecture_pipeline.png | Bin 0 -> 343128 bytes diagrams/diagram3_blast_radius.png | Bin 0 -> 493875 bytes diagrams/diagram4_incremental_update.png | Bin 0 -> 364730 bytes diagrams/diagram5_benchmark_board.png | Bin 0 -> 463883 bytes diagrams/diagram6_monorepo_funnel.png | Bin 0 -> 403721 bytes diagrams/diagram7_mcp_integration_flow.png | Bin 0 -> 440293 bytes diagrams/diagram8_supported_platforms.png | Bin 0 -> 493944 bytes diagrams/diagram9_language_coverage.png | Bin 0 -> 467487 bytes diagrams/generate_diagrams.py | 730 ++ docs/COMMANDS.md | 404 + docs/CUSTOM_LANGUAGES.md | 171 + docs/FAQ.md | 252 + docs/FEATURES.md | 155 + docs/GITHUB_ACTION.md | 166 + docs/INDEX.md | 15 + docs/LEGAL.md | 16 + docs/LLM-OPTIMIZED-REFERENCE.md | 71 + docs/REPRODUCING.md | 429 + docs/ROADMAP.md | 114 + docs/TROUBLESHOOTING.md | 190 + docs/USAGE.md | 154 + docs/architecture.md | 120 + docs/schema.md | 276 + ...eview-graph_impact_accuracy_2026-05-25.csv | 3 + ...w-graph_multi_hop_retrieval_2026-05-25.csv | 3 + ...view-graph_token_efficiency_2026-05-25.csv | 3 + .../express_impact_accuracy_2026-05-25.csv | 3 + ...express_multi_hop_retrieval_2026-05-25.csv | 2 + .../express_token_efficiency_2026-05-25.csv | 3 + .../fastapi_impact_accuracy_2026-05-25.csv | 3 + ...fastapi_multi_hop_retrieval_2026-05-25.csv | 3 + .../fastapi_token_efficiency_2026-05-25.csv | 3 + .../flask_impact_accuracy_2026-05-25.csv | 3 + .../flask_multi_hop_retrieval_2026-05-25.csv | 3 + .../flask_token_efficiency_2026-05-25.csv | 3 + .../gin_impact_accuracy_2026-05-25.csv | 4 + .../gin_multi_hop_retrieval_2026-05-25.csv | 3 + .../gin_token_efficiency_2026-05-25.csv | 4 + .../httpx_impact_accuracy_2026-05-25.csv | 3 + .../httpx_multi_hop_retrieval_2026-05-25.csv | 3 + .../httpx_token_efficiency_2026-05-25.csv | 3 + hooks/hooks.json | 35 + hooks/session-start.sh | 25 + pyproject.toml | 138 + scripts/diagnose_pypi_connectivity.py | 62 + scripts/render_pr_comment.py | 333 + skills/build-graph/SKILL.md | 38 + skills/debug-issue/SKILL.md | 27 + skills/explore-codebase/SKILL.md | 28 + skills/refactor-safely/SKILL.md | 28 + skills/review-changes/SKILL.md | 29 + skills/review-delta/SKILL.md | 46 + skills/review-pr/SKILL.md | 66 + tests/__init__.py | 0 tests/fixtures/KafkaPatterns.java | 47 + tests/fixtures/MarkdownMsg.tsx | 3 + tests/fixtures/Sample.cs | 48 + tests/fixtures/SampleJava.java | 66 + tests/fixtures/SpringDI.java | 78 + tests/fixtures/TemporalWorkflow.java | 72 + tests/fixtures/__tests__/UserService.ts | 15 + tests/fixtures/alias_importer.ts | 6 + tests/fixtures/caller_example.py | 8 + tests/fixtures/detect_changes_sample.json | 156 + tests/fixtures/multi_call_example.py | 13 + tests/fixtures/sample.R | 30 + tests/fixtures/sample.c | 25 + tests/fixtures/sample.cpp | 30 + tests/fixtures/sample.dart | 42 + tests/fixtures/sample.ex | 36 + tests/fixtures/sample.gd | 41 + tests/fixtures/sample.hh | 22 + tests/fixtures/sample.jl | 67 + tests/fixtures/sample.kt | 27 + tests/fixtures/sample.lua | 139 + tests/fixtures/sample.luau | 119 + tests/fixtures/sample.m | 47 + tests/fixtures/sample.nix | 17 + tests/fixtures/sample.php | 100 + tests/fixtures/sample.pl | 33 + tests/fixtures/sample.rb | 38 + tests/fixtures/sample.res | 79 + tests/fixtures/sample.resi | 27 + tests/fixtures/sample.scala | 37 + tests/fixtures/sample.sh | 43 + tests/fixtures/sample.sol | 218 + tests/fixtures/sample.sql | 37 + tests/fixtures/sample.sv | 77 + tests/fixtures/sample.swift | 60 + tests/fixtures/sample.xs | 32 + tests/fixtures/sample_bun.test.ts | 27 + tests/fixtures/sample_callback_refs.py | 35 + tests/fixtures/sample_databricks_export.py | 37 + .../fixtures/sample_databricks_notebook.ipynb | 59 + tests/fixtures/sample_go.go | 48 + tests/fixtures/sample_lib.sh | 7 + tests/fixtures/sample_map_dispatch.py | 47 + tests/fixtures/sample_map_dispatch.ts | 55 + tests/fixtures/sample_mocha.test.ts | 16 + tests/fixtures/sample_module.nix | 12 + tests/fixtures/sample_notebook.ipynb | 91 + tests/fixtures/sample_python.py | 51 + tests/fixtures/sample_rust.rs | 69 + tests/fixtures/sample_typescript.ts | 41 + tests/fixtures/sample_vitest.test.ts | 18 + tests/fixtures/sample_vue.vue | 35 + tests/fixtures/src/lib/utils.ts | 3 + tests/fixtures/test_sample.R | 9 + tests/fixtures/test_sample.py | 19 + tests/fixtures/tsconfig.json | 9 + tests/test_action_render.py | 286 + tests/test_changes.py | 632 ++ tests/test_cli.py | 344 + tests/test_cli_install.py | 98 + tests/test_communities.py | 592 ++ tests/test_context_savings.py | 64 + tests/test_custom_languages.py | 306 + tests/test_daemon.py | 1126 +++ tests/test_embeddings.py | 1133 +++ tests/test_enrich.py | 237 + tests/test_eval.py | 928 +++ tests/test_flows.py | 612 ++ tests/test_fts_sync.py | 75 + tests/test_graph.py | 416 + tests/test_hints.py | 195 + tests/test_incremental.py | 870 +++ tests/test_integration_git.py | 253 + tests/test_integration_v2.py | 423 + tests/test_main.py | 329 + tests/test_migrations.py | 151 + tests/test_multilang.py | 2550 ++++++ tests/test_notebook.py | 445 ++ tests/test_parser.py | 1460 ++++ tests/test_postprocessing.py | 327 + tests/test_prompts.py | 192 + tests/test_refactor.py | 901 +++ tests/test_registry.py | 336 + tests/test_search.py | 270 + tests/test_skills.py | 1649 ++++ tests/test_tools.py | 1565 ++++ tests/test_transactions.py | 116 + tests/test_tsconfig_resolver.py | 56 + tests/test_visualization.py | 578 ++ tests/test_wiki.py | 254 + uv.lock | 4097 ++++++++++ 288 files changed, 76871 insertions(+) create mode 100644 .beads/.gitignore create mode 100644 .beads/README.md create mode 100644 .beads/config.yaml create mode 100755 .beads/hooks/post-checkout create mode 100755 .beads/hooks/post-merge create mode 100755 .beads/hooks/pre-commit create mode 100755 .beads/hooks/pre-push create mode 100755 .beads/hooks/prepare-commit-msg create mode 100644 .beads/metadata.json create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/platform_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/code-review-graph.instruction.md create mode 100644 .github/copilot-instructions.md create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/eval.yml create mode 100644 .github/workflows/pr-review.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .mcp.json create mode 100644 .serena/.gitignore create mode 100644 .serena/project.yml create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md create mode 100644 CLAUDE.md create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 GEMINI.md create mode 100644 LICENSE create mode 100644 README.hi-IN.md create mode 100644 README.ja-JP.md create mode 100644 README.ko-KR.md create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 README.zh-CN.md create mode 100644 SECURITY.md create mode 100644 action.yml create mode 100644 code-review-graph-vscode/.gitignore create mode 100644 code-review-graph-vscode/.vscodeignore create mode 100644 code-review-graph-vscode/CHANGELOG.md create mode 100644 code-review-graph-vscode/LICENSE create mode 100644 code-review-graph-vscode/README.md create mode 100644 code-review-graph-vscode/esbuild.mjs create mode 100644 code-review-graph-vscode/media/icons/graph.svg create mode 100644 code-review-graph-vscode/media/icons/icon.png create mode 100644 code-review-graph-vscode/media/walkthrough/build.md create mode 100644 code-review-graph-vscode/media/walkthrough/explore.md create mode 100644 code-review-graph-vscode/media/walkthrough/install.md create mode 100644 code-review-graph-vscode/package-lock.json create mode 100644 code-review-graph-vscode/package.json create mode 100644 code-review-graph-vscode/src/backend/cli.ts create mode 100644 code-review-graph-vscode/src/backend/sqlite.ts create mode 100644 code-review-graph-vscode/src/backend/watcher.ts create mode 100644 code-review-graph-vscode/src/extension.ts create mode 100644 code-review-graph-vscode/src/features/blastRadius.ts create mode 100644 code-review-graph-vscode/src/features/cursorResolver.ts create mode 100644 code-review-graph-vscode/src/features/navigation.ts create mode 100644 code-review-graph-vscode/src/features/reviewAssistant.ts create mode 100644 code-review-graph-vscode/src/features/scmDecorations.ts create mode 100644 code-review-graph-vscode/src/features/search.ts create mode 100644 code-review-graph-vscode/src/onboarding/installer.ts create mode 100644 code-review-graph-vscode/src/onboarding/welcome.ts create mode 100644 code-review-graph-vscode/src/views/graphWebview.ts create mode 100644 code-review-graph-vscode/src/views/statusBar.ts create mode 100644 code-review-graph-vscode/src/views/treeItems.ts create mode 100644 code-review-graph-vscode/src/views/treeView.ts create mode 100644 code-review-graph-vscode/src/webview/graph.ts create mode 100644 code-review-graph-vscode/test/sqlite.test.ts create mode 100644 code-review-graph-vscode/tsconfig.json create mode 100644 code_review_graph/__init__.py create mode 100644 code_review_graph/__main__.py create mode 100644 code_review_graph/analysis.py create mode 100644 code_review_graph/changes.py create mode 100644 code_review_graph/cli.py create mode 100644 code_review_graph/communities.py create mode 100644 code_review_graph/constants.py create mode 100644 code_review_graph/context_savings.py create mode 100644 code_review_graph/custom_languages.py create mode 100644 code_review_graph/daemon.py create mode 100644 code_review_graph/daemon_cli.py create mode 100644 code_review_graph/embeddings.py create mode 100644 code_review_graph/enrich.py create mode 100644 code_review_graph/eval/__init__.py create mode 100644 code_review_graph/eval/benchmarks/__init__.py create mode 100644 code_review_graph/eval/benchmarks/agent_baseline.py create mode 100644 code_review_graph/eval/benchmarks/build_performance.py create mode 100644 code_review_graph/eval/benchmarks/flow_completeness.py create mode 100644 code_review_graph/eval/benchmarks/impact_accuracy.py create mode 100644 code_review_graph/eval/benchmarks/multi_hop_retrieval.py create mode 100644 code_review_graph/eval/benchmarks/search_quality.py create mode 100644 code_review_graph/eval/benchmarks/token_efficiency.py create mode 100644 code_review_graph/eval/configs/code-review-graph.yaml create mode 100644 code_review_graph/eval/configs/express.yaml create mode 100644 code_review_graph/eval/configs/fastapi.yaml create mode 100644 code_review_graph/eval/configs/flask.yaml create mode 100644 code_review_graph/eval/configs/gin.yaml create mode 100644 code_review_graph/eval/configs/httpx.yaml create mode 100644 code_review_graph/eval/reporter.py create mode 100644 code_review_graph/eval/runner.py create mode 100644 code_review_graph/eval/scorer.py create mode 100644 code_review_graph/eval/token_benchmark.py create mode 100644 code_review_graph/exports.py create mode 100644 code_review_graph/flows.py create mode 100644 code_review_graph/graph.py create mode 100644 code_review_graph/graph_diff.py create mode 100644 code_review_graph/hints.py create mode 100644 code_review_graph/incremental.py create mode 100644 code_review_graph/jedi_resolver.py create mode 100644 code_review_graph/main.py create mode 100644 code_review_graph/memory.py create mode 100644 code_review_graph/migrations.py create mode 100644 code_review_graph/parser.py create mode 100644 code_review_graph/postprocessing.py create mode 100644 code_review_graph/prompts.py create mode 100644 code_review_graph/refactor.py create mode 100644 code_review_graph/registry.py create mode 100644 code_review_graph/rescript_resolver.py create mode 100644 code_review_graph/search.py create mode 100644 code_review_graph/skills.py create mode 100644 code_review_graph/spring_resolver.py create mode 100644 code_review_graph/temporal_resolver.py create mode 100644 code_review_graph/token_benchmark.py create mode 100644 code_review_graph/tools/__init__.py create mode 100644 code_review_graph/tools/_common.py create mode 100644 code_review_graph/tools/analysis_tools.py create mode 100644 code_review_graph/tools/build.py create mode 100644 code_review_graph/tools/community_tools.py create mode 100644 code_review_graph/tools/context.py create mode 100644 code_review_graph/tools/docs.py create mode 100644 code_review_graph/tools/flows_tools.py create mode 100644 code_review_graph/tools/query.py create mode 100644 code_review_graph/tools/refactor_tools.py create mode 100644 code_review_graph/tools/registry_tools.py create mode 100644 code_review_graph/tools/review.py create mode 100644 code_review_graph/tsconfig_resolver.py create mode 100644 code_review_graph/visualization.py create mode 100644 code_review_graph/wiki.py create mode 100644 diagrams/context-savings-demo.gif create mode 100644 diagrams/context-savings-demo.tape create mode 100644 diagrams/diagram1_before_vs_after.png create mode 100644 diagrams/diagram2_architecture_pipeline.png create mode 100644 diagrams/diagram3_blast_radius.png create mode 100644 diagrams/diagram4_incremental_update.png create mode 100644 diagrams/diagram5_benchmark_board.png create mode 100644 diagrams/diagram6_monorepo_funnel.png create mode 100644 diagrams/diagram7_mcp_integration_flow.png create mode 100644 diagrams/diagram8_supported_platforms.png create mode 100644 diagrams/diagram9_language_coverage.png create mode 100644 diagrams/generate_diagrams.py create mode 100644 docs/COMMANDS.md create mode 100644 docs/CUSTOM_LANGUAGES.md create mode 100644 docs/FAQ.md create mode 100644 docs/FEATURES.md create mode 100644 docs/GITHUB_ACTION.md create mode 100644 docs/INDEX.md create mode 100644 docs/LEGAL.md create mode 100644 docs/LLM-OPTIMIZED-REFERENCE.md create mode 100644 docs/REPRODUCING.md create mode 100644 docs/ROADMAP.md create mode 100644 docs/TROUBLESHOOTING.md create mode 100644 docs/USAGE.md create mode 100644 docs/architecture.md create mode 100644 docs/schema.md create mode 100644 evaluate/results/code-review-graph_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/code-review-graph_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/code-review-graph_token_efficiency_2026-05-25.csv create mode 100644 evaluate/results/express_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/express_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/express_token_efficiency_2026-05-25.csv create mode 100644 evaluate/results/fastapi_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/fastapi_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/fastapi_token_efficiency_2026-05-25.csv create mode 100644 evaluate/results/flask_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/flask_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/flask_token_efficiency_2026-05-25.csv create mode 100644 evaluate/results/gin_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/gin_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/gin_token_efficiency_2026-05-25.csv create mode 100644 evaluate/results/httpx_impact_accuracy_2026-05-25.csv create mode 100644 evaluate/results/httpx_multi_hop_retrieval_2026-05-25.csv create mode 100644 evaluate/results/httpx_token_efficiency_2026-05-25.csv create mode 100644 hooks/hooks.json create mode 100755 hooks/session-start.sh create mode 100644 pyproject.toml create mode 100644 scripts/diagnose_pypi_connectivity.py create mode 100644 scripts/render_pr_comment.py create mode 100644 skills/build-graph/SKILL.md create mode 100644 skills/debug-issue/SKILL.md create mode 100644 skills/explore-codebase/SKILL.md create mode 100644 skills/refactor-safely/SKILL.md create mode 100644 skills/review-changes/SKILL.md create mode 100644 skills/review-delta/SKILL.md create mode 100644 skills/review-pr/SKILL.md create mode 100644 tests/__init__.py create mode 100644 tests/fixtures/KafkaPatterns.java create mode 100644 tests/fixtures/MarkdownMsg.tsx create mode 100644 tests/fixtures/Sample.cs create mode 100644 tests/fixtures/SampleJava.java create mode 100644 tests/fixtures/SpringDI.java create mode 100644 tests/fixtures/TemporalWorkflow.java create mode 100644 tests/fixtures/__tests__/UserService.ts create mode 100644 tests/fixtures/alias_importer.ts create mode 100644 tests/fixtures/caller_example.py create mode 100644 tests/fixtures/detect_changes_sample.json create mode 100644 tests/fixtures/multi_call_example.py create mode 100644 tests/fixtures/sample.R create mode 100644 tests/fixtures/sample.c create mode 100644 tests/fixtures/sample.cpp create mode 100644 tests/fixtures/sample.dart create mode 100644 tests/fixtures/sample.ex create mode 100644 tests/fixtures/sample.gd create mode 100644 tests/fixtures/sample.hh create mode 100644 tests/fixtures/sample.jl create mode 100644 tests/fixtures/sample.kt create mode 100644 tests/fixtures/sample.lua create mode 100644 tests/fixtures/sample.luau create mode 100644 tests/fixtures/sample.m create mode 100644 tests/fixtures/sample.nix create mode 100644 tests/fixtures/sample.php create mode 100644 tests/fixtures/sample.pl create mode 100644 tests/fixtures/sample.rb create mode 100644 tests/fixtures/sample.res create mode 100644 tests/fixtures/sample.resi create mode 100644 tests/fixtures/sample.scala create mode 100644 tests/fixtures/sample.sh create mode 100644 tests/fixtures/sample.sol create mode 100644 tests/fixtures/sample.sql create mode 100644 tests/fixtures/sample.sv create mode 100644 tests/fixtures/sample.swift create mode 100644 tests/fixtures/sample.xs create mode 100644 tests/fixtures/sample_bun.test.ts create mode 100644 tests/fixtures/sample_callback_refs.py create mode 100644 tests/fixtures/sample_databricks_export.py create mode 100644 tests/fixtures/sample_databricks_notebook.ipynb create mode 100644 tests/fixtures/sample_go.go create mode 100644 tests/fixtures/sample_lib.sh create mode 100644 tests/fixtures/sample_map_dispatch.py create mode 100644 tests/fixtures/sample_map_dispatch.ts create mode 100644 tests/fixtures/sample_mocha.test.ts create mode 100644 tests/fixtures/sample_module.nix create mode 100644 tests/fixtures/sample_notebook.ipynb create mode 100644 tests/fixtures/sample_python.py create mode 100644 tests/fixtures/sample_rust.rs create mode 100644 tests/fixtures/sample_typescript.ts create mode 100644 tests/fixtures/sample_vitest.test.ts create mode 100644 tests/fixtures/sample_vue.vue create mode 100644 tests/fixtures/src/lib/utils.ts create mode 100644 tests/fixtures/test_sample.R create mode 100644 tests/fixtures/test_sample.py create mode 100644 tests/fixtures/tsconfig.json create mode 100644 tests/test_action_render.py create mode 100644 tests/test_changes.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_cli_install.py create mode 100644 tests/test_communities.py create mode 100644 tests/test_context_savings.py create mode 100644 tests/test_custom_languages.py create mode 100644 tests/test_daemon.py create mode 100644 tests/test_embeddings.py create mode 100644 tests/test_enrich.py create mode 100644 tests/test_eval.py create mode 100644 tests/test_flows.py create mode 100644 tests/test_fts_sync.py create mode 100644 tests/test_graph.py create mode 100644 tests/test_hints.py create mode 100644 tests/test_incremental.py create mode 100644 tests/test_integration_git.py create mode 100644 tests/test_integration_v2.py create mode 100644 tests/test_main.py create mode 100644 tests/test_migrations.py create mode 100644 tests/test_multilang.py create mode 100644 tests/test_notebook.py create mode 100644 tests/test_parser.py create mode 100644 tests/test_postprocessing.py create mode 100644 tests/test_prompts.py create mode 100644 tests/test_refactor.py create mode 100644 tests/test_registry.py create mode 100644 tests/test_search.py create mode 100644 tests/test_skills.py create mode 100644 tests/test_tools.py create mode 100644 tests/test_transactions.py create mode 100644 tests/test_tsconfig_resolver.py create mode 100644 tests/test_visualization.py create mode 100644 tests/test_wiki.py create mode 100644 uv.lock diff --git a/.beads/.gitignore b/.beads/.gitignore new file mode 100644 index 0000000..eb82c48 --- /dev/null +++ b/.beads/.gitignore @@ -0,0 +1,72 @@ +# Dolt database (managed by Dolt, not git) +dolt/ + +# Runtime files +bd.sock +bd.sock.startlock +sync-state.json +last-touched +.exclusive-lock + +# Daemon runtime (lock, log, pid) +daemon.* + +# Interactions log (runtime, not versioned) +interactions.jsonl + +# Push state (runtime, per-machine) +push-state.json + +# Lock files (various runtime locks) +*.lock + +# Credential key (encryption key for federation peer auth — never commit) +.beads-credential-key + +# Local version tracking (prevents upgrade notification spam after git ops) +.local_version + +# Worktree redirect file (contains relative path to main repo's .beads/) +# Must not be committed as paths would be wrong in other clones +redirect + +# Sync state (local-only, per-machine) +# These files are machine-specific and should not be shared across clones +.sync.lock +export-state/ +export-state.json + +# Ephemeral store (SQLite - wisps/molecules, intentionally not versioned) +ephemeral.sqlite3 +ephemeral.sqlite3-journal +ephemeral.sqlite3-wal +ephemeral.sqlite3-shm + +# Dolt server management (auto-started by bd) +dolt-server.pid +dolt-server.log +dolt-server.lock +dolt-server.port +dolt-server.activity + +# Corrupt backup directories (created by bd doctor --fix recovery) +*.corrupt.backup/ + +# Backup data (auto-exported JSONL, local-only) +backup/ + +# Per-project environment file (Dolt connection config, GH#2520) +.env + +# Legacy files (from pre-Dolt versions) +*.db +*.db?* +*.db-journal +*.db-wal +*.db-shm +db.sqlite +bd.db +# NOTE: Do NOT add negation patterns here. +# They would override fork protection in .git/info/exclude. +# Config files (metadata.json, config.yaml) are tracked by git by default +# since no pattern above ignores them. diff --git a/.beads/README.md b/.beads/README.md new file mode 100644 index 0000000..dbfe363 --- /dev/null +++ b/.beads/README.md @@ -0,0 +1,81 @@ +# Beads - AI-Native Issue Tracking + +Welcome to Beads! This repository uses **Beads** for issue tracking - a modern, AI-native tool designed to live directly in your codebase alongside your code. + +## What is Beads? + +Beads is issue tracking that lives in your repo, making it perfect for AI coding agents and developers who want their issues close to their code. No web UI required - everything works through the CLI and integrates seamlessly with git. + +**Learn more:** [github.com/steveyegge/beads](https://github.com/steveyegge/beads) + +## Quick Start + +### Essential Commands + +```bash +# Create new issues +bd create "Add user authentication" + +# View all issues +bd list + +# View issue details +bd show + +# Update issue status +bd update --claim +bd update --status done + +# Sync with Dolt remote +bd dolt push +``` + +### Working with Issues + +Issues in Beads are: +- **Git-native**: Stored in Dolt database with version control and branching +- **AI-friendly**: CLI-first design works perfectly with AI coding agents +- **Branch-aware**: Issues can follow your branch workflow +- **Always in sync**: Auto-syncs with your commits + +## Why Beads? + +✨ **AI-Native Design** +- Built specifically for AI-assisted development workflows +- CLI-first interface works seamlessly with AI coding agents +- No context switching to web UIs + +🚀 **Developer Focused** +- Issues live in your repo, right next to your code +- Works offline, syncs when you push +- Fast, lightweight, and stays out of your way + +🔧 **Git Integration** +- Automatic sync with git commits +- Branch-aware issue tracking +- Dolt-native three-way merge resolution + +## Get Started with Beads + +Try Beads in your own projects: + +```bash +# Install Beads +curl -sSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash + +# Initialize in your repo +bd init + +# Create your first issue +bd create "Try out Beads" +``` + +## Learn More + +- **Documentation**: [github.com/steveyegge/beads/docs](https://github.com/steveyegge/beads/tree/main/docs) +- **Quick Start Guide**: Run `bd quickstart` +- **Examples**: [github.com/steveyegge/beads/examples](https://github.com/steveyegge/beads/tree/main/examples) + +--- + +*Beads: Issue tracking that moves at the speed of thought* ⚡ diff --git a/.beads/config.yaml b/.beads/config.yaml new file mode 100644 index 0000000..232b151 --- /dev/null +++ b/.beads/config.yaml @@ -0,0 +1,54 @@ +# Beads Configuration File +# This file configures default behavior for all bd commands in this repository +# All settings can also be set via environment variables (BD_* prefix) +# or overridden with command-line flags + +# Issue prefix for this repository (used by bd init) +# If not set, bd init will auto-detect from directory name +# Example: issue-prefix: "myproject" creates issues like "myproject-1", "myproject-2", etc. +# issue-prefix: "" + +# Use no-db mode: JSONL-only, no Dolt database +# When true, bd will use .beads/issues.jsonl as the source of truth +# no-db: false + +# Enable JSON output by default +# json: false + +# Feedback title formatting for mutating commands (create/update/close/dep/edit) +# 0 = hide titles, N > 0 = truncate to N characters +# output: +# title-length: 255 + +# Default actor for audit trails (overridden by BEADS_ACTOR or --actor) +# actor: "" + +# Export events (audit trail) to .beads/events.jsonl on each flush/sync +# When enabled, new events are appended incrementally using a high-water mark. +# Use 'bd export --events' to trigger manually regardless of this setting. +# events-export: false + +# Multi-repo configuration (experimental - bd-307) +# Allows hydrating from multiple repositories and routing writes to the correct database +# repos: +# primary: "." # Primary repo (where this database lives) +# additional: # Additional repos to hydrate from (read-only) +# - ~/beads-planning # Personal planning repo +# - ~/work-planning # Work planning repo + +# JSONL backup (periodic export for off-machine recovery) +# Auto-enabled when a git remote exists. Override explicitly: +# backup: +# enabled: false # Disable auto-backup entirely +# interval: 15m # Minimum time between auto-exports +# git-push: false # Disable git push (export locally only) +# git-repo: "" # Separate git repo for backups (default: project repo) + +# Integration settings (access with 'bd config get/set') +# These are stored in the database, not in this file: +# - jira.url +# - jira.project +# - linear.url +# - linear.api-key +# - github.org +# - github.repo diff --git a/.beads/hooks/post-checkout b/.beads/hooks/post-checkout new file mode 100755 index 0000000..67ad327 --- /dev/null +++ b/.beads/hooks/post-checkout @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run post-checkout "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'post-checkout' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run post-checkout "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'post-checkout'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/post-merge b/.beads/hooks/post-merge new file mode 100755 index 0000000..a731aec --- /dev/null +++ b/.beads/hooks/post-merge @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run post-merge "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'post-merge' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run post-merge "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'post-merge'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/pre-commit b/.beads/hooks/pre-commit new file mode 100755 index 0000000..02cf2ac --- /dev/null +++ b/.beads/hooks/pre-commit @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run pre-commit "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'pre-commit' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run pre-commit "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'pre-commit'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/pre-push b/.beads/hooks/pre-push new file mode 100755 index 0000000..7918492 --- /dev/null +++ b/.beads/hooks/pre-push @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run pre-push "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'pre-push' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run pre-push "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'pre-push'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/hooks/prepare-commit-msg b/.beads/hooks/prepare-commit-msg new file mode 100755 index 0000000..c0c3ce1 --- /dev/null +++ b/.beads/hooks/prepare-commit-msg @@ -0,0 +1,24 @@ +#!/usr/bin/env sh +# --- BEGIN BEADS INTEGRATION v1.0.0 --- +# This section is managed by beads. Do not remove these markers. +if command -v bd >/dev/null 2>&1; then + export BD_GIT_HOOK=1 + _bd_timeout=${BEADS_HOOK_TIMEOUT:-300} + if command -v timeout >/dev/null 2>&1; then + timeout "$_bd_timeout" bd hooks run prepare-commit-msg "$@" + _bd_exit=$? + if [ $_bd_exit -eq 124 ]; then + echo >&2 "beads: hook 'prepare-commit-msg' timed out after ${_bd_timeout}s — continuing without beads" + _bd_exit=0 + fi + else + bd hooks run prepare-commit-msg "$@" + _bd_exit=$? + fi + if [ $_bd_exit -eq 3 ]; then + echo >&2 "beads: database not initialized — skipping hook 'prepare-commit-msg'" + _bd_exit=0 + fi + if [ $_bd_exit -ne 0 ]; then exit $_bd_exit; fi +fi +# --- END BEADS INTEGRATION v1.0.0 --- diff --git a/.beads/metadata.json b/.beads/metadata.json new file mode 100644 index 0000000..7a934e6 --- /dev/null +++ b/.beads/metadata.json @@ -0,0 +1,7 @@ +{ + "database": "dolt", + "backend": "dolt", + "dolt_mode": "embedded", + "dolt_database": "code_review_graph", + "project_id": "487c4722-5abe-4a00-92da-9e60064d65d0" +} \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..4168e75 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,94 @@ +name: Bug report +description: Report a defect in code-review-graph (CLI, MCP server, parser, or graph store) +title: "[Bug]: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report a bug. Before filing, please check the + [troubleshooting guide](https://github.com/tirth8205/code-review-graph/blob/main/docs/TROUBLESHOOTING.md) + and search existing issues for duplicates. + - type: input + id: crg-version + attributes: + label: code-review-graph version + description: >- + Output of `pip show code-review-graph` (or the version reported by + `code-review-graph status`). + placeholder: "2.3.5" + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - macOS + - Linux + - Windows + - Other (describe in "Additional context") + validations: + required: true + - type: input + id: python-version + attributes: + label: Python version + description: Output of `python --version`. Python 3.10+ is required. + placeholder: "3.12.4" + validations: + required: true + - type: dropdown + id: ai-platform + attributes: + label: AI platform + description: Which AI coding tool were you using when you hit the bug? + options: + - claude-code + - cursor + - codex + - windsurf + - zed + - opencode + - copilot + - other (describe in "Additional context") + validations: + required: true + - type: textarea + id: status-output + attributes: + label: Output of `code-review-graph status` + description: >- + Run `code-review-graph status` in the affected repository and paste the full + output. If the command itself fails, paste the error instead. + render: shell + validations: + required: true + - type: textarea + id: repro-steps + attributes: + label: Steps to reproduce + description: Exact commands or actions, in order, that trigger the bug. + placeholder: | + 1. code-review-graph build + 2. code-review-graph detect-changes --brief + 3. ... + validations: + required: true + - type: textarea + id: expected-actual + attributes: + label: Expected vs actual behavior + description: What did you expect to happen, and what happened instead? + placeholder: | + Expected: ... + Actual: ... + validations: + required: true + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Logs, stack traces, screenshots, or anything else that helps. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..08f27c9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Questions and ideas (GitHub Discussions) + url: https://github.com/tirth8205/code-review-graph/discussions + about: Ask questions, share setups, and discuss ideas before filing an issue. + - name: Troubleshooting guide + url: https://github.com/tirth8205/code-review-graph/blob/main/docs/TROUBLESHOOTING.md + about: Fixes for common installation, build, and MCP connection problems. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..8b153a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,54 @@ +name: Feature request +description: Suggest a new capability or an improvement to code-review-graph +title: "[Feature]: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Thanks for the suggestion! For requests to support a new AI coding tool, + please use the **Platform support request** template instead. + - type: textarea + id: problem + attributes: + label: Problem + description: >- + What problem are you trying to solve? What is painful or impossible today? + validations: + required: true + - type: textarea + id: proposed-solution + attributes: + label: Proposed solution + description: How would you like this to work? CLI flags, MCP tool shape, etc. + validations: + required: true + - type: dropdown + id: area + attributes: + label: Affected area + options: + - Parser / language support + - Graph store / SQLite + - MCP tools / server + - CLI + - Visualization + - Embeddings / search + - Docs + - Other + validations: + required: false + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Other approaches or workarounds you have tried or considered. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Links, examples from other tools, or anything else that helps. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/platform_request.yml b/.github/ISSUE_TEMPLATE/platform_request.yml new file mode 100644 index 0000000..8f8b31b --- /dev/null +++ b/.github/ISSUE_TEMPLATE/platform_request.yml @@ -0,0 +1,62 @@ +name: Platform support request +description: Request support for a new AI coding tool (MCP config, hooks, or skills) +title: "[Platform]: " +labels: ["enhancement", "platform-support"] +body: + - type: markdown + attributes: + value: | + code-review-graph already configures Claude Code, Codex, Cursor, Windsurf, Zed, + Continue, OpenCode, Antigravity, Gemini CLI, Qwen, Kiro, Qoder, and GitHub + Copilot. Use this form to request support for another AI coding tool. + - type: input + id: platform-name + attributes: + label: Platform name + description: The AI coding tool you want supported. + placeholder: "e.g. Aider" + validations: + required: true + - type: input + id: platform-docs + attributes: + label: Link to the platform's MCP / configuration docs + description: Official documentation describing how the tool consumes MCP servers. + placeholder: "https://..." + validations: + required: true + - type: dropdown + id: mcp-support + attributes: + label: Does the platform support MCP? + options: + - Yes — stdio transport + - Yes — HTTP / SSE transport + - Yes — both transports + - "No" + - Unknown + validations: + required: true + - type: textarea + id: config-location + attributes: + label: Where does its MCP configuration live? + description: >- + Config file path(s) and format, if known — e.g. `~/.tool/mcp.json`, + project-level `.tool/settings.json`, TOML, etc. + validations: + required: false + - type: textarea + id: additional-context + attributes: + label: Additional context + description: Hooks/skills support, rules-file injection points, or other details. + validations: + required: false + - type: checkboxes + id: testing + attributes: + label: Testing + options: + - label: I have this tool installed and can test a pre-release integration. + required: false diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..eed586b --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,34 @@ +# Pull Request + +## Linked issue + + + +Closes # + +## What & why + + + +## How it was tested + + + +```bash +uv run pytest tests/ --tb=short -q +uv run ruff check code_review_graph/ +uv run mypy code_review_graph/ --ignore-missing-imports --no-strict-optional +``` + +## Checklist + + + +- [ ] Tests added for new functionality +- [ ] All tests pass: `uv run pytest tests/ --tb=short -q` +- [ ] Linting passes: `uv run ruff check code_review_graph/` +- [ ] Type checking passes: `uv run mypy code_review_graph/ --ignore-missing-imports --no-strict-optional` +- [ ] Lines are at most 100 characters +- [ ] Docs updated where behavior changed (README, `docs/`, docstrings) diff --git a/.github/code-review-graph.instruction.md b/.github/code-review-graph.instruction.md new file mode 100644 index 0000000..0cdcabb --- /dev/null +++ b/.github/code-review-graph.instruction.md @@ -0,0 +1,43 @@ +--- +applyTo: '**' +description: Use code-review-graph MCP tools for token-efficient codebase exploration and code review instead of built-in file/search tools. +--- + + +## MCP Tools: code-review-graph + +**IMPORTANT: This project has a knowledge graph. ALWAYS use the +code-review-graph MCP tools BEFORE using #tool:read/readFile #tool:search/fileSearch #tool:search/textSearch to explore +the codebase.** The graph is faster, cheaper (fewer tokens), and gives +you structural context (callers, dependents, test coverage) that file +scanning cannot. + +### When to use graph tools FIRST + +- **Exploring code**: `semantic_search_nodes_tool` or `query_graph_tool` instead of Grep +- **Understanding impact**: `get_impact_radius_tool` instead of manually tracing imports +- **Code review**: `detect_changes_tool` + `get_review_context_tool` instead of reading entire files +- **Finding relationships**: `query_graph_tool` with callers_of/callees_of/imports_of/tests_for +- **Architecture questions**: `get_architecture_overview_tool` + `list_communities_tool` + +Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need. + +### Key Tools + +| Tool | Use when | +| ------ | ---------- | +| `detect_changes_tool` | Reviewing code changes — gives risk-scored analysis | +| `get_review_context_tool` | Need source snippets for review — token-efficient | +| `get_impact_radius_tool` | Understanding blast radius of a change | +| `get_affected_flows_tool` | Finding which execution paths are impacted | +| `query_graph_tool` | Tracing callers, callees, imports, tests, dependencies | +| `semantic_search_nodes_tool` | Finding functions/classes by name or keyword | +| `get_architecture_overview_tool` | Understanding high-level codebase structure | +| `refactor_tool` | Planning renames, finding dead code | + +### Workflow + +1. The graph auto-updates on file changes (via hooks). +2. Use `detect_changes_tool` for code review. +3. Use `get_affected_flows_tool` to understand impact. +4. Use `query_graph_tool` pattern="tests_for" to check coverage. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..0a9468f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,38 @@ + +## MCP Tools: code-review-graph + +**IMPORTANT: This project has a knowledge graph. ALWAYS use the +code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore +the codebase.** The graph is faster, cheaper (fewer tokens), and gives +you structural context (callers, dependents, test coverage) that file +scanning cannot. + +### When to use graph tools FIRST + +- **Exploring code**: `semantic_search_nodes_tool` or `query_graph_tool` instead of Grep +- **Understanding impact**: `get_impact_radius_tool` instead of manually tracing imports +- **Code review**: `detect_changes_tool` + `get_review_context_tool` instead of reading entire files +- **Finding relationships**: `query_graph_tool` with callers_of/callees_of/imports_of/tests_for +- **Architecture questions**: `get_architecture_overview_tool` + `list_communities_tool` + +Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need. + +### Key Tools + +| Tool | Use when | +| ------ | ---------- | +| `detect_changes_tool` | Reviewing code changes — gives risk-scored analysis | +| `get_review_context_tool` | Need source snippets for review — token-efficient | +| `get_impact_radius_tool` | Understanding blast radius of a change | +| `get_affected_flows_tool` | Finding which execution paths are impacted | +| `query_graph_tool` | Tracing callers, callees, imports, tests, dependencies | +| `semantic_search_nodes_tool` | Finding functions/classes by name or keyword | +| `get_architecture_overview_tool` | Understanding high-level codebase structure | +| `refactor_tool` | Planning renames, finding dead code | + +### Workflow + +1. The graph auto-updates on file changes (via hooks). +2. Use `detect_changes_tool` for code review. +3. Use `get_affected_flows_tool` to understand impact. +4. Use `query_graph_tool` pattern="tests_for" to check coverage. diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a03c823 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + labels: + - "dependencies" + commit-message: + prefix: "deps" + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 3 + labels: + - "dependencies" + commit-message: + prefix: "ci" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da10c42 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install dependencies + run: pip install -e ".[dev]" + - name: Lint with ruff + run: ruff check code_review_graph/ + + type-check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install dependencies + run: pip install -e ".[dev]" mypy types-networkx + - name: Run mypy + run: mypy code_review_graph/ --ignore-missing-imports --no-strict-optional + + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + - name: Install bandit + run: pip install bandit[toml] + - name: Run bandit security scan + run: bandit -r code_review_graph/ -c pyproject.toml + + schema-sync: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Python/VSCode schema versions match + run: | + PY_VER=$(grep -oP 'LATEST_VERSION\s*=\s*max\(MIGRATIONS\.keys\(\)\)' code_review_graph/migrations.py > /dev/null && python3 -c " + import re, ast + src = open('code_review_graph/migrations.py').read() + m = re.search(r'MIGRATIONS:\s*dict\[.*?\]\s*=\s*\{([^}]+)\}', src) + keys = [int(k.strip().rstrip(':')) for k in re.findall(r'(\d+):', m.group(1))] + print(max(keys)) + ") + TS_VER=$(grep -oP 'SUPPORTED_SCHEMA_VERSION\s*=\s*\K\d+' code-review-graph-vscode/src/backend/sqlite.ts) + echo "Python LATEST_VERSION: $PY_VER" + echo "VSCode SUPPORTED_SCHEMA_VERSION: $TS_VER" + if [ "$PY_VER" != "$TS_VER" ]; then + echo "::error::Schema version mismatch! Python=$PY_VER, VSCode=$TS_VER" + exit 1 + fi + echo "Schema versions in sync." + + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + steps: + - uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: pip install -e ".[dev]" pytest-cov + - name: Run tests with coverage + run: pytest --tb=short -q --cov=code_review_graph --cov-report=term-missing --cov-fail-under=65 diff --git a/.github/workflows/eval.yml b/.github/workflows/eval.yml new file mode 100644 index 0000000..e60eedf --- /dev/null +++ b/.github/workflows/eval.yml @@ -0,0 +1,65 @@ +name: Weekly Eval + +# Report-only benchmark run. This workflow surfaces benchmark drift in the +# job summary and the uploaded CSV artifact, but it must NOT fail the default +# branch on regressions (yet) — eval failures are informational until the +# co-change baseline has enough history to set thresholds against. + +on: + schedule: + - cron: "23 6 * * 1" # Mondays 06:23 UTC (off-minute to dodge load spikes) + workflow_dispatch: + +permissions: + contents: read + +jobs: + eval: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install with eval extras + run: pip install -e ".[eval]" + + - name: Run benchmarks (2 smallest pinned configs) + # httpx (~60 files) and flask (~83 files) are the two smallest + # pinned repos. Report-only: `|| true` keeps regressions and + # transient clone failures from failing the default branch. + run: | + code-review-graph eval \ + --repo httpx,flask \ + --benchmark token_efficiency,impact_accuracy,agent_baseline \ + --output-dir evaluate/results || true + + - name: Upload result CSVs + if: always() + uses: actions/upload-artifact@v4 + with: + name: eval-results-${{ github.run_id }} + path: evaluate/results/*.csv + if-no-files-found: warn + retention-days: 90 + + - name: Write job summary + if: always() + run: | + python - <<'PY' >> "$GITHUB_STEP_SUMMARY" + from code_review_graph.eval.reporter import generate_full_report + + print("# Weekly eval (report-only)") + print() + print( + "Configs: `httpx`, `flask` (the two smallest pinned repos). " + "Regressions are reported here and in the CSV artifact but do " + "not fail CI." + ) + print() + print(generate_full_report("evaluate/results")) + PY diff --git a/.github/workflows/pr-review.yml b/.github/workflows/pr-review.yml new file mode 100644 index 0000000..dde491b --- /dev/null +++ b/.github/workflows/pr-review.yml @@ -0,0 +1,21 @@ +# Dogfoods the local composite action (action.yml at the repo root) on PRs. +name: PR Review + +on: + pull_request: + branches: [main] + +permissions: + contents: read + pull-requests: write + +jobs: + review: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run code-review-graph review + uses: ./ + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + fail-on-risk: none diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..477cb03 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,32 @@ +name: Publish to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + publish: + runs-on: ubuntu-latest + environment: pypi + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install build tools + run: pip install build twine + + - name: Build package + run: python -m build + + - name: Publish to PyPI + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + run: twine upload dist/* diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5855e75 --- /dev/null +++ b/.gitignore @@ -0,0 +1,94 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +*.egg-info/ +dist/ +build/ +*.egg +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# Graph database +*.db +*.db-journal +*.db-wal +*.db-shm +.code-review-graph/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Node +node_modules/ + +# VS Code extension build artifacts +code-review-graph-vscode/dist/ +*.vsix + +# Claude Code +.claude/ +.claude-plugin/ + +# Qoder +.qoder/ +QODER.md + +# Coverage +htmlcov/ +.coverage +.coverage.* + +# pytest +.pytest_cache/ + +# mypy +.mypy_cache/ + +# Excalidraw source files (PNGs are tracked, sources are not) +*.excalidraw +diagrams/export_pngs.mjs + +# Evaluation (generated output — test repos and reports are local; canonical +# CSVs under evaluate/results/ are tracked as evidence for the numbers in +# docs/REPRODUCING.md, so contributors can verify without rerunning). +evaluate/test_repos/ +evaluate/reports/ +evaluate/standalone_token_benchmark.json +evaluate/eval-run.log + +# Superpowers brainstorm docs +.superpowers/ +docs/superpowers/ + +# Draft/duplicate assets +docs/assets/marketing-diagram* + +# One-off docs (audits, analyses, plans, articles) +medium/ +Quality-Audit-Report.docx +accessibility-audit.md +cross-audit-synthesis.md +design-critique.md +design-handoff.md +design-system-audit.md +research-synthesis.md +code-review-graph-analysis.md +SCALING_AND_TOKEN_EFFICIENCY_PLAN.md + +# Beads / Dolt files (added by bd init) +.dolt/ +.beads-credential-key diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..b38c21d --- /dev/null +++ b/.mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "code-review-graph": { + "command": "uvx", + "args": ["code-review-graph", "serve"] + } + } +} diff --git a/.serena/.gitignore b/.serena/.gitignore new file mode 100644 index 0000000..2e510af --- /dev/null +++ b/.serena/.gitignore @@ -0,0 +1,2 @@ +/cache +/project.local.yml diff --git a/.serena/project.yml b/.serena/project.yml new file mode 100644 index 0000000..832c638 --- /dev/null +++ b/.serena/project.yml @@ -0,0 +1,154 @@ +# the name by which the project can be referenced within Serena +project_name: "code-review-graph" + + +# list of languages for which language servers are started; choose from: +# al bash clojure cpp csharp +# csharp_omnisharp dart elixir elm erlang +# fortran fsharp go groovy haskell +# haxe java julia kotlin lua +# markdown +# matlab nix pascal perl php +# php_phpactor powershell python python_jedi r +# rego ruby ruby_solargraph rust scala +# swift terraform toml typescript typescript_vts +# vue yaml zig +# (This list may be outdated. For the current list, see values of Language enum here: +# https://github.com/oraios/serena/blob/main/src/solidlsp/ls_config.py +# For some languages, there are alternative language servers, e.g. csharp_omnisharp, ruby_solargraph.) +# Note: +# - For C, use cpp +# - For JavaScript, use typescript +# - For Free Pascal/Lazarus, use pascal +# Special requirements: +# Some languages require additional setup/installations. +# See here for details: https://oraios.github.io/serena/01-about/020_programming-languages.html#language-servers +# When using multiple languages, the first language server that supports a given file will be used for that file. +# The first language is the default language and the respective language server will be used as a fallback. +# Note that when using the JetBrains backend, language servers are not used and this list is correspondingly ignored. +languages: +- python + +# the encoding used by text files in the project +# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings +encoding: "utf-8" + +# line ending convention to use when writing source files. +# Possible values: unset (use global setting), "lf", "crlf", or "native" (platform default) +# This does not affect Serena's own files (e.g. memories and configuration files), which always use native line endings. +line_ending: + +# The language backend to use for this project. +# If not set, the global setting from serena_config.yml is used. +# Valid values: LSP, JetBrains +# Note: the backend is fixed at startup. If a project with a different backend +# is activated post-init, an error will be returned. +language_backend: + +# whether to use project's .gitignore files to ignore files +ignore_all_files_in_gitignore: true + +# advanced configuration option allowing to configure language server-specific options. +# Maps the language key to the options. +# Have a look at the docstring of the constructors of the LS implementations within solidlsp (e.g., for C# or PHP) to see which options are available. +# No documentation on options means no options are available. +ls_specific_settings: {} + +# list of additional paths to ignore in this project. +# Same syntax as gitignore, so you can use * and **. +# Note: global ignored_paths from serena_config.yml are also applied additively. +ignored_paths: [] + +# whether the project is in read-only mode +# If set to true, all editing tools will be disabled and attempts to use them will result in an error +# Added on 2025-04-18 +read_only: false + +# list of tool names to exclude. +# This extends the existing exclusions (e.g. from the global configuration) +# +# Below is the complete list of tools for convenience. +# To make sure you have the latest list of tools, and to view their descriptions, +# execute `uv run scripts/print_tool_overview.py`. +# +# * `activate_project`: Activates a project based on the project name or path. +# * `check_onboarding_performed`: Checks whether project onboarding was already performed. +# * `create_text_file`: Creates/overwrites a file in the project directory. +# * `delete_memory`: Delete a memory file. Should only happen if a user asks for it explicitly, +# for example by saying that the information retrieved from a memory file is no longer correct +# or no longer relevant for the project. +# * `edit_memory`: Replaces content matching a regular expression in a memory. +# * `execute_shell_command`: Executes a shell command. +# * `find_file`: Finds files in the given relative paths +# * `find_referencing_symbols`: Finds symbols that reference the given symbol using the language server backend +# * `find_symbol`: Performs a global (or local) search using the language server backend. +# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes. +# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file. +# * `initial_instructions`: Provides instructions Serena usage (i.e. the 'Serena Instructions Manual') +# for clients that do not read the initial instructions when the MCP server is connected. +# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol. +# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol. +# * `list_dir`: Lists files and directories in the given directory (optionally with recursion). +# * `list_memories`: List available memories. Any memory can be read using the `read_memory` tool. +# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building). +# * `read_file`: Reads a file within the project directory. +# * `read_memory`: Read the content of a memory file. This tool should only be used if the information +# is relevant to the current task. You can infer whether the information +# is relevant from the memory file name. +# You should not read the same memory file multiple times in the same conversation. +# * `rename_memory`: Renames or moves a memory. Moving between project and global scope is supported +# (e.g., renaming "global/foo" to "bar" moves it from global to project scope). +# * `rename_symbol`: Renames a symbol throughout the codebase using language server refactoring capabilities. +# For JB, we use a separate tool. +# * `replace_content`: Replaces content in a file (optionally using regular expressions). +# * `replace_symbol_body`: Replaces the full definition of a symbol using the language server backend. +# * `safe_delete_symbol`: +# * `search_for_pattern`: Performs a search for a pattern in the project. +# * `write_memory`: Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format. +# The memory name should be meaningful. +excluded_tools: [] + +# list of tools to include that would otherwise be disabled (particularly optional tools that are disabled by default). +# This extends the existing inclusions (e.g. from the global configuration). +included_optional_tools: [] + +# fixed set of tools to use as the base tool set (if non-empty), replacing Serena's default set of tools. +# This cannot be combined with non-empty excluded_tools or included_optional_tools. +fixed_tools: [] + +# list of mode names to that are always to be included in the set of active modes +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the base_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this setting overrides the global configuration. +# Set this to [] to disable base modes for this project. +# Set this to a list of mode names to always include the respective modes for this project. +base_modes: + +# list of mode names that are to be activated by default. +# The full set of modes to be activated is base_modes + default_modes. +# If the setting is undefined, the default_modes from the global configuration (serena_config.yml) apply. +# Otherwise, this overrides the setting from the global configuration (serena_config.yml). +# This setting can, in turn, be overridden by CLI parameters (--mode). +default_modes: + +# initial prompt for the project. It will always be given to the LLM upon activating the project +# (contrary to the memories, which are loaded on demand). +initial_prompt: "" + +# time budget (seconds) per tool call for the retrieval of additional symbol information +# such as docstrings or parameter information. +# This overrides the corresponding setting in the global configuration; see the documentation there. +# If null or missing, use the setting from the global configuration. +symbol_info_budget: + +# list of regex patterns which, when matched, mark a memory entry as read‑only. +# Extends the list from the global configuration, merging the two lists. +read_only_memory_patterns: [] + +# list of regex patterns for memories to completely ignore. +# Matching memories will not appear in list_memories or activate_project output +# and cannot be accessed via read_memory or write_memory. +# To access ignored memory files, use the read_file tool on the raw file path. +# Extends the list from the global configuration, merging the two lists. +# Example: ["_archive/.*", "_episodes/.*"] +ignored_memory_patterns: [] diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..418ea2c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,123 @@ +# Agent Instructions + +This project uses **bd** (beads) for issue tracking. Run `bd prime` for full workflow context. + +## Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work atomically +bd close # Complete work +bd dolt push # Push beads data to remote +``` + +## Non-Interactive Shell Commands + +**ALWAYS use non-interactive flags** with file operations to avoid hanging on confirmation prompts. + +Shell commands like `cp`, `mv`, and `rm` may be aliased to include `-i` (interactive) mode on some systems, causing the agent to hang indefinitely waiting for y/n input. + +**Use these forms instead:** +```bash +# Force overwrite without prompting +cp -f source dest # NOT: cp source dest +mv -f source dest # NOT: mv source dest +rm -f file # NOT: rm file + +# For recursive operations +rm -rf directory # NOT: rm -r directory +cp -rf source dest # NOT: cp -r source dest +``` + +**Other commands that may prompt:** +- `scp` - use `-o BatchMode=yes` for non-interactive +- `ssh` - use `-o BatchMode=yes` to fail instead of prompting +- `apt-get` - use `-y` flag +- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var + + +## Beads Issue Tracker + +This project uses **bd (beads)** for issue tracking. Run `bd prime` to see full workflow context and commands. + +### Quick Reference + +```bash +bd ready # Find available work +bd show # View issue details +bd update --claim # Claim work +bd close # Complete work +``` + +### Rules + +- Use `bd` for ALL task tracking — do NOT use TodoWrite, TaskCreate, or markdown TODO lists +- Run `bd prime` for detailed command reference and session close protocol +- Use `bd remember` for persistent knowledge — do NOT use MEMORY.md files + +## Session Completion + +**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds. + +**MANDATORY WORKFLOW:** + +1. **File issues for remaining work** - Create issues for anything that needs follow-up +2. **Run quality gates** (if code changed) - Tests, linters, builds +3. **Update issue status** - Close finished work, update in-progress items +4. **PUSH TO REMOTE** - This is MANDATORY: + ```bash + git pull --rebase + bd dolt push + git push + git status # MUST show "up to date with origin" + ``` +5. **Clean up** - Clear stashes, prune remote branches +6. **Verify** - All changes committed AND pushed +7. **Hand off** - Provide context for next session + +**CRITICAL RULES:** +- Work is NOT complete until `git push` succeeds +- NEVER stop before pushing - that leaves work stranded locally +- NEVER say "ready to push when you are" - YOU must push +- If push fails, resolve and retry until it succeeds + + + +## MCP Tools: code-review-graph + +**IMPORTANT: This project has a knowledge graph. ALWAYS use the +code-review-graph MCP tools BEFORE using Grep/Glob/Read to explore +the codebase.** The graph is faster, cheaper (fewer tokens), and gives +you structural context (callers, dependents, test coverage) that file +scanning cannot. + +### When to use graph tools FIRST + +- **Exploring code**: `semantic_search_nodes_tool` or `query_graph_tool` instead of Grep +- **Understanding impact**: `get_impact_radius_tool` instead of manually tracing imports +- **Code review**: `detect_changes_tool` + `get_review_context_tool` instead of reading entire files +- **Finding relationships**: `query_graph_tool` with callers_of/callees_of/imports_of/tests_for +- **Architecture questions**: `get_architecture_overview_tool` + `list_communities_tool` + +Fall back to Grep/Glob/Read **only** when the graph doesn't cover what you need. + +### Key Tools + +| Tool | Use when | +|------|----------| +| `detect_changes_tool` | Reviewing code changes — gives risk-scored analysis | +| `get_review_context_tool` | Need source snippets for review — token-efficient | +| `get_impact_radius_tool` | Understanding blast radius of a change | +| `get_affected_flows_tool` | Finding which execution paths are impacted | +| `query_graph_tool` | Tracing callers, callees, imports, tests, dependencies | +| `semantic_search_nodes_tool` | Finding functions/classes by name or keyword | +| `get_architecture_overview_tool` | Understanding high-level codebase structure | +| `refactor_tool` | Planning renames, finding dead code | + +### Workflow + +1. The graph auto-updates on file changes (via hooks). +2. Use `detect_changes_tool` for code review. +3. Use `get_affected_flows_tool` to understand impact. +4. Use `query_graph_tool` pattern="tests_for" to check coverage. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5632f8a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,821 @@ +# Changelog + +## [Unreleased] + +## [2.3.6] - 2026-06-10 + +**Community-response release.** Built from a full audit of every open PR, +issue, and discussion: community fixes merged with credit, verified defects +fixed (including two open Windows bugs), benchmark claims made independently +checkable, and the project's first self-hosted PR review bot — this repo now +reviews its own pull requests with its own graph. No breaking changes. + +### Added + +- **Custom languages without forking** (#320): drop a + `.code-review-graph/languages.toml` into your repo to index any grammar + shipped by tree-sitter-language-pack (extension map + node-type lists, + validated and capped, built-ins always win). See docs/CUSTOM_LANGUAGES.md. +- **GitHub Action** for risk-scored PR review comments: composite `action.yml` + builds/restores the graph from CI cache, runs `detect-changes` against the + PR base, and upserts a sticky comment with risk table, affected flows, test + gaps, and the Token Savings line. Dogfooded on this repo via + `.github/workflows/pr-review.yml`. See docs/GITHUB_ACTION.md. +- **`agent_baseline` eval benchmark**: compares graph queries against a + realistic grep-and-read-top-k agent baseline instead of the whole-corpus + strawman; wired into all six pinned eval configs. +- **Co-change ground truth for `impact_accuracy`**: predictions are now also + graded against files actually co-changed in the same commit; the legacy + metric is explicitly labelled "graph-derived (circular — upper bound)". +- **Weekly eval CI** (`.github/workflows/eval.yml`): report-only cron run of + the two smallest pinned configs with CSV artifacts and a job summary. +- **docs/FAQ.md**: how CRG compares to LSP, RAG, grep/agentic search, and + adjacent tools; when NOT to use it; verification steps; monorepo/worktree + and registry guidance. Linked from the README. +- GitHub issue forms (bug/feature/platform), a PR template mirroring the + CONTRIBUTING checklist, and dependabot config for pip + GitHub Actions. + +### Fixed + +- `store_file_batch` is now guarded against open transactions like its sibling + (#489, merged from community PR #529 by @Devilthelegend — thank you). +- **Windows: `daemon status` no longer crashes with WinError 87** (#511): + PID liveness now uses `OpenProcess`/`WaitForSingleObject` on win32 instead + of `os.kill(pid, 0)`. +- **Windows: CLI `detect-changes` mapped 0 functions** (#528): diff paths are + now remapped to absolute native paths before node lookup, matching the MCP + tool's behavior; also prevents the misleading "~100% token savings" line on + an empty result. +- Eval benchmarks no longer record failed runs as inflated wins: thrown + `get_review_context`/`analyze_changes` calls are marked `status=error` and + excluded from aggregates instead of producing naive/1 ratios or recall=1.0. +- Unknown embedding provider names now raise a clear error listing valid + providers instead of silently falling back to the local model. +- The five analysis MCP tools and the wiki-page tool no longer leak SQLite + connections (try/finally `store.close()`). +- `install` git hooks now resolve the real hooks directory via + `git rev-parse --git-path hooks`, so linked worktrees and `core.hooksPath` + (husky) setups get a working pre-commit hook (#313 residue). +- Shipped `hooks/hooks.json` and `hooks/session-start.sh` now drain stdin, + matching the generated configs (#493 class). +- `fastmcp` is now capped `<4` so the next major cannot silently break the + server (the #488 failure mode). + +### Changed + +- README benchmarks section now leads with the ~82x median per-question + reduction (528x presented as the best case, not the headline), the + limitations block is visible instead of collapsed, and "100% impact recall" + is reframed as a graph-derived upper bound alongside the new co-change + metric. +- Stale translated READMEs (zh-CN, ja-JP, ko-KR, hi-IN) carry a staleness + banner; the zh-CN benchmark captions and docs/USAGE.md no longer contradict + the English README. +- SECURITY.md now points to GitHub private vulnerability reporting as the + canonical channel. + +## [2.3.5] - 2026-05-25 + +**Real-time token savings, visible to humans.** The estimated context-savings +metric introduced in 2.3.4 was JSON-only. In 2.3.5 it surfaces as a clean +boxed panel on the CLI and is verifiable against a real tokenizer in one +flag — so when you reach for `code-review-graph` to review a change, you +can immediately *see* how much of your context window the graph just kept +out. No breaking changes. + +### Added — Token Savings (headline feature) + +- **Boxed `Token Savings` panel on every `--brief` CLI call.** Both + `code-review-graph detect-changes --brief` and the new + `code-review-graph update --brief` print a four-line panel: the full-context + baseline, the graph response size, total saved tokens with percent, and a + per-category breakdown (Functions / Tests / Risk / Other) that **sums + exactly** to the graph response size — no padding, no rounding magic. + + ```text + ┌─────────────────────── Token Savings ────────────────────────┐ + │ Full context would be: 12,921 tokens │ + │ Graph context used: 762 tokens │ + │ Saved: 12,159 tokens (~94%) │ + │ Breakdown: Functions 244 · Tests 191 · Risk 244 · Other 83 │ + └──────────────────────────────────────────────────────────────┘ + ``` + +- **`--verify` flag** cross-checks the displayed numbers against OpenAI's + `cl100k_base` tokenizer (the GPT-4 family). Adds a second + `Verified (tiktoken)` row to the panel showing the real token counts. + Requires `pip install tiktoken`. A one-time calibration across 222 mixed + source files (Python/JS/TS/Go/Rust/RST/MD) committed in + `docs/REPRODUCING.md` shows the `chars/4` approximation stays within + **+0.5%** of real tokens in aggregate; per-repo bias is bounded to ±12% + and the **ratio** stays stable because both sides of the divide are + equally biased. + +- **`code-review-graph update --brief`** — incremental update plus the same + risk + Token Savings panel in one command. Distinct from + `detect-changes --brief` (which is read-only against the existing graph). + Use `update --brief` when the graph might be stale (post-rebase, large + change set); use `detect-changes --brief` when hooks/`crg-daemon` have + already kept the graph fresh. + +### Added — Reproducible benchmarks + +- **`docs/REPRODUCING.md`** — end-to-end reproduction recipe with canonical + numbers, the tiktoken calibration table, and an explicit explanation of + the three different "token" benchmarks in the codebase and what each + measures. Two people running the recipe on different machines on + different days now produce **identical** numbers, within float rounding. +- **`multi_hop_retrieval` benchmark** — 11 hand-curated 2-step tool-chain + tasks (semantic_search → query_graph) across the 6 test repos. Average + score **0.909**. Per-task CSV in `evaluate/results/`. +- **`code-review-graph embed` CLI subcommand** — explicit shell-level access + to embedding generation. Previously only reachable via MCP, which made + the benchmark recipe awkward. + +### Changed — Deterministic eval pipeline + +- **Every config under `code_review_graph/eval/configs/*.yaml` now pins an + upstream SHA.** Previously every config used `commit: HEAD`, which made + benchmarks drift whenever upstream pushed. Pinned SHAs: express + `b4ab7d65`, fastapi `0227991a`, flask `a29f88ce`, gin `5c00df8a`, httpx + `b55d4635`, code-review-graph `84bde354`. +- **`nextjs.yaml` renamed to `code-review-graph.yaml`.** The historical + "nextjs" entry pointed at this repo, not a Next.js codebase. Renamed to + match reality. +- **`eval/runner.py` uses full clones with explicit `returncode` checks.** + Previously `--depth 50` silently fell back to `HEAD~1..HEAD` whenever a + pinned test-commit SHA was past the shallow window, producing benchmark + numbers tied to whichever HEAD the clone happened to grab. +- **Leiden community detection seeded** (`CRG_LEIDEN_SEED`, default `42`). + Previously unseeded — community IDs and sizes drifted run-to-run on the + same graph, breaking benchmark comparability. +- **`eval/runner.py` resolves repo paths absolutely before storing.** The + parser previously stored file_path as the path you passed in, so eval + builds and CLI/MCP builds could disagree, producing duplicate nodes for + the same source location. Fixed by `.resolve()` in the runner. +- **`eval/runner.py` calls `run_post_processing` after `full_build`.** + Previously the eval framework left FTS5 unpopulated (shadow tables + `nodes_fts_idx` and `nodes_fts_docsize` empty), so downstream search and + multi-hop benchmarks silently returned no results. + +### Changed — Search and embeddings + +- **`embeddings._node_to_text` is richer.** Embedded text per node now + includes the dotted form (`Parent.name`, e.g. `APIRoute.get_route_handler`), + the identifier split into words (`get route handler`), and the enclosing + module directory (`routing`, `dependencies`). Forces an automatic + re-embedding because the text hash changes. Lifts multi-hop benchmark + accuracy from **0.545 → 0.818**. +- **Identifier-aware search boost** (`search.extract_query_identifiers`). + Natural-language queries like *"Who advances the gin middleware chain + via Context.Next"* now have their dotted / snake_case / CamelCase tokens + extracted and used to boost matching qualified-names by 2.0× in hybrid + search. Combined with the richer embed text, multi-hop accuracy reaches + **0.909** (10 of 11 tasks pass). + +### Fixed + +- **Test-gap dedup in the brief summary.** If duplicate `qualified_names` + ever slip into the graph (e.g. after a path-normalization mismatch), + the `Untested:` line in the human summary now collapses to unique names. + The underlying `test_gaps` list still carries every entry. +- **`token_benchmark.py` warns when embeddings are missing.** The standalone + benchmark's default NL questions need semantic search to match anything; + without embeddings the benchmark used to silently report 0× reduction + ratios. Now logs an explicit warning pointing users to `embed`. + +### Documentation + +- **`docs/REPRODUCING.md`** (new). End-to-end recipe, canonical numbers, + the tiktoken calibration table, and a side-by-side explanation of the + three "token" benchmarks in the codebase. +- **`README.md` Token Savings section** (new collapsible block under + Usage). Plain-English explanation of `detect-changes --brief` vs + `update --brief` — read-only vs re-parses-first — with a side-by-side + decision table. +- **`docs/COMMANDS.md`** lists the new `--brief`, `--verify`, and `embed` + forms with an inline "which one?" comment block on the analysis pair. +- **Updated benchmark headline** to reflect today's pinned-SHA snapshot: + range **38× – 528×** (median ~82×) across the 6 repos, **100% impact + recall**, **F1 0.71** across 13 commits. Old numbers (73× – 895×) + reflected pre-fix conditions (leftover build artifacts, smaller graph + responses) and have been superseded. +- **9 Excalidraw diagrams** regenerated with current canonical numbers + (`diagrams/*.excalidraw`, source kept locally; PNG re-exports manual). + +### Demo + +- **`diagrams/context-savings-demo.gif`** — 44 s screencast showing both + CLI surfaces and the `--verify` cross-check. Rendered from + `diagrams/context-savings-demo.tape` (regenerable with `vhs`). + +## [2.3.4] - 2026-05-25 + +Focused reliability and token-efficiency release for MCP/CLI review workflows. No breaking changes. + +### Added + +- **Estimated context savings metadata** for graph-filtered review/impact/architecture responses. The new `context_savings` field is intentionally compact (`estimated`, `saved_tokens`, `saved_percent`) and uses the existing conservative character-count approximation rather than claiming exact tokenization. +- **CLI estimated savings line** for `code-review-graph detect-changes --brief`; full JSON output includes the same compact `context_savings` metadata. + +### Changed + +- **Architecture overview is compact by default**: `get_architecture_overview_tool` now defaults to `detail_level="minimal"`, dropping per-community member lists and aggregating cross-community edges by community pair. Full per-edge output remains available with `detail_level="standard"`. +- **Bounded change analysis**: `detect_changes_tool` can now cap very large changed-function and transitive-test frontiers with `CRG_MAX_CHANGED_FUNCS` and `CRG_MAX_TRANSITIVE_FRONTIER`, and can return a structured timeout error via `CRG_TOOL_TIMEOUT`. + +### Fixed + +- **Windows semantic search deadlock** (#508/#507): local embedding models are pre-warmed on the main thread on Windows before FastMCP starts worker dispatch. +- **Rust test detection** (#503/#502): Rust `#[test]` and common async test attributes now produce `Test` nodes. +- **Generated hook stdin handling** (#494/#493): Codex and Claude hook commands drain stdin to avoid caller-side broken pipes on large hook payloads. +- **Cross-file callers** (#486/#472): `callers_of` now returns cross-file callers even when same-file callers exist. +- **Graph path lookup** (#469): review, impact, and file-summary tools resolve user-facing paths to the path format stored in the graph. +- **Bundled MCP docs** (#485/#480): `get_docs_section` can load the packaged `LLM-OPTIMIZED-REFERENCE.md` from installed wheels. +- **Local embedding provider availability** (#484/#448): missing `sentence-transformers` now reports local provider unavailability instead of silently producing zero embeddings. +- **Dead-code response fields** (#481/#447): dead-code results now include `file_path`, `relative_path`, and `language` while preserving the legacy `file` key. +- **SVN root validation** (#456): MCP/daemon/registry root validation now accepts `.svn` working copies consistently. +- **CLI postprocess flags** (#487): `build --skip-postprocess` and `update --skip-flows` no longer run an extra full post-processing pass. + +### Documentation + +- Updated stale release-facing version references for 2.3.4. +- Replaced fragile language-count wording with current broad language and notebook support wording. +- Added the missing VS Code extension `0.2.2` changelog entry without changing the extension package version. + +### Tests + +- Added regression coverage for compact architecture overview output and #476 mitigation. +- Added tests for estimated context savings calculation, compact metadata shape, MCP metadata, CLI brief/JSON output, Rust test parsing, hook stdin draining, graph path resolution, dead-code fields, SVN root validation, CLI postprocess flags, embedding availability, and bounded detect-changes behavior. + +## [2.3.3] - 2026-05-08 + +Large additive release accumulated since v2.3.2 — 141 non-merge commits, 8 new languages/extensions, 5 new platform install targets, 6 new framework call resolvers, comprehensive Windows hardening, VS Code accessibility pass, and a full sweep of community PRs. + +### Added + +#### Languages and extensions + +- **Nix support** (flake-aware): `.nix` files are parsed via the `nix` tree-sitter grammar shipped with `tree-sitter-language-pack`. Top-level and nested attrset bindings become `Function` nodes with flattened dotted names (e.g. `packages.default`, `devShells.default`). In `flake.nix`, `inputs..url = "..."` strings emit `IMPORTS_FROM` edges to the URL; `import ` and `callPackage ` applications in any `.nix` file emit `IMPORTS_FROM` edges (relative paths are resolved against the caller's directory). Adds 7 tests (`TestNixParsing`) and fixtures `tests/fixtures/sample.nix`, `tests/fixtures/sample_module.nix`. +- **GDScript support** (Godot, PR #316): `.gd` files are parsed via the `gdscript` tree-sitter grammar. Extracts inner classes (`class Name:`), the file-level `class_name` identity, functions (including `static func`), `extends` parent class as an IMPORTS_FROM edge, direct calls and method calls. Adds 10 tests and `tests/fixtures/sample.gd`. +- **Verilog / SystemVerilog support** (PR #428): `.v`, `.sv`, `.svh` files parse modules, classes, packages, interfaces, programs, functions, and tasks via the `verilog` tree-sitter grammar. Per-construct extractors with dedicated unit tests. +- **SQL support** (PR #398): `.sql` files parse `CREATE FUNCTION`, `CREATE PROCEDURE`, `CREATE TABLE`, and `CREATE VIEW` statements; emits CALLS edges for function invocations. +- **ReScript support** (PR #309/323): `.res`/`.resi` parsing for modules, let-bindings, and external declarations. +- **`.hh` extension support**: C++ header variants now resolve into the C++ parser path. +- **`.ksh` extension and shebang-based detection** (PR #276): `.ksh` files parsed as shell; extension-less scripts detected via `#!/usr/bin/env ` shebang lines. +- **Julia improvements**: parametric constructors, `@enum` declarations, and `public` module exports now produce graph nodes. + +#### Platforms and install targets + +- **GitHub Copilot platform support** (PR #445): `code-review-graph install --platform copilot` writes Copilot-CLI-compatible MCP config without generating Claude-specific skill artifacts. +- **Gemini CLI platform support** (PR #391): `--platform gemini-cli` skips Claude skills and writes Gemini-native MCP config. +- **Qoder platform support** (PR #245): `--platform qoder` adds MCP server registration for Qoder. +- **OpenCode plugin support** (PR #198 via #366): `--platform opencode` registers the MCP server with the OpenCode plugin manifest. +- **Cursor hooks support** (PR #196): `install` now writes Cursor hook entries (gated behind `~/.cursor` detection so non-Cursor users are not affected). +- **Codex install alignment**: native Codex integration path; no Claude skill files generated for Codex targets. + +#### MCP server and CLI features + +- **`crg-daemon`**: new multi-repo watch daemon that supervises per-repo file watchers via `subprocess.Popen` child processes. Documented in README, COMMANDS.md, and ROADMAP.md. 35 dedicated tests. +- **Streamable HTTP transport** (PR #277): MCP server can now run over streamable HTTP in addition to stdio. +- **`serve --tools` flag and `CRG_TOOLS` env var**: MCP tool filtering at startup so callers can expose only the subset they need. +- **`--repo` precedence and validation in `get_docs_section`** (PR #378): honors `serve --repo` and validates path containment before returning section content. +- **Search enrichment via PreToolUse hooks** (PR #248): hook-driven search index enrichment ahead of tool calls. +- **External database directory support**: graph DB can now live on a network filesystem via the existing `CRG_DATA_DIR` mechanism, with the file locking path adjusted accordingly. +- **SVN support** (PR #255): basic Subversion working-copy detection alongside git for change analysis. + +#### Parser and resolver improvements + +- **Spring DI call resolution** (PR #413): receiver method calls (`this.userService.find(...)`) resolve through `@Autowired`/constructor-injected fields to the concrete `InjectedType.method`. Emits `INJECTS` edges and stereotype metadata (`@Service`, `@Component`, `@Repository`, `@Controller`); writes fully-qualified `target_qualified` so `callers_of` queries work. +- **Temporal workflow/activity call resolution**: `WorkflowStub.start(...)` and `ActivityStub.execute(...)` resolve to their concrete workflow/activity implementations. +- **Kafka consumer/producer detection**: `@KafkaListener`-annotated methods and `KafkaTemplate.send(...)` calls emit `CONSUMES` and `PRODUCES` edges keyed on topic. +- **Jedi-based Python call resolution** (PR #247): improved cross-file Python call resolution using the Jedi static-analysis library. +- **Python callback REFERENCES edges** (PR #363): function names passed as callback arguments (`schedule(my_handler)`) now emit `REFERENCES` edges instead of being dropped. +- **Mocha TDD `suite()` recognition** (PR #423): files using Mocha's TDD interface now classify as tests. +- **Bun test runtime support** (PR #421): files importing `bun:test` are detected as tests. +- **`__tests__/` directory detection** (PR #422): all files under `__tests__/` are classified as test files regardless of name. + +#### Embeddings + +- **OpenAI-compatible embedding provider** (PR #321): pluggable provider supporting OpenAI, Azure OpenAI, and any OpenAI-API-compatible endpoint, with configurable batch size. +- **Localized embedding READMEs**: provider docs translated for non-English users. + +#### Visualization, accessibility, and VS Code extension + +- **WCAG 2.1 AA contrast pass**: 4.5:1 minimum text contrast across the standalone HTML and VS Code webview. +- **Distinct `d3.symbol` shapes per node kind**: colorblind-friendly differentiation in both the standalone visualization and the VS Code webview. +- **Keyboard navigation**: tab/arrow/enter/escape navigation across nodes, with focus styles and a skip-link to bypass the legend. +- **ARIA roles and labels**: tooltip, detail panel, legend, search results, communities button, edge-pill keyboard activation, search input label. +- **Help overlay**: interaction guide for both the standalone HTML and the keyboard-help overlay. +- **Empty-state webview** in VS Code with a contextual depth slider and tooltip. +- **Edge filter popover** in the VS Code toolbar — fixes density on narrow panels. +- **Detail panel relocated to the left** so it no longer occludes top controls; close button restyled to match the toolbar. +- **CONTAINS edge opacity** raised from 0.08 → 0.14 for visibility on dense graphs. +- **GitHub Dark palette** unified across the VS Code extension. +- **`IMPLEMENTS`, `TESTED_BY`, `DEPENDS_ON` edge types** rendered in the standalone HTML visualization. + +### Fixed + +#### `__version__` reporting + +- **`code_review_graph.__version__` now matches `pyproject.toml`** (was `2.1.0` since the v2.1.0 release). The User-Agent header that `embeddings.py` sends on cloud HTTP requests is built from this string, so cloud-embedding traffic was being mis-attributed across all releases between v2.1.0 and v2.3.2. + +#### C++ / Java / PHP parsing + +- **C++ scoped/destructor/operator method names** (PR #371, PR #403): `void Foo::bar()`, `Foo::~Foo()`, `Foo::operator==(...)` now extract the correct member name instead of the qualifier or the operator token. +- **Java method name extraction** (PR #275): method names are now read from the `identifier` child of `method_declaration` rather than the return-type child (which was producing names like `int64`). +- **Java superclass / super-interfaces** (PR #278): `extends Foo` and `implements Bar, Baz` now extract bare type names from the `superclass`/`super_interfaces` AST nodes. +- **Java import resolution to file paths** (PR #280): `import com.example.foo.Bar` resolves through `src/main/java/...` and configured source roots to the actual file. +- **PHP `CALL` extraction** (PR #298): method calls (`$obj->foo()`), static calls (`Foo::bar()`), and unqualified function calls now produce CALLS edges. +- **Module-scope `CALLS` edges** (PR #285): top-level executable statements emit CALLS edges (previously only function/method bodies did). + +#### Windows + +- **Windows MCP stdio hang on long-running tools** (PR #400, PR #292): thread-pool selection now auto-selects on Windows MCP stdio so build/embed do not deadlock. +- **Windows MCP stdin hang** (PR #425): all `git`/`svn` subprocesses now run with `stdin=DEVNULL`, preventing the FastMCP-stdio buffer from filling on Windows. +- **Windows non-UTF-8 locale**: `subprocess.run` calls now pass `encoding="utf-8"` so cp1252 hosts no longer mis-decode git output. +- **Windows test failures** (PR #274): UTF-8 encoding, CRLF normalization, and `stop_at` boundary handling fixes for Windows CI. + +#### Hooks and install + +- **Hooks JSON schema** (PR #288): `hooks.json` validation no longer fails on the wrapper layout — `matcher` is required and the wrapper is removed. +- **Hooks merge instead of overwrite** (PR #114, PR #145, PR #203): `install_hooks` now merges into existing hook arrays and creates a `settings.json.bak` backup before modifying user config. +- **Pre-commit hook adds `update` command** (PR #315): generated pre-commit hook runs `code-review-graph update` rather than the obsolete subcommand. +- **Skip hooks gracefully outside git** (PR #293): `install` no longer fails when invoked from a non-git directory. +- **Poetry / uv environment detection** (PR #287): `install` now generates the correct MCP serve command for projects using Poetry or uv. +- **Hook quoting and `docs` repo_root** (PR #192): hook commands now quote repo paths with spaces, and the docs repo path is restored on install. + +#### MCP server + +- **fastmcp 3.x compatibility**: `_apply_tool_filter` restored on fastmcp ≥3, dependency floor bumped to `fastmcp>=3.2.4` to pick up the upstream Windows stdio EOF fixes. +- **FastMCP banner suppressed for stdio transport** (PR #290): the startup banner no longer corrupts the stdio handshake. +- **MCP config `cwd`, skills path, and JSONC parsing**: install now writes `cwd` into MCP config, points skills at the correct project path, and tolerates JSONC (comments + trailing commas) in existing config files. + +#### SQLite and post-processing + +- **SQLite transaction safety, FTS5 sync, and atomic operations** (PR #94, PR #279): nested-transaction handling, FTS5 content-table synchronization, and resource cleanup on error paths. +- **CLI build/update/watch run post-processing** (PR #98): signatures, FTS, flows, and communities are now refreshed after every CLI graph mutation (was previously only refreshed by the MCP server). +- **`reconcile()` auto-builds graphs and registers new repos**: cold-start path no longer requires a manual `build` before `reconcile`. +- **Flow trace adjacency in-memory** (PR #296): `trace_flows` loads adjacency once instead of querying SQLite per hop. + +#### Other + +- **`UnicodeDecodeError` in `read_text`** (PR #303): all text reads now use `errors="replace"`. +- **Dead-code callback references** (PR #424): functions referenced as callbacks no longer mis-classify as dead code. +- **Skills.py table formatting** (PR #302). +- **Search.py duplicate logger** removed. +- **`status` command reports alive/dead** from the persisted state file. + +### Security + +- **Embeddings RCE hardening** (PR #397): remote code execution paths in the embedding provider are gated behind an explicit env var; cloud HTTP requests now send a versioned User-Agent (PR #390) and refuse to mix indexes built with different providers. + +### Documentation + +- **MCP tools documentation** (PR #306): catalog of all MCP tools with usage examples. +- **venv usage guide** (PR #307). +- **Windows setup guide** for Claude Code MCP integration. +- **pipx / PyPI failure troubleshooting** with a `diagnose_pypi_connectivity.py` diagnostic script. +- **MseeP.ai badge** added to README (PR #399). + +### Maintenance + +- **Beads (`bd`) issue tracking** initialized for the project (`bd prime` for workflow context). +- **iCloud sync duplicate files** removed from the working tree. +- **Working spec docs** moved out of git (already in `.gitignore`). +- **CI lint and test failures** swept across multiple merged PRs. + +### Upgrade notes + +- `uvx --reinstall code-review-graph` or `pip install -U code-review-graph`. +- Re-run `code-review-graph install` once after upgrading to pick up the JSONC-tolerant config writer and the corrected `cwd` / skills path in `.mcp.json`. +- The `__version__` fix changes the User-Agent string emitted by cloud embedding providers from `code-review-graph/2.1.0` to `code-review-graph/2.3.3`. Anyone allow-listing the old User-Agent on a proxy needs to update their rule. +- VS Code extension still ships separately — repackage and republish the `.vsix` if you want the v2.3.3 a11y improvements in the Marketplace build. + +## [2.3.2] - 2026-04-14 + +Major feature release — 15 new capabilities, 6 community PRs merged, 6 new MCP tools, 4 new languages, multi-format export, and graph analysis suite. + +### Added + +- **Hub node detection** (`get_hub_nodes_tool`): find the most-connected nodes in the codebase (architectural hotspots) by in+out degree, excluding File nodes. +- **Bridge node detection** (`get_bridge_nodes_tool`): find architectural chokepoints via betweenness centrality with sampling approximation for graphs >5000 nodes. +- **Knowledge gap analysis** (`get_knowledge_gaps_tool`): identify structural weaknesses — isolated nodes, thin communities (<3 members), untested hotspots, and single-file communities. +- **Surprise scoring** (`get_surprising_connections_tool`): composite scoring for unexpected architectural coupling (cross-community, cross-language, peripheral-to-hub, cross-test-boundary). +- **Suggested questions** (`get_suggested_questions_tool`): auto-generate prioritized review questions from graph analysis (bridge nodes, untested hubs, surprising connections, thin communities). +- **BFS/DFS traversal** (`traverse_graph_tool`): free-form graph exploration from any node with configurable depth (1-6) and token budget. +- **Edge confidence scoring**: three-tier system (EXTRACTED/INFERRED/AMBIGUOUS) with float confidence scores on all edges. Schema migration v9. +- **Export formats**: GraphML (Gephi/yEd/Cytoscape), Neo4j Cypher statements, Obsidian vault (wikilinks + YAML frontmatter + community pages), SVG static graph. CLI: `visualize --format graphml|cypher|obsidian|svg`. +- **Graph diff**: snapshot/compare graph state over time — new/removed nodes, edges, community membership changes. +- **Token reduction benchmark**: measure naive full-corpus tokens vs graph query tokens with per-question reduction ratios. +- **Memory/feedback loop**: persist Q&A results as markdown for re-ingestion via `save_result` / `list_memories` / `clear_memories`. +- **Oversized community auto-splitting**: communities exceeding 25% of graph are recursively split via Leiden algorithm. +- **4 new languages**: Zig, PowerShell, Julia, Svelte SFC (23 total). +- **Visualization enhancements**: node size scaled by degree, community legend with toggle visibility, improved interactivity. +- **README translations**: Simplified Chinese, Japanese, Korean, Hindi. + +### Merged community PRs + +- **#127** (xtfer): SQLite compound edge indexes for query performance. +- **#184** (realkotob): batch `_compute_summaries` — fixes build hangs on large repos. +- **#202** (lngyeen): Swift extension detection, inheritance edges, type kind metadata. +- **#249** (gzenz): community detection resolution scaling (21x speedup), expanded framework patterns, framework-aware dead code detection (56 new tests). +- **#253** (cwoolum): automatic graph build for new worktrees in Claude Code. +- **#267** (jindalarpit): Kiro platform support with 9 tests. + +### Changed + +- MCP tool count: 22 → 28. +- Schema version: 8 → 9 (edge confidence columns). +- Community detection uses resolution scaling for large graphs. +- Risk scoring uses weighted flow criticality and graduated test coverage. +- Dead code detection is framework-aware (ORM models, Pydantic, CDK constructs filtered). +- Flow entry points expanded with 30+ framework decorator patterns. + +## [2.3.1] - 2026-04-11 + +Hotfix for the Windows long-running-MCP-tool hang that v2.2.4 only partially fixed. + +### Fixed +- **Windows MCP hang on long-running tools** (PR #231, fixes #46, #136): follow-up to v2.2.4. [@dev-limucc reported on #136](https://github.com/tirth8205/code-review-graph/issues/136) that the `WindowsSelectorEventLoopPolicy` fix from v2.2.4 was necessary but not sufficient — read-only tools worked, but `build_or_update_graph_tool(full_rebuild=True)` and `embed_graph_tool` still hung indefinitely on Windows 11 / Python 3.14. Root cause: FastMCP 2.x dispatches sync handlers inline on the only event-loop thread, so handlers that run for more than a few seconds (especially those that spawn subprocesses or do CPU-bound inference) stop the loop from pumping stdin/stdout. **Fix**: converted the five heavy tools (`build_or_update_graph_tool`, `run_postprocess_tool`, `embed_graph_tool`, `detect_changes_tool`, `generate_wiki_tool`) to `async def` and offloaded the blocking work via `asyncio.to_thread`. The other 19 tools are fast SQLite-read paths and stay sync. Zero config, works on every platform. New regression tests assert the five tools are registered as coroutines AND that each one's source literally contains `asyncio.to_thread` as a defense-in-depth lock-in. + +## [2.3.0] - 2026-04-11 + +Additive feature release — new language parsers, new platform install target, MCP tool UX improvements, and out-of-tree graph storage. No breaking changes from v2.2.4. + +### Added + +- **Elixir parser** (PR #228, closes #112): `.ex` and `.exs` files now produce modules as Class nodes, `def`/`defp`/`defmacro`/`defmacrop` as Function/Test nodes attached to their enclosing module, `alias`/`import`/`require`/`use` as `IMPORTS_FROM` edges, and everything else as `CALLS` edges. Internal call resolution walks into `do_block` bodies so `MathHelpers.double` correctly resolves its call to `Calculator.compute`. +- **Objective-C parser** (PR #227, closes #88): `.m` files parse classes (`@interface`, `@implementation`, `@protocol`), instance and class methods, `[receiver message:args]` message expressions, C-style `main()`, and `#import`/`#include`. Multi-part selectors like `add:to:` keep `add` as the canonical method name. +- **Bash/Shell parser** (PR #227, closes #197): `.sh`, `.bash`, and `.zsh` files parse functions, `command` invocations as `CALLS`, and `source path` / `. path` as `IMPORTS_FROM` edges with path resolution when the target file exists. +- **Qwen Code as a supported MCP install platform** (PR #227, closes #83): `code-review-graph install --platform qwen` writes a merged `~/.qwen/settings.json` using the same `mcpServers` schema as Cursor/Windsurf — it does not clobber existing Qwen config. +- **`apply_refactor_tool` dry-run mode** (PR #228, closes #176): new `dry_run: bool = False` parameter on the MCP tool and underlying `apply_refactor()` function. When true, returns a unified diff per file without touching disk and leaves the `refactor_id` valid for a follow-up real apply. Multi-edit files now apply sequentially against updated content in both modes (fixes a subtle bug where separate edits on the same file could stomp each other). +- **`CRG_DATA_DIR` environment variable** (PR #228, closes #155): when set, replaces the default `/.code-review-graph` directory verbatim. Useful for ephemeral workspaces, Docker volumes, shared CI caches, and multi-repo orchestrators. Supported by the CLI, MCP tools, and the registry. +- **`CRG_REPO_ROOT` environment variable** (PR #228, closes #155): `find_project_root()` now checks `CRG_REPO_ROOT` before the usual git-root walk — useful for anyone scripting the CLI from a cwd outside the target repo. +- **`install --no-instructions` and `-y`/`--yes` flags** (PR #228, closes #173): new flags on `code-review-graph install` to opt out of the `CLAUDE.md`/`AGENTS.md`/`.cursorrules`/`.windsurfrules` injection entirely (`--no-instructions`) or auto-confirm it without an interactive prompt (`-y`/`--yes`). The CLI also now prints the list of files it will touch before writing, so even without `--dry-run` users see what's coming. +- **Cloud embeddings stderr warning** (PR #228, closes #174): `get_provider()` now prints an explicit warning to stderr before returning a Google Gemini or MiniMax provider, explaining that source code will be sent to an external API. `CRG_ACCEPT_CLOUD_EMBEDDINGS=1` suppresses the warning for scripted workflows. The warning is on stderr only — it never writes to stdout or reads from stdin, so the MCP stdio transport remains uncorrupted. +- **TROUBLESHOOTING quick-reference** (PR #228): new top section in `docs/TROUBLESHOOTING.md` covering the four most common support questions — hook schema errors, `command not found` after pip install, project-vs-user scoping, and "built the graph but Claude Code doesn't see it". + +### Fixed + +- **Multi-edit refactor correctness** (PR #228): when a single `apply_refactor` call had multiple edits targeting the same file, the previous implementation re-read the file once per edit and could silently stomp earlier changes. The plan-computation step now groups edits by file and applies them sequentially against the updated content; this fix applies to both the real-write and the new dry-run path. + +### Changed + +- `install` and `init` commands now preview instruction-file targets before writing (no-op if nothing would change). This is always-on and does not require `--dry-run`. +- Default embedding path remains fully local (`sentence-transformers`); no behavior change unless you explicitly opt in to a cloud provider. + +### Deprecated + +Nothing. + +### Security + +- The cloud-embedding stderr warning (#174) is a privacy improvement; it does not change the behavior of offline local embeddings, which remain the default. + +### Upgrade notes + +- Nothing to do beyond `uvx --reinstall code-review-graph` or `pip install -U code-review-graph`. If you're coming from v2.2.2 or earlier, re-run `code-review-graph install` once to pick up the v2.2.3 hook schema rewrite. +- `CRG_DATA_DIR` is optional — if you don't set it, graphs continue to live at `/.code-review-graph` as before. +- VS Code extension v0.2.2 (from v2.2.4) still needs to be **repackaged and republished** separately; the PyPI `publish.yml` workflow does not cover it. + +### Superseded PRs + +- PR #204 (install preview, @lngyeen) — reimplemented cleanly in #228 with `isatty()`-guarded confirmation. +- PR #207 (`CRG_DATA_DIR`/`CRG_REPO_ROOT`, @yashmewada9618) — reimplemented cleanly in #228 without `input()`-on-stdio and `mcp._local_only` fragility. +- PR #179 (cloud embeddings warning, @Bakul2006) — reimplemented cleanly in #228 with stderr-only messaging and no stdio reads. + +Credit to @lngyeen, @yashmewada9618, and @Bakul2006 for the original designs. + +## [2.2.4] - 2026-04-11 + +Ships the 11 bugs from PR #222 plus the `v2.2.3.1` smoke-test hotfixes, for users upgrading directly from `v2.2.3` or earlier. + +### Security +- **fastmcp bumped from 1.0 → ≥2.14.0** (PR #222, fixes #139, #195): closes CVE-2025-62800 (XSS), CVE-2025-62801 (command injection via server_name), CVE-2025-66416 (Confused Deputy). Transitively drops the `docket → fakeredis` chain that was broken by a `FakeConnection` → `FakeRedisConnection` rename in recent fakeredis releases (#195). The FastMCP public API (`FastMCP(name, instructions=...)`, `@mcp.tool()`, `@mcp.prompt()`, `mcp.run(transport="stdio")`) is unchanged across the 1 → 2 bump, so no source changes were needed beyond the pin. All 24 tools verified to register on fastmcp 2.14.6 and round-trip real per-repo data via stdio MCP in a 6-repo smoke test. + +### Fixed +- **Windows build/embed hangs** (PR #222, fixes #46, #136): `main()` now sets `WindowsSelectorEventLoopPolicy` before `mcp.run()` on `sys.platform == "win32"`. The default `ProactorEventLoop` on Windows Python 3.8+ deadlocks with `ProcessPoolExecutor` (used by `full_build`) over a stdio MCP transport — producing the silent "Synthesizing…" hangs on `build` and `embed_graph_tool`. This is a no-op on macOS/Linux. **Note**: the fix was applied blind; maintainer could not verify on Windows. Please open a fresh issue if you still see a hang on v2.2.4 Windows with either `sentence-transformers` or Gemini providers. +- **Go method receivers** (PR #222, fixes #190): `func (s *T) Foo()` now attaches `Foo` to `T` as a member (`parent_name="T"`) with the usual `CONTAINS` edge instead of appearing as a top-level function. New `_get_go_receiver_type()` helper walks the method_declaration's first parameter_list to extract the receiver type name. +- **Dart parser — three bugs** (PR #222, fixes #87): + - Dart `CALLS` edges (`_extract_dart_calls_from_children()`) — tree-sitter-dart doesn't wrap calls in a single `call_expression` node; the pattern is `identifier + selector > argument_part`. New walker handles both direct (`print('x')`) and method-chained (`obj.foo()`) shapes. + - Dart `package:` URI resolution in `_do_resolve_module()` — `package:/` now walks up to a `pubspec.yaml` whose `name:` declaration matches `` and resolves to `/lib/`. + - `inheritors_of` bare-vs-qualified name mismatch in `tools/query.py` — falls back to `search_edges_by_target_name(node.name, kind=...)` for `INHERITS`/`IMPLEMENTS` when the qualified-name lookup returns nothing. Affects all languages (INHERITS targets are stored as bare strings for every language), not just Dart. +- **Nested `node_modules` and framework ignore defaults** (PR #222, fixes #91): `_should_ignore()` now treats single-segment `/**` patterns as "this directory at any depth", so `node_modules/**` also matches `packages/app/node_modules/react/index.js` inside monorepos. Extended `DEFAULT_IGNORE_PATTERNS` with Laravel/Composer (`vendor/**`, `bootstrap/cache/**`, `public/build/**`), Ruby (`.bundle/**`), Gradle (`.gradle/**`, `*.jar`), Flutter/Dart (`.dart_tool/**`, `.pub-cache/**`), and generic `coverage/**`, `.cache/**`. Deliberately did **not** add `packages/**` or `bin/**`/`obj/**` — those are false positives in yarn/pnpm workspace monorepos and .NET source trees respectively. +- **Bare `except Exception` cleanup** (PR #222, fixes #194): Replaced with specific exception classes + `logger.debug(...)` in 11 files (`cli.py`, `graph.py`, `migrations.py`, `parser.py`, `registry.py`, `tools/context.py`, `tsconfig_resolver.py`, `visualization.py`, `wiki.py`, `eval/benchmarks/search_quality.py`). No behavioral change; debuggability improvement. +- **Visualization auto-collapse hiding all edges** (PR #222, fixes #132): `visualization.py` no longer unconditionally auto-collapses every File node on page load. Auto-collapse now only kicks in above 2000 nodes — previously any graph above ~300 nodes would silently hide every CALLS/IMPORTS/INHERITS edge because they connect Functions/Classes nested inside the collapsed Files. +- **`eval` command crashes on `yaml.safe_load`** (PR #222, fixes #212): `eval.runner.load_all_configs()` now calls `_require_yaml()` before reading YAML, so users without `code-review-graph[eval]` installed get `ImportError: pyyaml is required: pip install code-review-graph[eval]` instead of `AttributeError: 'NoneType' object has no attribute 'safe_load'`. + +### VS Code extension (0.2.2) +- **`better-sqlite3` bumped 11.x → 12.x** (PR #222, fixes #218): VS Code 1.115 ships Electron 39 / V8 14.2 which removed `v8::Context::GetIsolate()`, the C++ API used by `better-sqlite3@11`. The extension couldn't activate at all — every command was undefined. `better-sqlite3@12.4.1+` (installs 12.8.0) uses the new V8 API and ships Electron 39 prebuilds. `@types/better-sqlite3: ^7.6.8 → ^7.6.13`, plus type-import adjustments in `src/backend/sqlite.ts` for the `Node16` module resolution and the new CJS `export =` types. Extension version bumped to 0.2.2. **Remember to repackage and republish the `.vsix`** — the existing `publish.yml` workflow only covers PyPI. + +### Carried forward from 2.2.3.1 +- `serve --repo ` is now honored by all 24 MCP tools (was only read by `get_docs_section_tool`). See #223. +- Wiki slug collisions no longer silently overwrite pages (~70% data loss on real repos). See #223. + +### Upgrade notes +- `uvx --reinstall code-review-graph` or `pip install -U code-review-graph`, then re-run `code-review-graph install` (the 2.2.3 hook-schema rewrite is still a requirement if you're coming from 2.2.2 or earlier). +- VS Code extension needs to be repackaged + republished separately; the Python release does not include it. + +## [2.2.3.1] - 2026-04-11 + +Hotfix on top of 2.2.3 for two bugs surfaced by a full first-time-user smoke test against six real OSS repos (express, fastapi, flask, gin, httpx, next.js). + +### Fixed +- **`serve --repo ` was ignored by 21 of 24 MCP tools** (PR #223): `main.py` captured the `--repo` CLI flag into `_default_repo_root`, but only `get_docs_section_tool` read it. The other 21 `@mcp.tool()` wrappers all took `repo_root: Optional[str] = None` and passed that straight through to the impl, which fell back to `find_repo_root()` from cwd. The real-world blast radius is small — the `install` command writes `.mcp.json` without a `--repo` flag and Claude Code launches the server with `cwd=` — but anyone scripting `serve` manually or running a multi-repo orchestrator would silently get the wrong graph. Added a single `_resolve_repo_root()` helper with explicit precedence (client arg > `--repo` flag > `None → cwd`) and threaded it through all 24 wrappers. New unit tests cover the precedence rules. +- **Wiki slug collisions silently overwrote pages** (PR #223): `_slugify()` folds non-alphanumerics to dashes and truncates to 80 chars, so similar community names collided (`"Data Processing"`, `"data processing"`, `"Data Processing"` all → `data-processing.md`). `generate_wiki()` wrote each community to `.md` regardless, so later iterations overwrote earlier files while the counter reported them as "updated". On the express smoke test this was **~70% silent data loss** (32 real files vs 107 claimed pages). Fixed by tracking used slugs per-run and appending `-2`, `-3`, … until unique. Every community now gets its own page; the counter matches the physical file count; `get_wiki_page()` still resolves by name via the existing partial-match fallback. New regression test monkey-patches three colliding names and asserts no content loss. + +## [2.2.3] - 2026-04-11 + +### Fixed +- **Claude Code hook schema** (PR #208, fixes #97, #138, #163, #168, #172, #182, #188, #191, #201): `generate_hooks_config()` now emits the valid v1.x+ Claude Code schema — every hook entry has `matcher` + a nested `hooks: [{type, command, timeout}]` array, and timeouts are in seconds. The invalid `PreCommit` event has been removed; pre-commit checks are now installed as a real git hook via `install_git_hook()`. Users upgrading from 2.2.2 must re-run `code-review-graph install` to rewrite `.claude/settings.json`. +- **SQLite transaction nesting** (PR #205, fixes #110, #135, #181): `GraphStore.__init__` now connects with `isolation_level=None`, disabling Python's implicit transactions that were the root cause of `sqlite3.OperationalError: cannot start a transaction within a transaction` on `update`. `store_file_nodes_edges` adds a defensive `in_transaction` flush before `BEGIN IMMEDIATE`. +- **Go method receivers** (PR #166): `_extract_name_from_node` now resolves Go method names from `field_identifier` inside `method_declaration`, fixing method names that were previously picked up as the result type (e.g. `int64`) instead of the method name. +- **UTF-8 decode errors in `detect_changes`** (PR #170, fixes #169): Diff parsing now uses `errors="replace"` so diffs containing binary files no longer crash the tool. +- **`--platform` target scope** (PR #142, fixes #133): `code-review-graph install --platform ` now correctly filters skills, hooks, and instruction files so you only get configuration for the requested platform. +- **Large-repo community detection hangs** (PR #213, PR #183): Removed recursive sub-community splitting, capped Leiden at `n_iterations=2`, and batched `store_communities` writes. 100k+ node graphs no longer hang in `_compute_summaries`. +- **CI**: ruff lint + `tomllib` on Python 3.10 (PR #220) — `tests/test_skills.py` now uses a conditional `tomli` backport on 3.10, `N806`/`E501`/`W291` fixes in `skills.py`/`communities.py`/`parser.py`, and the embedded `noqa` reference in `visualization.py` was rephrased so ruff stops parsing it as a directive. +- **Missing dev dependencies** (PR #159): `pytest-cov` added to dev extras, 50 ruff errors swept, one failing test fixed. +- **JSX component CALLS edges** (PR #154): JSX component usage now produces CALLS edges so component-to-component relationships appear in the graph. + +### Added +- **Codex platform install support** (PR #177): `code-review-graph install --platform codex` appends a `mcp_servers.code-review-graph` section to `~/.codex/config.toml` without overwriting existing Codex settings. +- **Luau language support** (PR #165, closes #153): Roblox Luau (`.luau`) parsing — functions, classes, local functions, requires, tests. +- **REFERENCES edge type** (PR #217): New edge kind for symbol references that aren't direct calls (map/dispatch lookups, string-keyed handlers), including Python and TypeScript patterns. +- **`recurse_submodules` build option** (PR #215): Build/update can now optionally recurse into git submodules. +- **`.gitignore` default for `.code-review-graph/`** (PR #185): Fresh installs automatically add the SQLite DB directory to `.gitignore` so the database isn't accidentally committed. +- **Clearer gitignore docs** (PR #171, closes #157): Documentation now spells out that `code-review-graph` already respects `.gitignore` via `git ls-files`. + +### Changed +- Community detection is now bounded — large repos complete in reasonable time instead of hanging indefinitely. + +### Fixed +- **`install_hooks` now merges instead of overwriting** (PR #203, fixes #114): `install_hooks()` previously used `dict.update()` which clobbered any user-defined hooks in `.claude/settings.json`. Now merges new entries into existing hook arrays, preserving user hooks. Creates a backup (`settings.json.bak`) before modification. + +## [2.2.2] - 2026-04-08 + +### Added +- **Kotlin call extraction**: `simple_identifier` + `navigation_expression` support for Kotlin method calls (PR #107) +- **JUnit/Kotlin test detection**: Annotation-based test classification (`@Test`, `@ParameterizedTest`, etc.) for Java/Kotlin/C# (PR #107) + +### Fixed +- **Windows encoding crash**: All `write_text`/`read_text` calls in `skills.py` now use `encoding='utf-8'` explicitly (PR #152, fixes #147, #148) +- **Invalid `--quiet` flag in hooks**: Removed non-existent `--quiet` and `--json` flags from generated hook commands (PR #152, fixes #149) + +### Housekeeping +- Untracked `.claude-plugin/` directory and added to `.gitignore` +- GitHub issue triage: responded to 30+ issues, closed 14, reviewed 24 PRs + +## [2.2.1] - 2026-04-07 + +### Added +- **Parallel parsing**: `ProcessPoolExecutor` for 3-5x faster builds (`CRG_PARSE_WORKERS`, `CRG_SERIAL_PARSE`) +- **Lazy post-processing**: `postprocess="full"|"minimal"|"none"` parameter, `run_postprocess` MCP tool + CLI command +- **SQLite-native BFS**: Recursive CTE replaces NetworkX for impact analysis (`CRG_BFS_ENGINE`) +- **Configurable limits**: `CRG_MAX_IMPACT_NODES`, `CRG_MAX_IMPACT_DEPTH`, `CRG_MAX_BFS_DEPTH`, `CRG_MAX_SEARCH_RESULTS` +- **Multi-hop dependents**: N-hop `find_dependents()` with `CRG_DEPENDENT_HOPS` (default 2) and 500-file cap +- **Token-efficient output**: `detail_level="minimal"` on 8 tools for 40-60% token reduction +- **`get_minimal_context` tool**: Ultra-compact entry point (~100 tokens) with task-based tool routing +- **Token-efficient prompts**: All 5 MCP prompts rewritten with minimal-first workflows +- **Incremental flow/community updates**: `incremental_trace_flows()`, `incremental_detect_communities()` +- **Visualization aggregation**: Community/file/auto modes with drill-down for large graphs (`--mode`) +- **Token-efficiency benchmarks**: 5 workflow benchmarks in `eval/token_benchmark.py` +- **DB schema v6**: Pre-computed `community_summaries`, `flow_snapshots`, `risk_index` tables +- **Token Efficiency Rules** in all skill templates and CLAUDE.md + +### Changed +- CLI `build`/`update` support `--skip-flows`, `--skip-postprocess` flags +- PostToolUse hook uses `--skip-flows` for faster incremental updates +- VS Code extension schema version bumped to v6 + +### Fixed +- mypy type errors in parallel parsing and context tool +- Bandit false positive on prompt preamble string +- Import sorting in graph.py, main.py, tools/__init__.py +- Unused imports cleaned up in cli.py + +### Housekeeping +- Gitignore: untrack `marketing-diagram.excalidraw`, `evaluate/results/`, `evaluate/reports/` +- Updated FEATURES.md, LLM-OPTIMIZED-REFERENCE.md, CHANGELOG.md for v2.2.1 + +## [2.1.0] - 2026-04-03 + +### Added +- **Jupyter notebook parsing**: Parse `.ipynb` files — extract functions, classes, imports across Python, R, and SQL cells +- **Databricks notebook parsing**: Parse Databricks `.py` notebook exports with `# COMMAND ----------` cell boundaries +- **Lua language support**: Full parsing for `.lua` files (functions, local functions, method calls, requires) — 20th language +- **Perl XS support**: Parse `.xs` files with improved Perl call detection and test coverage +- **Zero-config onboarding**: `install` now sets up skills, hooks, and CLAUDE.md by default so the graph is used automatically +- **Platform rule injection**: Graph instructions injected into all platform rule files (CLAUDE.md, .cursorrules, etc.) on install +- **Smart install detection**: Auto-detects whether installed via uvx or pip and generates correct `.mcp.json` +- **`--platform claude-code` alias**: Accepts both `claude` and `claude-code` as platform names + +### Fixed +- **JS/TS arrow functions indexed**: `const foo = () => {}` and `const bar = function() {}` now correctly appear as nodes (#66) +- **`importers_of` path resolution**: Normalized with `resolve()` to match stored edge targets (#65) +- **Custom embedding models**: Support for custom model architectures and restored model param wiring in search (#79) + +## [2.0.0] - 2026-03-27 + +### Added +- **12 new features**: flows, communities, hybrid search, change analysis, refactoring, hints, prompts, skills, wiki, multi-repo registry, migrations, eval framework +- **14 new modules** (~10,000 lines): `flows.py`, `communities.py`, `search.py`, `changes.py`, `refactor.py`, `hints.py`, `prompts.py`, `skills.py`, `wiki.py`, `registry.py`, `migrations.py`, `eval/` +- **15 new MCP tools**: `list_flows`, `get_flow`, `get_affected_flows`, `list_communities`, `get_community`, `get_architecture_overview`, `detect_changes`, `refactor`, `apply_refactor`, `generate_wiki`, `get_wiki_page`, `list_repos`, `cross_repo_search`, `find_large_functions`, `semantic_search_nodes` +- **5 MCP prompts**: `review_changes`, `architecture_map`, `debug_issue`, `onboard_developer`, `pre_merge_check` +- **7 new CLI commands**: `detect-changes`, `wiki`, `eval`, `register`, `unregister`, `repos`, `install --skills/--hooks/--all` +- **Interactive visualization upgrade**: Detail panel, community coloring, flow path highlighting, search-to-zoom, kind filters + +### Security +- Fix path traversal in wiki page reader +- Add regex allowlist for git ref validation +- Add explicit SSL context for MiniMax API + +### Fixed +- Fix git diff argument ordering (broke incremental updates) +- Fix `node_qualified_name` schema mismatch in wiki flow query +- Batch N+1 queries in `get_impact_radius` and risk scoring + +### Architecture +- Decompose `_extract_from_tree` into 6 focused methods +- Add 17 public query methods to `GraphStore` +- Split `tools.py` into 10 themed sub-modules + +## [1.8.4] - 2026-03-20 + +### Added +- **Vue SFC parsing**: Parse `.vue` Single File Components by extracting `` escaped in JSON | +| Subprocess Injection | No `shell=True`; all git commands use list arguments | +| Supply Chain | Dependencies pinned with upper bounds; `uv.lock` has SHA256 hashes | +| CDN Tampering | D3.js loaded with Subresource Integrity (SRI) hash | +| API Key Leakage | Cloud embedding credentials are loaded from environment/configuration only, never hardcoded | + +### Optional Network Calls + +- **Cloud embeddings**: Only when explicitly configured with OpenAI-compatible, Google Gemini, or MiniMax providers. Cloud providers emit an egress warning unless `CRG_ACCEPT_CLOUD_EMBEDDINGS=1` is set. +- **Local embeddings model download**: One-time download from HuggingFace on first use of `sentence-transformers` +- **D3.js CDN**: Visualization HTML loads D3.js v7 from `d3js.org` (with SRI verification) + +## Security Scanning + +The CI pipeline runs: +- **Bandit** security scanner on every PR +- **Ruff** linter for code quality +- **mypy** type checker + +Bandit exemptions are documented in `pyproject.toml` with justifications for each skip. diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..7971812 --- /dev/null +++ b/action.yml @@ -0,0 +1,127 @@ +# Composite GitHub Action: risk-scored, graph-aware PR review comments. +# Local-first — the analysis runs entirely on the runner; no source code is +# sent to any external service. See docs/GITHUB_ACTION.md for usage. +name: "code-review-graph PR Review" +description: >- + Post a risk-scored, graph-aware review comment on pull requests. + Local-first: builds a Tree-sitter knowledge graph on the runner and + analyzes change impact without sending code to external services. +author: "Tirth" +branding: + icon: "git-pull-request" + color: "purple" + +inputs: + github-token: + description: >- + Token used to post the sticky PR comment via the GitHub API. + Needs `pull-requests: write` (the default GITHUB_TOKEN works). + required: true + comment: + description: "Post (and keep updated) a sticky PR comment with the report." + required: false + default: "true" + fail-on-risk: + description: >- + Fail the job when the overall risk score reaches this level: + none (never fail), high (risk >= 0.70), or critical (risk >= 0.85). + required: false + default: "none" + python-version: + description: "Python version used to run code-review-graph." + required: false + default: "3.12" + +runs: + using: "composite" + steps: + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install code-review-graph + shell: bash + run: python -m pip install --quiet code-review-graph + + # Cache the SQLite knowledge graph between runs. The "schema9" segment + # tracks LATEST_VERSION in code_review_graph/migrations.py — bump it when + # the database schema changes so stale caches are not restored. + - name: Cache knowledge graph + uses: actions/cache@v4 + with: + path: .code-review-graph + key: code-review-graph-schema9-${{ runner.os }}-${{ hashFiles('**/uv.lock', '**/poetry.lock', '**/requirements*.txt', '**/Pipfile.lock', '**/package-lock.json', '**/pnpm-lock.yaml', '**/yarn.lock', '**/go.sum', '**/Cargo.lock', '**/Gemfile.lock', '**/composer.lock') }} + restore-keys: | + code-review-graph-schema9-${{ runner.os }}- + + - name: Resolve diff base + shell: bash + env: + BASE_REF: ${{ github.base_ref }} + run: | + if [ -n "${BASE_REF}" ]; then + git fetch --no-tags --depth=1 origin \ + "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" + echo "CRG_BASE=origin/${BASE_REF}" >> "${GITHUB_ENV}" + else + # Not a pull_request event — fall back to the previous commit. + echo "CRG_BASE=HEAD~1" >> "${GITHUB_ENV}" + fi + + - name: Build or update the graph + shell: bash + run: | + if [ -f .code-review-graph/graph.db ]; then + # Cache hit: re-parse only the files that differ from the base ref. + # If the restored database is unusable, fall back to a full build. + code-review-graph update --base "${CRG_BASE}" || code-review-graph build + else + code-review-graph build + fi + + - name: Run risk-scored change analysis + shell: bash + run: | + code-review-graph detect-changes --base "${CRG_BASE}" \ + > "${RUNNER_TEMP}/crg-report.json" + + - name: Render markdown report + shell: bash + run: | + python "${GITHUB_ACTION_PATH}/scripts/render_pr_comment.py" \ + --input "${RUNNER_TEMP}/crg-report.json" \ + --output "${RUNNER_TEMP}/crg-comment.md" + + - name: Upsert sticky PR comment + if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }} + shell: bash + env: + GH_TOKEN: ${{ inputs.github-token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: | + marker='' + comment_id=$(gh api \ + "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + --paginate \ + --jq ".[] | select(.body | contains(\"${marker}\")) | .id" | head -n 1) + if [ -n "${comment_id}" ]; then + gh api --method PATCH --silent \ + "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" \ + -F body=@"${RUNNER_TEMP}/crg-comment.md" + else + gh api --method POST --silent \ + "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ + -F body=@"${RUNNER_TEMP}/crg-comment.md" + fi + + - name: Enforce risk gate + if: ${{ inputs.fail-on-risk != 'none' }} + shell: bash + env: + FAIL_ON_RISK: ${{ inputs.fail-on-risk }} + run: | + python "${GITHUB_ACTION_PATH}/scripts/render_pr_comment.py" \ + --input "${RUNNER_TEMP}/crg-report.json" \ + --fail-on-risk "${FAIL_ON_RISK}" \ + --quiet diff --git a/code-review-graph-vscode/.gitignore b/code-review-graph-vscode/.gitignore new file mode 100644 index 0000000..0de8570 --- /dev/null +++ b/code-review-graph-vscode/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +out/ +*.vsix diff --git a/code-review-graph-vscode/.vscodeignore b/code-review-graph-vscode/.vscodeignore new file mode 100644 index 0000000..ef95398 --- /dev/null +++ b/code-review-graph-vscode/.vscodeignore @@ -0,0 +1,10 @@ +src/** +test/** +node_modules/** +.vscode/** +*.ts +!*.d.ts +.gitignore +tsconfig.json +esbuild.mjs +**/*.map diff --git a/code-review-graph-vscode/CHANGELOG.md b/code-review-graph-vscode/CHANGELOG.md new file mode 100644 index 0000000..adf899b --- /dev/null +++ b/code-review-graph-vscode/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +## 0.2.2 - 2026-04-11 + +### Fixed +- Compatible with VS Code 1.115 / Electron 39 by updating the extension SQLite dependency stack. + +## 0.2.1 — 2026-04-08 + +### Fixed +- Compatible with Python backend schema v6 (no extension-side schema changes in this release) + +## 0.2.0 — 2026-03-20 + +### Added +- **Query Graph** command with 8 query patterns (callers_of, callees_of, imports_of, etc.) +- **Find Callees** command to trace all functions called by a target +- **Find Large Functions** command to identify oversized functions/classes +- **Compute Embeddings** command to generate vector embeddings +- **Watch Mode** command for continuous graph updates +- Cursor-aware resolution for blast radius and navigation commands +- Fuzzy fallback search when exact node matches fail +- SCM decorations for git-aware file status + +### Changed +- Updated README with complete command table (13 commands) +- All 13 commands now documented + +## 0.1.1 — 2026-03-17 + +### Fixed +- CLI path setting scoped to `machine` level (security fix) +- Secure nonce generation using `crypto.randomBytes()` + +## 0.1.0 — 2026-03-17 + +Initial release. + +- Code Graph tree view with file, class, function, type, and test nodes +- Interactive D3.js graph visualisation in a webview panel +- Blast radius analysis from cursor position +- Find callers and find tests commands +- Search across all graph nodes +- Review changes with git-aware impact analysis +- Auto-update graph on file save +- CLI auto-detection and guided installation +- Getting Started walkthrough diff --git a/code-review-graph-vscode/LICENSE b/code-review-graph-vscode/LICENSE new file mode 100644 index 0000000..83c1ad6 --- /dev/null +++ b/code-review-graph-vscode/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Tirth Kanani + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/code-review-graph-vscode/README.md b/code-review-graph-vscode/README.md new file mode 100644 index 0000000..f35b4a9 --- /dev/null +++ b/code-review-graph-vscode/README.md @@ -0,0 +1,94 @@ +# Code Review Graph for VS Code + +Visualize code dependencies, blast radius, and review context from your code graph -- directly in VS Code. + +## Features + +- **Code Graph Explorer** -- Browse files, classes, functions, and their relationships in a tree view +- **Blast Radius** -- See which files and symbols are impacted when you change code +- **Review Changes** -- Automatically detect git changes and show their blast radius +- **Find Callers / Callees** -- Trace all callers or callees of any function +- **Find Tests** -- Locate tests for any symbol +- **Query Graph** -- Run semantic queries (callers, callees, imports, inheritance, tests) with 8 patterns +- **Find Large Functions** -- Identify functions or classes exceeding a line-count threshold +- **Interactive Graph** -- Force-directed D3.js visualization of your code dependencies +- **Live Search** -- Fuzzy search across your entire code graph with instant results +- **Compute Embeddings** -- Generate vector embeddings for semantic search +- **Watch Mode** -- Continuous graph updates as you work +- **Auto-Update** -- Graph rebuilds in the background when you save files + +## Quick Start + +### 1. Install the Extension + +Install **Code Review Graph** from the VS Code Marketplace. + +### 2. Install the Backend + +The extension requires the `code-review-graph` Python CLI to parse your codebase. + +```bash +# Recommended +uv pip install code-review-graph + +# Alternatives +pipx install code-review-graph +pip install code-review-graph +``` + +Requires Python 3.10+. + +### 3. Build Your Graph + +Open the Command Palette (`Ctrl+Shift+P`) and run **Code Graph: Build Graph**. + +The graph database is stored locally at `.code-review-graph/graph.db` and updates automatically on file save. + +## Commands + +| Command | Description | +|---|---| +| `Code Graph: Build Graph` | Parse the codebase and create the graph database | +| `Code Graph: Update Graph` | Incrementally update the graph | +| `Code Graph: Show Blast Radius` | Show the blast radius for a symbol | +| `Code Graph: Review Changes` | Analyze git changes and show impacted files | +| `Code Graph: Find Callers` | Find all callers of a function | +| `Code Graph: Find Callees` | Find all functions called by a target | +| `Code Graph: Find Tests` | Find tests for a symbol | +| `Code Graph: Find Large Functions` | Find functions/classes exceeding a size threshold | +| `Code Graph: Query Graph` | Run semantic queries (8 patterns: callers_of, callees_of, etc.) | +| `Code Graph: Search` | Search the code graph | +| `Code Graph: Show Graph` | Open the interactive graph visualization | +| `Code Graph: Compute Embeddings` | Generate vector embeddings for semantic search | +| `Code Graph: Watch Mode` | Run graph in watch mode for continuous updates | + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `codeReviewGraph.cliPath` | `""` | Path to the CLI binary. Leave empty to use the bundled version or one found on `PATH`. | +| `codeReviewGraph.autoUpdate` | `true` | Auto-update the graph on file save. | +| `codeReviewGraph.blastRadiusDepth` | `2` | Max traversal depth for blast radius (1--10). | +| `codeReviewGraph.graphTheme` | `"auto"` | Graph color theme: `auto`, `light`, or `dark`. | +| `codeReviewGraph.graph.maxNodes` | `500` | Max nodes in the graph visualization (10--5000). | +| `codeReviewGraph.graph.defaultEdges` | All except CONTAINS | Edge types shown by default. | +| `codeReviewGraph.treeView.showFiles` | `true` | Show file nodes in the tree view. | +| `codeReviewGraph.treeView.showClasses` | `true` | Show class nodes in the tree view. | +| `codeReviewGraph.treeView.showFunctions` | `true` | Show function nodes in the tree view. | +| `codeReviewGraph.treeView.showTypes` | `true` | Show type nodes in the tree view. | +| `codeReviewGraph.treeView.showTests` | `true` | Show test nodes in the tree view. | + +## Requirements + +- VS Code 1.85+ +- Python 3.10+ (for the backend CLI) +- A workspace with source code to analyze + +## Links + +- [Main Repository](https://github.com/tirth8205/code-review-graph) +- [Report an Issue](https://github.com/tirth8205/code-review-graph/issues) + +## License + +MIT diff --git a/code-review-graph-vscode/esbuild.mjs b/code-review-graph-vscode/esbuild.mjs new file mode 100644 index 0000000..cfd02df --- /dev/null +++ b/code-review-graph-vscode/esbuild.mjs @@ -0,0 +1,51 @@ +import * as esbuild from "esbuild"; + +const isWatch = process.argv.includes("--watch"); +const isProduction = process.argv.includes("--production"); + +/** @type {esbuild.BuildOptions} */ +const extensionConfig = { + entryPoints: ["src/extension.ts"], + bundle: true, + outfile: "dist/extension.js", + external: ["vscode", "better-sqlite3"], + format: "cjs", + platform: "node", + target: "node18", + sourcemap: !isProduction, + minify: isProduction, + logLevel: "info", +}; + +/** @type {esbuild.BuildOptions} */ +const webviewConfig = { + entryPoints: ["src/webview/graph.ts"], + bundle: true, + outfile: "dist/webview/graph.js", + format: "iife", + platform: "browser", + target: "es2022", + sourcemap: !isProduction, + minify: isProduction, + logLevel: "info", +}; + +async function main() { + if (isWatch) { + const extensionCtx = await esbuild.context(extensionConfig); + const webviewCtx = await esbuild.context(webviewConfig); + await Promise.all([extensionCtx.watch(), webviewCtx.watch()]); + console.log("[watch] Build started. Watching for changes..."); + } else { + await Promise.all([ + esbuild.build(extensionConfig), + esbuild.build(webviewConfig), + ]); + console.log("Build complete."); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/code-review-graph-vscode/media/icons/graph.svg b/code-review-graph-vscode/media/icons/graph.svg new file mode 100644 index 0000000..d189f04 --- /dev/null +++ b/code-review-graph-vscode/media/icons/graph.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/code-review-graph-vscode/media/icons/icon.png b/code-review-graph-vscode/media/icons/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..fd8abdaec3139717882757b513865c97c2231358 GIT binary patch literal 37267 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.0.0" + } + }, + "node_modules/@azure/core-auth": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.10.1.tgz", + "integrity": "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-util": "^1.13.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-client": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@azure/core-client/-/core-client-1.10.1.tgz", + "integrity": "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-rest-pipeline": "^1.22.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-rest-pipeline": { + "version": "1.23.0", + "resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.23.0.tgz", + "integrity": "sha512-Evs1INHo+jUjwHi1T6SG6Ua/LHOQBCLuKEEE6efIpt4ZOoNonaT1kP32GoOcdNDbfqsD2445CPri3MubBy5DEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@azure/core-auth": "^1.10.0", + "@azure/core-tracing": "^1.3.0", + "@azure/core-util": "^1.13.0", + "@azure/logger": "^1.3.0", + "@typespec/ts-http-runtime": "^0.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-tracing": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@azure/core-tracing/-/core-tracing-1.3.1.tgz", + "integrity": "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/core-util": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.13.1.tgz", + "integrity": "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.1.2", + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/identity": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.0.tgz", + "integrity": "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/abort-controller": "^2.0.0", + "@azure/core-auth": "^1.9.0", + "@azure/core-client": "^1.9.2", + "@azure/core-rest-pipeline": "^1.17.0", + "@azure/core-tracing": "^1.0.0", + "@azure/core-util": "^1.11.0", + "@azure/logger": "^1.0.0", + "@azure/msal-browser": "^4.2.0", + "@azure/msal-node": "^3.5.0", + "open": "^10.1.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/logger": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.3.0.tgz", + "integrity": "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typespec/ts-http-runtime": "^0.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@azure/msal-browser": { + "version": "4.29.1", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.29.1.tgz", + "integrity": "sha512-1Vrt27du1cl4QHkzLc6L4aeXqliPIDIs5l/1I4hWWMXkXccY/EznJT1+pBdoVze0azTAI8sCyq5B4cBVYG1t9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.16.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-common": { + "version": "15.16.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.16.1.tgz", + "integrity": "sha512-qxUG9TCl+TVSSX58onVDHDWrvT5CE0+NeeUAbkQqaESpSm79u5IePLnPWMMjCUnUR2zJd4+Bt9vioVRzLmJb2g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/msal-node": { + "version": "3.8.9", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.8.9.tgz", + "integrity": "sha512-jZ0pw/BbdEUWGhomCaAiVDfXRI/9K56m5hTNqB/CzcbZEYhXm5qpK1cDngN1iXfwSfmUMorOUQ2FC0dyuQ9uRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/msal-common": "15.16.1", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.110.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.110.0.tgz", + "integrity": "sha512-AGuxUEpU4F4mfuQjxPPaQVyuOMhs+VT/xRok1jiHVBubHK7lBRvCuOMZG0LKUwxncrPorJ5qq/uil3IdZBd5lA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typespec/ts-http-runtime": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.3.4.tgz", + "integrity": "sha512-CI0NhTrz4EBaa0U+HaaUZrJhPoso8sG7ZFya8uQoBA57fjzrjRSv87ekCjLZOFExN+gXE/z0xuN2QfH4H2HrLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@vscode/test-electron": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@vscode/test-electron/-/test-electron-2.5.2.tgz", + "integrity": "sha512-8ukpxv4wYe0iWMRQU18jhzJOHkeGKbnw7xWRX3Zw1WJA4cEKbHcmmLPdPrPtL6rhDcrlCZN+xKRpv09n4gRHYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "jszip": "^3.10.1", + "ora": "^8.1.0", + "semver": "^7.6.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vscode/vsce": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/@vscode/vsce/-/vsce-2.32.0.tgz", + "integrity": "sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@azure/identity": "^4.1.0", + "@vscode/vsce-sign": "^2.0.0", + "azure-devops-node-api": "^12.5.0", + "chalk": "^2.4.2", + "cheerio": "^1.0.0-rc.9", + "cockatiel": "^3.1.2", + "commander": "^6.2.1", + "form-data": "^4.0.0", + "glob": "^7.0.6", + "hosted-git-info": "^4.0.2", + "jsonc-parser": "^3.2.0", + "leven": "^3.1.0", + "markdown-it": "^12.3.2", + "mime": "^1.3.4", + "minimatch": "^3.0.3", + "parse-semver": "^1.1.1", + "read": "^1.0.7", + "semver": "^7.5.2", + "tmp": "^0.2.1", + "typed-rest-client": "^1.8.4", + "url-join": "^4.0.1", + "xml2js": "^0.5.0", + "yauzl": "^2.3.1", + "yazl": "^2.2.2" + }, + "bin": { + "vsce": "vsce" + }, + "engines": { + "node": ">= 16" + }, + "optionalDependencies": { + "keytar": "^7.7.0" + } + }, + "node_modules/@vscode/vsce-sign": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign/-/vsce-sign-2.0.9.tgz", + "integrity": "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==", + "dev": true, + "hasInstallScript": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optionalDependencies": { + "@vscode/vsce-sign-alpine-arm64": "2.0.6", + "@vscode/vsce-sign-alpine-x64": "2.0.6", + "@vscode/vsce-sign-darwin-arm64": "2.0.6", + "@vscode/vsce-sign-darwin-x64": "2.0.6", + "@vscode/vsce-sign-linux-arm": "2.0.6", + "@vscode/vsce-sign-linux-arm64": "2.0.6", + "@vscode/vsce-sign-linux-x64": "2.0.6", + "@vscode/vsce-sign-win32-arm64": "2.0.6", + "@vscode/vsce-sign-win32-x64": "2.0.6" + } + }, + "node_modules/@vscode/vsce-sign-alpine-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-arm64/-/vsce-sign-alpine-arm64-2.0.6.tgz", + "integrity": "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-alpine-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-alpine-x64/-/vsce-sign-alpine-x64-2.0.6.tgz", + "integrity": "sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "alpine" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-arm64/-/vsce-sign-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-darwin-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-darwin-x64/-/vsce-sign-darwin-x64-2.0.6.tgz", + "integrity": "sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm/-/vsce-sign-linux-arm-2.0.6.tgz", + "integrity": "sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-arm64/-/vsce-sign-linux-arm64-2.0.6.tgz", + "integrity": "sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-linux-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-linux-x64/-/vsce-sign-linux-x64-2.0.6.tgz", + "integrity": "sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@vscode/vsce-sign-win32-arm64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-arm64/-/vsce-sign-win32-arm64-2.0.6.tgz", + "integrity": "sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@vscode/vsce-sign-win32-x64": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@vscode/vsce-sign-win32-x64/-/vsce-sign-win32-x64-2.0.6.tgz", + "integrity": "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "SEE LICENSE IN LICENSE.txt", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/azure-devops-node-api": { + "version": "12.5.0", + "resolved": "https://registry.npmjs.org/azure-devops-node-api/-/azure-devops-node-api-12.5.0.tgz", + "integrity": "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==", + "dev": true, + "license": "MIT", + "dependencies": { + "tunnel": "0.0.6", + "typed-rest-client": "^1.8.4" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/better-sqlite3": { + "version": "12.8.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.8.0.tgz", + "integrity": "sha512-RxD2Vd96sQDjQr20kdP+F+dK/1OUNiVOl200vKBZY8u0vTwysfolF6Hq+3ZK2+h8My9YvZhHsF+RSGZW2VYrPQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + }, + "engines": { + "node": "20.x || 22.x || 23.x || 24.x || 25.x" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cockatiel": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", + "integrity": "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/default-browser": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", + "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "license": "ISC" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "dev": true, + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "dev": true, + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/keytar": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/keytar/-/keytar-7.9.0.tgz", + "integrity": "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-addon-api": "^4.3.0", + "prebuild-install": "^7.0.1" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true, + "license": "ISC" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.89.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.89.0.tgz", + "integrity": "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-4.3.0.tgz", + "integrity": "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true, + "license": "(MIT AND Zlib)" + }, + "node_modules/parse-semver": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parse-semver/-/parse-semver-1.1.1.tgz", + "integrity": "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^5.1.0" + } + }, + "node_modules/parse-semver/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/read": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz", + "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "mute-stream": "~0.0.4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/sax": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.5.0.tgz", + "integrity": "sha512-21IYA3Q5cQf089Z6tgaUTr7lDAyzoTPx5HRtbhsME8Udispad8dC/+sziTNugOEx54ilvatQ9YCzl4KQLPcRHA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "dev": true, + "license": "MIT" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tmp": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.5.tgz", + "integrity": "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.14" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typed-rest-client": { + "version": "1.8.11", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-1.8.11.tgz", + "integrity": "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "qs": "^6.9.1", + "tunnel": "0.0.6", + "underscore": "^1.12.1" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "dev": true, + "license": "MIT" + }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.4.tgz", + "integrity": "sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml2js": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yazl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yazl/-/yazl-2.5.1.tgz", + "integrity": "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3" + } + } + } +} diff --git a/code-review-graph-vscode/package.json b/code-review-graph-vscode/package.json new file mode 100644 index 0000000..a425835 --- /dev/null +++ b/code-review-graph-vscode/package.json @@ -0,0 +1,311 @@ +{ + "name": "code-review-graph", + "displayName": "Code Review Graph", + "description": "Visualize code dependencies, blast radius, and review context from your code-review-graph database directly in VS Code.", + "version": "0.2.2", + "publisher": "tirth8205", + "license": "MIT", + "icon": "media/icons/icon.png", + "keywords": [ + "code analysis", + "graph", + "visualization", + "dependencies", + "blast radius", + "code review", + "tree-sitter" + ], + "repository": { + "type": "git", + "url": "https://github.com/tirth8205/code-review-graph" + }, + "engines": { + "vscode": "^1.85.0" + }, + "extensionKind": [ + "workspace" + ], + "categories": [ + "Visualization", + "Other" + ], + "activationEvents": [ + "workspaceContains:.code-review-graph/graph.db", + "onCommand:codeReviewGraph.*", + "onView:codeReviewGraph.*" + ], + "main": "./dist/extension.js", + "contributes": { + "viewsContainers": { + "activitybar": [ + { + "id": "codeReviewGraph", + "title": "Code Graph", + "icon": "media/icons/graph.svg" + } + ] + }, + "views": { + "codeReviewGraph": [ + { + "id": "codeReviewGraph.codeGraph", + "name": "Code Graph" + }, + { + "id": "codeReviewGraph.blastRadius", + "name": "Blast Radius" + }, + { + "id": "codeReviewGraph.stats", + "name": "Stats" + } + ] + }, + "commands": [ + { + "command": "codeReviewGraph.showBlastRadius", + "title": "Code Graph: Show Blast Radius", + "icon": "$(pulse)" + }, + { + "command": "codeReviewGraph.findCallers", + "title": "Code Graph: Find Callers", + "icon": "$(references)" + }, + { + "command": "codeReviewGraph.findTests", + "title": "Code Graph: Find Tests", + "icon": "$(beaker)" + }, + { + "command": "codeReviewGraph.queryGraph", + "title": "Code Graph: Query Graph", + "icon": "$(symbol-keyword)" + }, + { + "command": "codeReviewGraph.findCallees", + "title": "Code Graph: Find Callees", + "icon": "$(call-outgoing)" + }, + { + "command": "codeReviewGraph.findLargeFunctions", + "title": "Code Graph: Find Large Functions", + "icon": "$(warning)" + }, + { + "command": "codeReviewGraph.showGraph", + "title": "Code Graph: Show Graph", + "icon": "$(type-hierarchy)" + }, + { + "command": "codeReviewGraph.search", + "title": "Code Graph: Search", + "icon": "$(search)" + }, + { + "command": "codeReviewGraph.reviewChanges", + "title": "Code Graph: Review Changes", + "icon": "$(git-compare)" + }, + { + "command": "codeReviewGraph.embedGraph", + "title": "Code Graph: Compute Embeddings", + "icon": "$(sparkle)" + }, + { + "command": "codeReviewGraph.watchGraph", + "title": "Code Graph: Watch Mode", + "icon": "$(eye)" + }, + { + "command": "codeReviewGraph.buildGraph", + "title": "Code Graph: Build Graph", + "icon": "$(database)" + }, + { + "command": "codeReviewGraph.updateGraph", + "title": "Code Graph: Update Graph", + "icon": "$(sync)" + } + ], + "menus": { + "scm/title": [ + { + "command": "codeReviewGraph.reviewChanges", + "group": "navigation", + "when": "scmProvider == git" + } + ], + "view/item/context": [ + { + "command": "codeReviewGraph.showBlastRadius", + "when": "viewItem =~ /node-/", + "group": "codeGraph@1" + }, + { + "command": "codeReviewGraph.findCallers", + "when": "viewItem =~ /node-/", + "group": "codeGraph@2" + }, + { + "command": "codeReviewGraph.findCallees", + "when": "viewItem =~ /node-/", + "group": "codeGraph@3" + }, + { + "command": "codeReviewGraph.findTests", + "when": "viewItem =~ /node-/", + "group": "codeGraph@4" + }, + { + "command": "codeReviewGraph.showGraph", + "when": "viewItem =~ /node-/", + "group": "codeGraph@5" + } + ] + }, + "configuration": { + "title": "Code Review Graph", + "properties": { + "codeReviewGraph.cliPath": { + "type": "string", + "default": "", + "scope": "machine", + "description": "Path to the code-review-graph CLI binary. Leave empty to use the bundled version or the one found on PATH." + }, + "codeReviewGraph.autoUpdate": { + "type": "boolean", + "default": true, + "description": "Automatically update the graph database when files are saved." + }, + "codeReviewGraph.blastRadiusDepth": { + "type": "number", + "default": 2, + "minimum": 1, + "maximum": 10, + "description": "Maximum depth for blast radius traversal." + }, + "codeReviewGraph.graphTheme": { + "type": "string", + "default": "auto", + "enum": [ + "auto", + "light", + "dark" + ], + "description": "Color theme for the graph visualization. 'auto' follows the VS Code theme." + }, + "codeReviewGraph.treeView.showFunctions": { + "type": "boolean", + "default": true, + "description": "Show function nodes in the tree view." + }, + "codeReviewGraph.treeView.showClasses": { + "type": "boolean", + "default": true, + "description": "Show class nodes in the tree view." + }, + "codeReviewGraph.treeView.showFiles": { + "type": "boolean", + "default": true, + "description": "Show file nodes in the tree view." + }, + "codeReviewGraph.treeView.showTypes": { + "type": "boolean", + "default": true, + "description": "Show type nodes in the tree view." + }, + "codeReviewGraph.treeView.showTests": { + "type": "boolean", + "default": true, + "description": "Show test nodes in the tree view." + }, + "codeReviewGraph.graph.defaultEdges": { + "type": "array", + "default": [ + "CALLS", + "IMPORTS_FROM", + "INHERITS", + "IMPLEMENTS", + "TESTED_BY", + "DEPENDS_ON" + ], + "items": { + "type": "string", + "enum": [ + "CALLS", + "IMPORTS_FROM", + "INHERITS", + "IMPLEMENTS", + "CONTAINS", + "TESTED_BY", + "DEPENDS_ON" + ] + }, + "description": "Edge types shown by default in the graph visualization." + }, + "codeReviewGraph.graph.maxNodes": { + "type": "number", + "default": 500, + "minimum": 10, + "maximum": 5000, + "description": "Maximum number of nodes to display in the graph visualization." + } + } + }, + "walkthroughs": [ + { + "id": "codeReviewGraph.welcome", + "title": "Get Started with Code Review Graph", + "description": "Build a code graph and explore your codebase visually.", + "steps": [ + { + "id": "codeReviewGraph.welcome.install", + "title": "Install the CLI", + "description": "Install the code-review-graph CLI tool to build your graph database.\n\n[Install CLI](command:codeReviewGraph.walkthrough.install)", + "media": { + "markdown": "media/walkthrough/install.md" + } + }, + { + "id": "codeReviewGraph.welcome.build", + "title": "Build Your Graph", + "description": "Run the build command to analyze your codebase and create a graph database.\n\n[Build Graph](command:codeReviewGraph.buildGraph)", + "media": { + "markdown": "media/walkthrough/build.md" + } + }, + { + "id": "codeReviewGraph.welcome.explore", + "title": "Explore the Graph", + "description": "Open the Code Graph panel in the activity bar to explore your code's structure, blast radius, and review context.\n\n[Open Code Graph](command:codeReviewGraph.walkthrough.explore)", + "media": { + "markdown": "media/walkthrough/explore.md" + } + } + ] + } + ] + }, + "scripts": { + "compile": "node esbuild.mjs", + "watch": "node esbuild.mjs --watch", + "package": "vsce package", + "lint": "tsc --noEmit", + "test": "node --experimental-vm-modules node_modules/@vscode/test-electron/out/runTest.js" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.13", + "@types/d3": "^7.4.3", + "@types/node": "^20.11.0", + "@types/vscode": "^1.85.0", + "@vscode/test-electron": "^2.3.8", + "@vscode/vsce": "^2.22.0", + "esbuild": "^0.20.0", + "typescript": "^5.3.3" + }, + "dependencies": { + "better-sqlite3": "^12.4.1", + "d3": "^7.9.0" + } +} diff --git a/code-review-graph-vscode/src/backend/cli.ts b/code-review-graph-vscode/src/backend/cli.ts new file mode 100644 index 0000000..843b87e --- /dev/null +++ b/code-review-graph-vscode/src/backend/cli.ts @@ -0,0 +1,220 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import * as vscode from 'vscode'; + +const execFileAsync = promisify(execFile); + +const CLI_TIMEOUT_MS = 60_000; +const INSTALL_TIMEOUT_MS = 120_000; + +export interface CliResult { + success: boolean; + stdout: string; + stderr: string; +} + +export class CliWrapper { + private readonly cliPath: string; + + constructor() { + this.cliPath = this.getCliPath(); + } + + /** + * Check whether the CLI binary is reachable. + */ + async isInstalled(): Promise { + try { + await execFileAsync(this.cliPath, ['--version'], { timeout: 10_000 }); + return true; + } catch (err: unknown) { + if (isEnoent(err)) { + return false; + } + // Non-zero exit or other transient error — treat as not installed. + return false; + } + } + + /** + * Return the CLI version string, or undefined when the CLI is not available. + */ + async getVersion(): Promise { + try { + const { stdout } = await execFileAsync(this.cliPath, ['--version'], { + timeout: 10_000, + }); + return stdout.trim(); + } catch { + return undefined; + } + } + + /** + * Build (or fully rebuild) the graph database for a workspace. + */ + async buildGraph( + workspaceRoot: string, + options?: { fullRebuild?: boolean }, + ): Promise { + const args = ['build']; + if (options?.fullRebuild) { + args.push('--full'); + } + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Code Review Graph: Building graph\u2026', + cancellable: false, + }, + () => this.exec(args, workspaceRoot), + ); + } + + /** + * Incrementally update the graph database for a workspace. + */ + async updateGraph(workspaceRoot: string): Promise { + return this.exec(['update'], workspaceRoot); + } + + /** + * Start the watch daemon for continuous file monitoring. + */ + async watchGraph(workspaceRoot: string): Promise { + return this.exec(['watch'], workspaceRoot); + } + + /** + * Compute embeddings for all graph nodes. + */ + async embedGraph(workspaceRoot: string): Promise { + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Code Review Graph: Computing embeddings\u2026', + cancellable: false, + }, + () => this.exec(['embed'], workspaceRoot), + ); + } + + /** + * Detect which Python package installer is available on the system. + * Checks in preference order: uv, pipx, pip3. + */ + async detectPythonInstaller(): Promise<'uv' | 'pipx' | 'pip' | null> { + const candidates: Array<{ bin: string; result: 'uv' | 'pipx' | 'pip' }> = [ + { bin: 'uv', result: 'uv' }, + { bin: 'pipx', result: 'pipx' }, + { bin: 'pip3', result: 'pip' }, + ]; + + for (const { bin, result } of candidates) { + try { + await execFileAsync(bin, ['--version'], { timeout: 10_000 }); + return result; + } catch { + // Not found or errored — try next. + } + } + + return null; + } + + /** + * Install the `code-review-graph` package using the specified installer. + */ + async installBackend(installer: 'uv' | 'pipx' | 'pip'): Promise { + const commandMap: Record = { + uv: { bin: 'uv', args: ['pip', 'install', 'code-review-graph'] }, + pipx: { bin: 'pipx', args: ['install', 'code-review-graph'] }, + pip: { bin: 'pip3', args: ['install', 'code-review-graph'] }, + }; + + const { bin, args } = commandMap[installer]; + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Code Review Graph: Installing via ${installer}\u2026`, + cancellable: false, + }, + async () => { + try { + const { stdout, stderr } = await execFileAsync(bin, args, { + timeout: INSTALL_TIMEOUT_MS, + }); + return { success: true, stdout, stderr }; + } catch (err: unknown) { + return toCliResult(err); + } + }, + ); + } + + // ------------------------------------------------------------------ private + + private getCliPath(): string { + const configured = vscode.workspace + .getConfiguration('codeReviewGraph') + .get('cliPath', ''); + return configured || 'code-review-graph'; + } + + /** + * Execute the CLI with the given arguments inside `cwd`. + */ + private async exec(args: string[], cwd?: string): Promise { + try { + const { stdout, stderr } = await execFileAsync(this.cliPath, args, { + cwd, + timeout: CLI_TIMEOUT_MS, + }); + return { success: true, stdout, stderr }; + } catch (err: unknown) { + return toCliResult(err); + } + } +} + +// --------------------------------------------------------------------- helpers + +interface ExecError { + code?: string | number; + killed?: boolean; + stdout?: string; + stderr?: string; + message?: string; +} + +function isEnoent(err: unknown): boolean { + return (err as ExecError)?.code === 'ENOENT'; +} + +function toCliResult(err: unknown): CliResult { + const e = err as ExecError; + + if (isEnoent(err)) { + return { + success: false, + stdout: '', + stderr: 'CLI binary not found. Is code-review-graph installed?', + }; + } + + if (e.killed) { + return { + success: false, + stdout: e.stdout ?? '', + stderr: 'Command timed out.', + }; + } + + return { + success: false, + stdout: e.stdout ?? '', + stderr: e.stderr ?? e.message ?? 'Unknown error', + }; +} diff --git a/code-review-graph-vscode/src/backend/sqlite.ts b/code-review-graph-vscode/src/backend/sqlite.ts new file mode 100644 index 0000000..9cbae18 --- /dev/null +++ b/code-review-graph-vscode/src/backend/sqlite.ts @@ -0,0 +1,595 @@ +/** + * Read-only SQLite reader for the code-review-graph database. + * + * Opens the database created by the Python backend and provides typed + * query methods. All writes are performed by the Python side; this + * module never mutates the database. + * + * Uses `better-sqlite3` with prepared statements for performance. + */ + +import type BetterSqlite3 from 'better-sqlite3'; + +type DatabaseType = BetterSqlite3.Database; + +// Load better-sqlite3 with graceful error handling for ABI mismatches. +// On WSL or mismatched Node.js versions, the native module may fail to load. +// better-sqlite3 uses `export =` so we import the value via require() and +// type it as the DatabaseConstructor. +let Database: typeof import('better-sqlite3'); +try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + Database = require('better-sqlite3'); +} catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + const isAbiMismatch = msg.includes('NODE_MODULE_VERSION') + || msg.includes('was compiled against') + || msg.includes('not a valid Win32'); + if (isAbiMismatch) { + console.error( + '[code-review-graph] better-sqlite3 ABI mismatch. ' + + 'Your VS Code uses a different Node.js version than the one ' + + 'this extension was built for. ' + + 'Try: cd ~/.vscode/extensions/code-review-graph-* && npm rebuild better-sqlite3' + ); + } + throw err; +} + +// --------------------------------------------------------------------------- +// Interfaces +// --------------------------------------------------------------------------- + +export type NodeKind = 'File' | 'Class' | 'Function' | 'Type' | 'Test'; + +export type EdgeKind = + | 'CALLS' + | 'IMPORTS_FROM' + | 'INHERITS' + | 'IMPLEMENTS' + | 'CONTAINS' + | 'TESTED_BY' + | 'DEPENDS_ON'; + +export interface GraphNode { + id: number; + kind: NodeKind; + name: string; + qualifiedName: string; + filePath: string; + lineStart: number | null; + lineEnd: number | null; + language: string | null; + parentName: string | null; + params: string | null; + returnType: string | null; + modifiers: string | null; + isTest: boolean; + fileHash: string | null; +} + +export interface GraphEdge { + id: number; + kind: EdgeKind; + sourceQualified: string; + targetQualified: string; + filePath: string; + line: number; +} + +export interface GraphStats { + totalNodes: number; + totalEdges: number; + nodesByKind: Record; + edgesByKind: Record; + languages: string[]; + filesCount: number; + lastUpdated: string | null; + embeddingsCount: number; +} + +export interface ImpactRadius { + changedNodes: GraphNode[]; + impactedNodes: GraphNode[]; + impactedFiles: string[]; + edges: GraphEdge[]; +} + +// --------------------------------------------------------------------------- +// Raw row types returned by better-sqlite3 +// --------------------------------------------------------------------------- + +interface NodeRow { + id: number; + kind: string; + name: string; + qualified_name: string; + file_path: string; + line_start: number | null; + line_end: number | null; + language: string | null; + parent_name: string | null; + params: string | null; + return_type: string | null; + modifiers: string | null; + is_test: number; + file_hash: string | null; + extra: string; + updated_at: number; +} + +interface EdgeRow { + id: number; + kind: string; + source_qualified: string; + target_qualified: string; + file_path: string; + line: number; + extra: string; + updated_at: number; +} + +interface CountRow { + cnt: number; +} + +interface KindCountRow { + kind: string; + cnt: number; +} + +interface LanguageRow { + language: string; +} + +interface FilePathRow { + file_path: string; +} + +interface MetadataRow { + value: string; +} + +// --------------------------------------------------------------------------- +// SqliteReader +// --------------------------------------------------------------------------- + +const MAX_OPEN_RETRIES = 3; +const RETRY_BACKOFF_MS = 100; + +export class SqliteReader { + private db: DatabaseType | null = null; + + /** + * Create a SqliteReader with retry logic that does not block the event loop. + * Prefer this over the constructor when calling from async code. + */ + static async create(dbPath: string): Promise { + let lastError: unknown; + for (let attempt = 0; attempt < MAX_OPEN_RETRIES; attempt++) { + try { + return new SqliteReader(dbPath); + } catch (err) { + lastError = err; + if (attempt < MAX_OPEN_RETRIES - 1) { + await new Promise(resolve => + setTimeout(resolve, RETRY_BACKOFF_MS * (attempt + 1)) + ); + } + } + } + throw lastError; + } + + constructor(dbPath: string) { + this.db = new Database(dbPath, { readonly: true }); + this.db.pragma('journal_mode = WAL'); + this.db.pragma('busy_timeout = 5000'); + } + + /** + * Check if the database schema is compatible with this extension version. + * Returns a warning message if incompatible, or undefined if OK. + */ + checkSchemaCompatibility(): string | undefined { + if (!this.db) { return 'Database is not open'; } + try { + // Check that required tables exist + const tables = this.db + .prepare("SELECT name FROM sqlite_master WHERE type='table'") + .all() as Array<{ name: string }>; + const tableNames = new Set(tables.map((t) => t.name)); + + if (!tableNames.has('nodes') || !tableNames.has('edges')) { + return 'Database is missing required tables (nodes/edges). Rebuild required.'; + } + + // Check for schema_version in metadata if it exists + if (tableNames.has('metadata')) { + const row = this.db + .prepare("SELECT value FROM metadata WHERE key = 'schema_version'") + .get() as { value: string } | undefined; + if (row) { + const version = parseInt(row.value, 10); + // Must match LATEST_VERSION in code_review_graph/migrations.py + const SUPPORTED_SCHEMA_VERSION = 9; + if (!isNaN(version) && version > SUPPORTED_SCHEMA_VERSION) { + return `Database was created with a newer version (schema v${version}). Update the extension.`; + } + } + } + + return undefined; + } catch { + return 'Could not verify database schema.'; + } + } + + /** Close the database connection. Safe to call multiple times. */ + close(): void { + if (this.db) { + this.db.close(); + this.db = null; + } + } + + /** Returns true if the database is open and contains the nodes table. */ + isValid(): boolean { + if (!this.db) { return false; } + try { + const row = this.db + .prepare( + "SELECT name FROM sqlite_master WHERE type='table' AND name='nodes'" + ) + .get() as { name: string } | undefined; + return row !== undefined; + } catch { + return false; + } + } + + // ----------------------------------------------------------------------- + // Node queries + // ----------------------------------------------------------------------- + + /** All file paths (kind='File'), ordered by file_path. */ + getAllFiles(): string[] { + const rows = this._db() + .prepare( + "SELECT DISTINCT file_path FROM nodes WHERE kind = 'File' ORDER BY file_path" + ) + .all() as FilePathRow[]; + return rows.map((r) => r.file_path); + } + + /** All nodes in a file, ordered by line_start. */ + getNodesByFile(filePath: string): GraphNode[] { + const rows = this._db() + .prepare( + 'SELECT * FROM nodes WHERE file_path = ? ORDER BY line_start' + ) + .all(filePath) as NodeRow[]; + return rows.map((r) => this._rowToNode(r)); + } + + /** Single node lookup by qualified_name. */ + getNode(qualifiedName: string): GraphNode | undefined { + const row = this._db() + .prepare('SELECT * FROM nodes WHERE qualified_name = ?') + .get(qualifiedName) as NodeRow | undefined; + return row ? this._rowToNode(row) : undefined; + } + + /** + * Innermost node at a cursor position. + * + * Returns the node whose line range contains `line` with the smallest + * span (i.e. the most specific / innermost enclosing node). + */ + getNodeAtCursor(filePath: string, line: number): GraphNode | undefined { + const row = this._db() + .prepare( + `SELECT * FROM nodes + WHERE file_path = ? AND line_start <= ? AND line_end >= ? + ORDER BY (line_end - line_start) ASC + LIMIT 1` + ) + .get(filePath, line, line) as NodeRow | undefined; + return row ? this._rowToNode(row) : undefined; + } + + /** LIKE search on name and qualified_name. */ + searchNodes(query: string, limit: number = 20): GraphNode[] { + const pattern = `%${query}%`; + const rows = this._db() + .prepare( + 'SELECT * FROM nodes WHERE name LIKE ? OR qualified_name LIKE ? LIMIT ?' + ) + .all(pattern, pattern, limit) as NodeRow[]; + return rows.map((r) => this._rowToNode(r)); + } + + // ----------------------------------------------------------------------- + // Edge queries + // ----------------------------------------------------------------------- + + /** Outgoing edges from a node. */ + getEdgesBySource(qualifiedName: string): GraphEdge[] { + const rows = this._db() + .prepare('SELECT * FROM edges WHERE source_qualified = ?') + .all(qualifiedName) as EdgeRow[]; + return rows.map((r) => this._rowToEdge(r)); + } + + /** Incoming edges to a node. */ + getEdgesByTarget(qualifiedName: string): GraphEdge[] { + const rows = this._db() + .prepare('SELECT * FROM edges WHERE target_qualified = ?') + .all(qualifiedName) as EdgeRow[]; + return rows.map((r) => this._rowToEdge(r)); + } + + /** + * Edges where both source and target are in the given set. + * + * Uses a parameterised IN clause -- safe for arbitrary set sizes + * (better-sqlite3 handles large parameter lists efficiently). + */ + getEdgesAmong(qualifiedNames: Set): GraphEdge[] { + if (qualifiedNames.size === 0) { return []; } + const qns = [...qualifiedNames]; + const placeholders = qns.map(() => '?').join(','); + const rows = this._db() + .prepare( + `SELECT * FROM edges + WHERE source_qualified IN (${placeholders}) + AND target_qualified IN (${placeholders})` + ) + .all(...qns, ...qns) as EdgeRow[]; + return rows.map((r) => this._rowToEdge(r)); + } + + // ----------------------------------------------------------------------- + // Statistics & metadata + // ----------------------------------------------------------------------- + + /** Aggregate counts, languages, last_updated, and embeddings count. */ + getStats(): GraphStats { + const db = this._db(); + + const totalNodes = ( + db.prepare('SELECT COUNT(*) AS cnt FROM nodes').get() as CountRow + ).cnt; + + const totalEdges = ( + db.prepare('SELECT COUNT(*) AS cnt FROM edges').get() as CountRow + ).cnt; + + const nodesByKind: Record = {}; + const nkRows = db + .prepare('SELECT kind, COUNT(*) AS cnt FROM nodes GROUP BY kind') + .all() as KindCountRow[]; + for (const r of nkRows) { nodesByKind[r.kind] = r.cnt; } + + const edgesByKind: Record = {}; + const ekRows = db + .prepare('SELECT kind, COUNT(*) AS cnt FROM edges GROUP BY kind') + .all() as KindCountRow[]; + for (const r of ekRows) { edgesByKind[r.kind] = r.cnt; } + + const languages = ( + db + .prepare( + "SELECT DISTINCT language FROM nodes WHERE language IS NOT NULL AND language != ''" + ) + .all() as LanguageRow[] + ).map((r) => r.language); + + const filesCount = ( + db + .prepare("SELECT COUNT(*) AS cnt FROM nodes WHERE kind = 'File'") + .get() as CountRow + ).cnt; + + const lastUpdated = this.getMetadata('last_updated') ?? null; + + // Embeddings count -- table may not exist + let embeddingsCount = 0; + try { + embeddingsCount = ( + db.prepare('SELECT COUNT(*) AS cnt FROM embeddings').get() as CountRow + ).cnt; + } catch { + // embeddings table does not exist -- that is fine + } + + return { + totalNodes, + totalEdges, + nodesByKind, + edgesByKind, + languages, + filesCount, + lastUpdated, + embeddingsCount, + }; + } + + /** Read a single key from the metadata table. */ + getMetadata(key: string): string | undefined { + const row = this._db() + .prepare('SELECT value FROM metadata WHERE key = ?') + .get(key) as MetadataRow | undefined; + return row?.value; + } + + // ----------------------------------------------------------------------- + // Impact radius (BFS traversal) + // ----------------------------------------------------------------------- + + /** + * BFS from changed files to find all impacted nodes within `maxDepth` hops. + * + * Matches the Python `GraphStore.get_impact_radius` logic exactly: + * 1. Collect all nodes in `changedFiles` as seed set. + * 2. BFS forward (outgoing) AND backward (incoming) edges up to `maxDepth`. + * 3. Return impacted nodes (excluding seeds), impacted files, and edges + * among all involved nodes. + */ + getImpactRadius( + changedFiles: string[], + maxDepth: number = 2, + ): ImpactRadius { + // 1. Seed: all qualified names in changed files + const seeds = new Set(); + for (const f of changedFiles) { + for (const node of this.getNodesByFile(f)) { + seeds.add(node.qualifiedName); + } + } + + // 2. BFS outward through all edge types (forward + backward) + const visited = new Set(); + let frontier = new Set(seeds); + const impacted = new Set(); + let depth = 0; + + while (frontier.size > 0 && depth < maxDepth) { + const nextFrontier = new Set(); + for (const qn of frontier) { + visited.add(qn); + + // Forward edges (things this node affects) + for (const e of this.getEdgesBySource(qn)) { + if (!visited.has(e.targetQualified)) { + nextFrontier.add(e.targetQualified); + impacted.add(e.targetQualified); + } + } + + // Reverse edges (things that depend on this node) + for (const e of this.getEdgesByTarget(qn)) { + if (!visited.has(e.sourceQualified)) { + nextFrontier.add(e.sourceQualified); + impacted.add(e.sourceQualified); + } + } + } + frontier = nextFrontier; + depth++; + } + + // 3. Resolve to full node info + const changedNodes: GraphNode[] = []; + for (const qn of seeds) { + const node = this.getNode(qn); + if (node) { changedNodes.push(node); } + } + + const impactedNodes: GraphNode[] = []; + for (const qn of impacted) { + if (seeds.has(qn)) { continue; } + const node = this.getNode(qn); + if (node) { impactedNodes.push(node); } + } + + const impactedFiles = [ + ...new Set(impactedNodes.map((n) => n.filePath)), + ]; + + // Collect relevant edges among all involved nodes + const allQns = new Set([...seeds, ...impacted]); + const edges = allQns.size > 0 ? this.getEdgesAmong(allQns) : []; + + return { changedNodes, impactedNodes, impactedFiles, edges }; + } + + // ----------------------------------------------------------------------- + // Size-based queries + // ----------------------------------------------------------------------- + + /** + * Find nodes exceeding a line-count threshold. + * + * Mirrors the Python `GraphStore.get_nodes_by_size()` method. + */ + getNodesBySize( + minLines: number = 50, + kind?: string, + filePathPattern?: string, + limit: number = 50, + ): Array { + const conditions = ['(line_end - line_start + 1) >= ?']; + const params: Array = [minLines]; + + if (kind) { + conditions.push('kind = ?'); + params.push(kind); + } + if (filePathPattern) { + conditions.push('file_path LIKE ?'); + params.push(`%${filePathPattern}%`); + } + + params.push(limit); + const where = conditions.join(' AND '); + const rows = this._db() + .prepare( + `SELECT * FROM nodes WHERE ${where} ` + // nosec + 'ORDER BY (line_end - line_start + 1) DESC LIMIT ?', + ) + .all(...params) as NodeRow[]; + + return rows.map((r) => ({ + ...this._rowToNode(r), + lineCount: + r.line_start != null && r.line_end != null + ? r.line_end - r.line_start + 1 + : 0, + })); + } + + // ----------------------------------------------------------------------- + // Private helpers + // ----------------------------------------------------------------------- + + /** Return the open database handle or throw. */ + private _db(): DatabaseType { + if (!this.db) { + throw new Error('SqliteReader: database is closed'); + } + return this.db; + } + + /** Convert a raw node row (snake_case) to a typed GraphNode (camelCase). */ + private _rowToNode(row: NodeRow): GraphNode { + return { + id: row.id, + kind: row.kind as NodeKind, + name: row.name, + qualifiedName: row.qualified_name, + filePath: row.file_path, + lineStart: row.line_start, + lineEnd: row.line_end, + language: row.language ?? null, + parentName: row.parent_name ?? null, + params: row.params ?? null, + returnType: row.return_type ?? null, + modifiers: row.modifiers ?? null, + isTest: row.is_test === 1, + fileHash: row.file_hash ?? null, + }; + } + + /** Convert a raw edge row (snake_case) to a typed GraphEdge (camelCase). */ + private _rowToEdge(row: EdgeRow): GraphEdge { + return { + id: row.id, + kind: row.kind as EdgeKind, + sourceQualified: row.source_qualified, + targetQualified: row.target_qualified, + filePath: row.file_path, + line: row.line ?? 0, + }; + } +} diff --git a/code-review-graph-vscode/src/backend/watcher.ts b/code-review-graph-vscode/src/backend/watcher.ts new file mode 100644 index 0000000..1925917 --- /dev/null +++ b/code-review-graph-vscode/src/backend/watcher.ts @@ -0,0 +1,60 @@ +import * as path from 'path'; +import * as vscode from 'vscode'; + +/** + * Return a debounced version of `fn` that delays invocation until `ms` + * milliseconds have elapsed since the last call. The returned function + * has the same signature as the original. + */ +export function debounce any>(fn: T, ms: number): T { + let timer: ReturnType | undefined; + + const debounced = (...args: Parameters): void => { + if (timer !== undefined) { + clearTimeout(timer); + } + timer = setTimeout(() => { + timer = undefined; + fn(...args); + }, ms); + }; + + return debounced as unknown as T; +} + +/** + * Watches a `graph.db` file on disk and fires a callback whenever the + * file is created or modified (debounced to avoid rapid successive events). + */ +export class GraphWatcher implements vscode.Disposable { + private readonly watcher: vscode.FileSystemWatcher; + private readonly disposables: vscode.Disposable[] = []; + + /** + * @param dbPath Absolute path to the `graph.db` file to watch. + * @param onChanged Callback invoked (at most once per 500 ms) when the file changes. + */ + constructor(dbPath: string, onChanged: () => void) { + const dir = path.dirname(dbPath); + const filename = path.basename(dbPath); + + this.watcher = vscode.workspace.createFileSystemWatcher( + new vscode.RelativePattern(vscode.Uri.file(dir), filename), + ); + + const debouncedOnChanged = debounce(onChanged, 500); + + this.disposables.push( + this.watcher.onDidChange(() => debouncedOnChanged()), + this.watcher.onDidCreate(() => debouncedOnChanged()), + this.watcher, + ); + } + + dispose(): void { + for (const d of this.disposables) { + d.dispose(); + } + this.disposables.length = 0; + } +} diff --git a/code-review-graph-vscode/src/extension.ts b/code-review-graph-vscode/src/extension.ts new file mode 100644 index 0000000..44acde3 --- /dev/null +++ b/code-review-graph-vscode/src/extension.ts @@ -0,0 +1,983 @@ +import * as vscode from "vscode"; +import * as path from "node:path"; +import * as fs from "node:fs"; + +import { SqliteReader } from "./backend/sqlite"; +import type { GraphNode } from "./backend/sqlite"; +import { CliWrapper } from "./backend/cli"; +import { + CodeGraphTreeProvider, + BlastRadiusTreeProvider, + StatsTreeProvider, +} from "./views/treeView"; +import { GraphWebviewPanel } from "./views/graphWebview"; +import { Installer } from "./onboarding/installer"; +import { registerWalkthroughCommands, showWelcomeIfNeeded } from "./onboarding/welcome"; +import { StatusBar } from "./views/statusBar"; +import { ScmDecorationProvider } from "./features/scmDecorations"; + +let sqliteReader: SqliteReader | undefined; +let autoUpdateTimer: ReturnType | undefined; +let scmDecorationProvider: ScmDecorationProvider | undefined; + +/** + * Locate the graph database file in the workspace. + * Checks `.code-review-graph/graph.db` first, then falls back to `.code-review-graph.db`. + */ +function findGraphDb(workspaceRoot: string): string | undefined { + const primary = path.join(workspaceRoot, ".code-review-graph", "graph.db"); + if (fs.existsSync(primary)) { + return primary; + } + + const fallback = path.join(workspaceRoot, ".code-review-graph.db"); + if (fs.existsSync(fallback)) { + return fallback; + } + + return undefined; +} + +/** + * Get the workspace root folder path, or undefined if no workspace is open. + * Checks all workspace folders for a graph database (multi-root support). + */ +function getWorkspaceRoot(): string | undefined { + const folders = vscode.workspace.workspaceFolders; + if (!folders) { return undefined; } + + // Prefer the folder that has a graph database + for (const folder of folders) { + if (findGraphDb(folder.uri.fsPath)) { + return folder.uri.fsPath; + } + } + + // Fall back to first folder + return folders[0]?.uri.fsPath; +} + + +/** + * Navigate to a node's source file location. + */ +async function navigateToNode(node: GraphNode): Promise { + const workspaceRoot = getWorkspaceRoot(); + const filePath = workspaceRoot + ? path.join(workspaceRoot, node.filePath) + : node.filePath; + + const doc = await vscode.workspace.openTextDocument(filePath); + const line = Math.max(0, (node.lineStart ?? 1) - 1); + await vscode.window.showTextDocument(doc, { + selection: new vscode.Range(line, 0, line, 0), + }); +} + +/** + * Register all extension commands. + */ +function registerCommands( + context: vscode.ExtensionContext, + cli: CliWrapper +): void { + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.buildGraph", + async () => { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage("No workspace folder is open."); + return; + } + + const result = await cli.buildGraph(workspaceRoot); + if (result.success) { + await reinitialize(context); + vscode.window.showInformationMessage("Code Graph: Build complete."); + } else { + vscode.window.showErrorMessage( + `Code Graph: Build failed. ${result.stderr}` + ); + } + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.updateGraph", + async () => { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage("No workspace folder is open."); + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Code Graph: Updating graph...", + cancellable: false, + }, + async () => { + const result = await cli.updateGraph(workspaceRoot); + if (!result.success) { + vscode.window.showErrorMessage( + `Code Graph: Update failed. ${result.stderr}` + ); + } + } + ); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.showBlastRadius", + async (qualifiedNameOrUri?: string | vscode.Uri) => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded." + ); + return; + } + + let qualifiedName: string | undefined; + if (typeof qualifiedNameOrUri === "string") { + qualifiedName = qualifiedNameOrUri; + } else { + qualifiedName = await vscode.window.showInputBox({ + prompt: + "Enter the qualified name (e.g., my_module.MyClass.my_method)", + placeHolder: "my_module.my_function", + }); + } + + if (!qualifiedName) { + return; + } + + // Find the file for this node and compute impact radius + const node = sqliteReader.getNode(qualifiedName); + if (!node) { + vscode.window.showInformationMessage( + `Code Graph: Node "${qualifiedName}" not found.` + ); + return; + } + + const config = vscode.workspace.getConfiguration("codeReviewGraph"); + const depth = config.get("blastRadiusDepth", 2); + const impact = sqliteReader.getImpactRadius([node.filePath], depth); + + if (impact.impactedNodes.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No blast radius found for "${qualifiedName}".` + ); + return; + } + + await vscode.commands.executeCommand( + "codeReviewGraph.blastRadius.focus" + ); + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.findCallers", + async (qualifiedName?: string) => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded." + ); + return; + } + + if (!qualifiedName) { + qualifiedName = await vscode.window.showInputBox({ + prompt: "Enter the qualified name to find callers for", + placeHolder: "my_module.my_function", + }); + } + + if (!qualifiedName) { + return; + } + + const edges = sqliteReader.getEdgesByTarget(qualifiedName); + const callerEdges = edges.filter((e) => e.kind === "CALLS"); + + if (callerEdges.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No callers found for "${qualifiedName}".` + ); + return; + } + + const items = callerEdges.map((e) => { + const callerNode = sqliteReader!.getNode(e.sourceQualified); + return { + label: callerNode?.name ?? e.sourceQualified, + description: callerNode?.filePath ?? e.filePath, + detail: `Line ${callerNode?.lineStart ?? e.line}`, + node: callerNode, + edge: e, + }; + }); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Callers of ${qualifiedName}`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + } + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.findTests", + async (qualifiedName?: string) => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded." + ); + return; + } + + if (!qualifiedName) { + qualifiedName = await vscode.window.showInputBox({ + prompt: "Enter the qualified name to find tests for", + placeHolder: "my_module.my_function", + }); + } + + if (!qualifiedName) { + return; + } + + // Find tests via TESTED_BY edges + const edges = sqliteReader.getEdgesByTarget(qualifiedName); + const testEdges = edges.filter((e) => e.kind === "TESTED_BY"); + + // Also check reverse: source is the node, target is the test + const outEdges = sqliteReader.getEdgesBySource(qualifiedName); + const outTestEdges = outEdges.filter((e) => e.kind === "TESTED_BY"); + + const allTestQualifiedNames = new Set([ + ...testEdges.map((e) => e.sourceQualified), + ...outTestEdges.map((e) => e.targetQualified), + ]); + + if (allTestQualifiedNames.size === 0) { + vscode.window.showInformationMessage( + `Code Graph: No tests found for "${qualifiedName}".` + ); + return; + } + + const items: Array<{ + label: string; + description: string; + detail: string; + node: GraphNode | undefined; + }> = []; + + for (const tqn of allTestQualifiedNames) { + const testNode = sqliteReader.getNode(tqn); + items.push({ + label: testNode?.name ?? tqn, + description: testNode?.filePath ?? "", + detail: `Line ${testNode?.lineStart ?? "?"}`, + node: testNode, + }); + } + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Tests for ${qualifiedName}`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + } + ) + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.queryGraph — expose all 8 query patterns + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand("codeReviewGraph.queryGraph", async () => { + if (!sqliteReader) { + vscode.window.showWarningMessage("Code Graph: No graph database loaded."); + return; + } + + const patterns = [ + { label: "callers_of", description: "Find functions calling the target" }, + { label: "callees_of", description: "Find functions called by the target" }, + { label: "imports_of", description: "Find modules imported by a file" }, + { label: "importers_of", description: "Find files importing from the target" }, + { label: "children_of", description: "Find nodes contained in a file or class" }, + { label: "tests_for", description: "Find tests for a function or class" }, + { label: "inheritors_of", description: "Find classes inheriting/implementing the target" }, + { label: "file_summary", description: "List all nodes in a file" }, + ]; + + const pattern = await vscode.window.showQuickPick(patterns, { + placeHolder: "Select a query pattern", + }); + if (!pattern) { return; } + + const target = await vscode.window.showInputBox({ + prompt: `Enter the target for ${pattern.label}`, + placeHolder: "e.g., my_module.py::my_function or path/to/file.py", + }); + if (!target) { return; } + + // Map pattern to edge kind + direction + type QueryDef = { edgeKind: string; direction: "incoming" | "outgoing"; nodeFilter?: string }; + const queryMap: Record = { + callers_of: { edgeKind: "CALLS", direction: "incoming" }, + callees_of: { edgeKind: "CALLS", direction: "outgoing" }, + imports_of: { edgeKind: "IMPORTS_FROM", direction: "outgoing" }, + importers_of: { edgeKind: "IMPORTS_FROM", direction: "incoming" }, + children_of: { edgeKind: "CONTAINS", direction: "outgoing" }, + tests_for: { edgeKind: "TESTED_BY", direction: "incoming" }, + inheritors_of: { edgeKind: "INHERITS", direction: "incoming" }, + file_summary: { edgeKind: "CONTAINS", direction: "outgoing" }, + }; + + const qdef = queryMap[pattern.label]; + if (!qdef) { return; } + + // Try exact match, then search + let node = sqliteReader.getNode(target); + if (!node) { + const matches = sqliteReader.searchNodes(target, 5); + if (matches.length === 1) { + node = matches[0]; + } else if (matches.length > 1) { + const selected = await vscode.window.showQuickPick( + matches.map(m => ({ + label: m.name, + description: `${m.kind} · ${m.filePath}`, + node: m, + })), + { placeHolder: `Multiple matches for "${target}" — select one` }, + ); + if (!selected) { return; } + node = selected.node; + } + } + + if (!node) { + vscode.window.showInformationMessage(`Code Graph: "${target}" not found.`); + return; + } + + const edges = qdef.direction === "incoming" + ? sqliteReader.getEdgesByTarget(node.qualifiedName) + : sqliteReader.getEdgesBySource(node.qualifiedName); + + const filtered = edges.filter(e => e.kind === qdef.edgeKind); + + if (filtered.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No ${pattern.label} results for "${node.name}".` + ); + return; + } + + const items = filtered.map(e => { + const relatedQn = qdef.direction === "incoming" ? e.sourceQualified : e.targetQualified; + const relatedNode = sqliteReader!.getNode(relatedQn); + return { + label: relatedNode?.name ?? relatedQn, + description: relatedNode ? `${relatedNode.kind} · ${relatedNode.filePath}` : "", + detail: `Line ${relatedNode?.lineStart ?? e.line}`, + node: relatedNode, + }; + }); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `${pattern.label}: ${node.name} (${filtered.length} results)`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + }) + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.findCallees + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.findCallees", + async (qualifiedName?: string) => { + if (!sqliteReader) { + vscode.window.showWarningMessage("Code Graph: No graph database loaded."); + return; + } + + if (!qualifiedName) { + qualifiedName = await vscode.window.showInputBox({ + prompt: "Enter the qualified name to find callees for", + placeHolder: "my_module.my_function", + }); + } + if (!qualifiedName) { return; } + + const edges = sqliteReader.getEdgesBySource(qualifiedName); + const calleeEdges = edges.filter(e => e.kind === "CALLS"); + + if (calleeEdges.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No callees found for "${qualifiedName}".` + ); + return; + } + + const items = calleeEdges.map(e => { + const calleeNode = sqliteReader!.getNode(e.targetQualified); + return { + label: calleeNode?.name ?? e.targetQualified, + description: calleeNode?.filePath ?? e.filePath, + detail: `Line ${calleeNode?.lineStart ?? e.line}`, + node: calleeNode, + }; + }); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Callees of ${qualifiedName}`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + } + ) + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.findLargeFunctions + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.findLargeFunctions", + async () => { + if (!sqliteReader) { + vscode.window.showWarningMessage("Code Graph: No graph database loaded."); + return; + } + + const minLinesStr = await vscode.window.showInputBox({ + prompt: "Minimum line count threshold", + placeHolder: "50", + value: "50", + }); + if (!minLinesStr) { return; } + + const minLines = parseInt(minLinesStr, 10); + if (isNaN(minLines) || minLines < 1) { + vscode.window.showWarningMessage("Code Graph: Invalid line count."); + return; + } + + const results = sqliteReader.getNodesBySize(minLines, undefined, undefined, 50); + + if (results.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No functions found with ${minLines}+ lines.` + ); + return; + } + + const items = results.map(r => ({ + label: `${r.name} (${r.lineCount} lines)`, + description: `${r.kind} · ${r.filePath}`, + detail: `Lines ${r.lineStart ?? "?"}–${r.lineEnd ?? "?"}`, + node: r as GraphNode, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `${results.length} nodes with ${minLines}+ lines`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + } + ) + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.embedGraph + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand("codeReviewGraph.embedGraph", async () => { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage("No workspace folder is open."); + return; + } + + const result = await cli.embedGraph(workspaceRoot); + if (result.success) { + vscode.window.showInformationMessage("Code Graph: Embeddings computed."); + } else { + const msg = result.stderr.includes("not installed") + ? "Install embeddings support: pip install code-review-graph[embeddings]" + : `Embedding failed: ${result.stderr}`; + vscode.window.showErrorMessage(`Code Graph: ${msg}`); + } + }) + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.watchGraph + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand("codeReviewGraph.watchGraph", async () => { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage("No workspace folder is open."); + return; + } + + vscode.window.showInformationMessage("Code Graph: Watch mode started."); + const result = await cli.watchGraph(workspaceRoot); + if (!result.success) { + vscode.window.showErrorMessage( + `Code Graph: Watch failed. ${result.stderr}` + ); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("codeReviewGraph.showGraph", async () => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded. Run 'Code Graph: Build Graph' first." + ); + return; + } + + GraphWebviewPanel.createOrShow(context.extensionUri, sqliteReader); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand("codeReviewGraph.search", async () => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded." + ); + return; + } + + const query = await vscode.window.showInputBox({ + prompt: "Search the code graph", + placeHolder: "Enter a function, class, or module name", + }); + + if (!query) { + return; + } + + const results = sqliteReader.searchNodes(query); + + if (results.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No results found for "${query}".` + ); + return; + } + + const items = results.map((r) => ({ + label: r.name, + description: r.kind, + detail: r.filePath + ? `${r.filePath}:${r.lineStart ?? ""}` + : undefined, + result: r, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Results for "${query}"`, + }); + + if (selected?.result) { + await navigateToNode(selected.result); + } + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.reviewChanges", + async () => { + if (!sqliteReader) { + vscode.window.showWarningMessage( + "Code Graph: No graph database loaded." + ); + return; + } + + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + vscode.window.showErrorMessage("No workspace folder is open."); + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: "Code Graph: Analyzing changes...", + cancellable: false, + }, + async () => { + const { execFile } = await import("node:child_process"); + const { promisify } = await import("node:util"); + const execFileAsync = promisify(execFile); + + let changedFiles: string[] = []; + try { + const r1 = await execFileAsync( + "git", ["diff", "--name-only", "HEAD"], + { cwd: workspaceRoot } + ); + const r2 = await execFileAsync( + "git", ["diff", "--cached", "--name-only"], + { cwd: workspaceRoot } + ); + changedFiles = [...new Set([ + ...r1.stdout.trim().split("\n").filter(Boolean), + ...r2.stdout.trim().split("\n").filter(Boolean), + ])]; + } catch { + // git not available or not a git repo + } + + if (changedFiles.length === 0) { + vscode.window.showInformationMessage( + "Code Graph: No changes detected." + ); + return; + } + + const absFiles = changedFiles.map(f => path.join(workspaceRoot, f)); + const impact = sqliteReader!.getImpactRadius(absFiles); + + // --- Generate review guidance --- + const guidance: string[] = []; + const impactedFileCount = new Set( + impact.impactedNodes.map(n => n.filePath) + ).size; + + // Test coverage check + const untestedFns: string[] = []; + for (const node of impact.changedNodes) { + if (node.kind !== "Function" || node.isTest) { continue; } + const edges = sqliteReader!.getEdgesByTarget(node.qualifiedName); + const hasCoverage = edges.some(e => e.kind === "TESTED_BY"); + if (!hasCoverage) { + const out = sqliteReader!.getEdgesBySource(node.qualifiedName); + if (!out.some(e => e.kind === "TESTED_BY")) { + untestedFns.push(node.name); + } + } + } + + if (untestedFns.length > 0) { + guidance.push( + `\u26a0\ufe0f **${untestedFns.length} changed function(s) lack test coverage**: ${untestedFns.slice(0, 5).join(", ")}${untestedFns.length > 5 ? "..." : ""}` + ); + } + + // Wide blast radius warning + if (impactedFileCount > 10) { + guidance.push( + `\u26a0\ufe0f **Wide blast radius**: ${impactedFileCount} files impacted — consider splitting this change.` + ); + } + + // Inheritance changes + const inheritanceChanges = impact.edges.filter( + e => e.kind === "INHERITS" || e.kind === "IMPLEMENTS" + ); + if (inheritanceChanges.length > 0) { + guidance.push( + `\u26a0\ufe0f **Inheritance chain affected**: ${inheritanceChanges.length} inheritance/implementation edge(s) touched.` + ); + } + + // Cross-file impacts + if (impact.impactedNodes.length > 0) { + guidance.push( + `\u2139\ufe0f ${impact.impactedNodes.length} nodes in ${impactedFileCount} file(s) may be affected by these changes.` + ); + } + + // Show guidance in output channel + const channel = vscode.window.createOutputChannel("Code Graph Review", { log: true }); + channel.appendLine("# Review Guidance"); + channel.appendLine(""); + channel.appendLine(`Changed files: ${changedFiles.length}`); + channel.appendLine(`Changed nodes: ${impact.changedNodes.length}`); + channel.appendLine(`Impacted nodes: ${impact.impactedNodes.length}`); + channel.appendLine(`Impacted files: ${impactedFileCount}`); + channel.appendLine(""); + if (guidance.length > 0) { + for (const g of guidance) { channel.appendLine(g); } + } else { + channel.appendLine("\u2705 No concerns detected."); + } + channel.show(true); + + // Also show in graph + GraphWebviewPanel.createOrShow( + context.extensionUri, + sqliteReader!, + impact + ); + + // Update SCM decorations + if (scmDecorationProvider && sqliteReader) { + await scmDecorationProvider.update(sqliteReader, workspaceRoot); + } + } + ); + } + ) + ); +} + +/** + * Reinitialize the reader and tree providers after a graph rebuild. + */ +async function reinitialize( + context: vscode.ExtensionContext +): Promise { + const workspaceRoot = getWorkspaceRoot(); + if (!workspaceRoot) { + return; + } + + const dbPath = findGraphDb(workspaceRoot); + if (!dbPath) { + return; + } + + sqliteReader?.close(); + sqliteReader = new SqliteReader(dbPath); + + // Refresh tree views + await vscode.commands.executeCommand( + "codeReviewGraph.codeGraph.refresh" + ); +} + +/** + * Set up a FileSystemWatcher to detect changes to the graph database. + */ +function watchGraphDb(context: vscode.ExtensionContext): void { + const watcher = vscode.workspace.createFileSystemWatcher( + "**/.code-review-graph/graph.db" + ); + + const dbPathRef = { current: "" }; + const workspaceRoot = getWorkspaceRoot(); + if (workspaceRoot) { + const dbPath = findGraphDb(workspaceRoot); + if (dbPath) { + dbPathRef.current = dbPath; + } + } + + watcher.onDidChange(() => { + // Close and reopen to pick up external writes + if (sqliteReader && dbPathRef.current) { + sqliteReader.close(); + sqliteReader = new SqliteReader(dbPathRef.current); + vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh"); + } + }); + + watcher.onDidCreate(async () => { + const wsRoot = getWorkspaceRoot(); + if (wsRoot && !sqliteReader) { + const dbPath = findGraphDb(wsRoot); + if (dbPath) { + dbPathRef.current = dbPath; + sqliteReader = new SqliteReader(dbPath); + vscode.commands.executeCommand("codeReviewGraph.codeGraph.refresh"); + } + } + }); + + watcher.onDidDelete(() => { + sqliteReader?.close(); + sqliteReader = undefined; + dbPathRef.current = ""; + }); + + context.subscriptions.push(watcher); +} + +/** + * Set up debounced auto-update on file save. + */ +function setupAutoUpdate( + context: vscode.ExtensionContext, + cli: CliWrapper +): void { + const AUTO_UPDATE_DEBOUNCE_MS = 2000; + + const onSave = vscode.workspace.onDidSaveTextDocument(() => { + const config = vscode.workspace.getConfiguration("codeReviewGraph"); + if (!config.get("autoUpdate", true)) { + return; + } + + if (autoUpdateTimer) { + clearTimeout(autoUpdateTimer); + } + + autoUpdateTimer = setTimeout(async () => { + const wsRoot = getWorkspaceRoot(); + if (!wsRoot || !sqliteReader) { + return; + } + + try { + await cli.updateGraph(wsRoot); + } catch { + // Silently ignore update errors on save; user can manually update + } + }, AUTO_UPDATE_DEBOUNCE_MS); + }); + + context.subscriptions.push(onSave); +} + +/** + * Extension activation entry point. + */ +export async function activate( + context: vscode.ExtensionContext +): Promise { + const cli = new CliWrapper(); + const installer = new Installer(cli); + + // Register walkthrough commands + registerWalkthroughCommands(context, cli, installer); + + const workspaceRoot = getWorkspaceRoot(); + + if (workspaceRoot) { + const dbPath = findGraphDb(workspaceRoot); + + if (dbPath) { + // Graph database found - initialize + sqliteReader = new SqliteReader(dbPath); + + // Schema compatibility check + const schemaWarning = sqliteReader.checkSchemaCompatibility(); + if (schemaWarning) { + const choice = await vscode.window.showWarningMessage( + `Code Graph: ${schemaWarning}`, + "Rebuild Graph", + "Dismiss" + ); + if (choice === "Rebuild Graph") { + await vscode.commands.executeCommand("codeReviewGraph.buildGraph"); + } + } + + // Register tree view providers + const codeGraphProvider = new CodeGraphTreeProvider( + sqliteReader, + workspaceRoot + ); + const blastRadiusProvider = new BlastRadiusTreeProvider(); + const statsProvider = new StatsTreeProvider(sqliteReader); + + context.subscriptions.push( + vscode.window.registerTreeDataProvider( + "codeReviewGraph.codeGraph", + codeGraphProvider + ), + vscode.window.registerTreeDataProvider( + "codeReviewGraph.blastRadius", + blastRadiusProvider + ), + vscode.window.registerTreeDataProvider( + "codeReviewGraph.stats", + statsProvider + ) + ); + + // Create status bar + const statusBar = new StatusBar(); + statusBar.update(sqliteReader); + statusBar.show(); + context.subscriptions.push(statusBar); + + // Register SCM file decoration provider + scmDecorationProvider = new ScmDecorationProvider(); + context.subscriptions.push( + vscode.window.registerFileDecorationProvider(scmDecorationProvider) + ); + } else { + // No graph database found - show welcome + showWelcomeIfNeeded(context); + } + } + + // Register revealInTree command for bidirectional graph→tree sync + context.subscriptions.push( + vscode.commands.registerCommand( + "codeReviewGraph.revealInTree", + (_qualifiedName: string) => { + // When a node is clicked in the graph, highlight it in the graph + // The graph webview already calls this; the tree view sync relies + // on the file navigation that nodeClicked also triggers. + // This command is a hook for future tree reveal integration. + GraphWebviewPanel.highlightNode(_qualifiedName); + } + ) + ); + + // Register commands (always, even without a database) + registerCommands(context, cli); + + // Watch for graph.db changes + watchGraphDb(context); + + // Set up auto-update on save + setupAutoUpdate(context, cli); +} + +/** + * Extension deactivation cleanup. + */ +export function deactivate(): void { + if (autoUpdateTimer) { + clearTimeout(autoUpdateTimer); + autoUpdateTimer = undefined; + } + + sqliteReader?.close(); + sqliteReader = undefined; +} diff --git a/code-review-graph-vscode/src/features/blastRadius.ts b/code-review-graph-vscode/src/features/blastRadius.ts new file mode 100644 index 0000000..5781231 --- /dev/null +++ b/code-review-graph-vscode/src/features/blastRadius.ts @@ -0,0 +1,70 @@ +import * as vscode from 'vscode'; +import { SqliteReader } from '../backend/sqlite'; +import { BlastRadiusTreeProvider } from '../views/treeView'; +import { resolveNodeAtCursor } from './cursorResolver'; + +/** + * Register the cursor-aware blast radius command. + * + * When invoked the command: + * 1. Gets the active editor's file path and cursor line. + * 2. Resolves the innermost node at cursor via the graph database. + * 3. Falls back to the file-level node when no specific node is found. + * 4. Runs a BFS impact radius query up to the configured depth. + * 5. Updates the BlastRadiusTreeProvider with the results. + * 6. Focuses the blast radius tree view. + */ +export function registerBlastRadiusCommand( + context: vscode.ExtensionContext, + getReader: () => SqliteReader | undefined, + blastRadiusProvider: BlastRadiusTreeProvider, + workspaceRoot: string, +): void { + context.subscriptions.push( + vscode.commands.registerCommand('codeReviewGraph.showBlastRadius', async () => { + const reader = getReader(); + if (!reader) { + vscode.window.showWarningMessage('Code Graph: No graph database loaded.'); + return; + } + + // --- Active editor check --- + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage('Open a file first'); + return; + } + + // --- Resolve file path and cursor position --- + const absFilePath = editor.document.uri.fsPath; + const cursorLine = editor.selection.active.line + 1; // 1-based + + // --- Resolve node at cursor --- + const nodeAtCursor = reader.getNodeAtCursor(absFilePath, cursorLine); + + // Determine the file path to feed into getImpactRadius. + // If we found a node, use its filePath (which is the canonical path + // stored in the database). Otherwise fall back to the editor path. + const filePath = nodeAtCursor ? nodeAtCursor.filePath : absFilePath; + + // --- Read depth from settings --- + const config = vscode.workspace.getConfiguration('codeReviewGraph'); + const depth = config.get('blastRadiusDepth', 2); + + // --- Compute blast radius --- + const impact = reader.getImpactRadius([filePath], depth); + + // --- Update tree provider --- + blastRadiusProvider.setResults(impact.changedNodes, impact.impactedNodes); + + // --- Focus the blast radius view --- + await vscode.commands.executeCommand('codeReviewGraph.blastRadius.focus'); + + // --- Summary message --- + const impactedFileCount = new Set(impact.impactedNodes.map((n) => n.filePath)).size; + vscode.window.showInformationMessage( + `Blast radius: ${impact.impactedNodes.length} nodes impacted across ${impactedFileCount} files`, + ); + }), + ); +} diff --git a/code-review-graph-vscode/src/features/cursorResolver.ts b/code-review-graph-vscode/src/features/cursorResolver.ts new file mode 100644 index 0000000..60cc03e --- /dev/null +++ b/code-review-graph-vscode/src/features/cursorResolver.ts @@ -0,0 +1,37 @@ +import * as vscode from 'vscode'; +import { SqliteReader, GraphNode } from '../backend/sqlite'; + +/** + * Resolve the innermost graph node at the current cursor position. + * + * Returns `undefined` when there is no active editor or no node spans the + * cursor line in the graph database. + */ +export function resolveNodeAtCursor( + reader: SqliteReader, +): GraphNode | undefined { + const editor = vscode.window.activeTextEditor; + if (!editor) { + return undefined; + } + + const filePath = editor.document.uri.fsPath; + const line = editor.selection.active.line + 1; // VS Code is 0-based, SQLite data is 1-based + + return reader.getNodeAtCursor(filePath, line); +} + +/** + * Open a document and scroll to the node's start line. + * + * The node's `filePath` is treated as an absolute path. If `lineStart` is + * null the file is opened at the top. + */ +export async function navigateToNode(node: GraphNode): Promise { + const uri = vscode.Uri.file(node.filePath); + const doc = await vscode.workspace.openTextDocument(uri); + const line = Math.max(0, (node.lineStart ?? 1) - 1); + await vscode.window.showTextDocument(doc, { + selection: new vscode.Range(line, 0, line, 0), + }); +} diff --git a/code-review-graph-vscode/src/features/navigation.ts b/code-review-graph-vscode/src/features/navigation.ts new file mode 100644 index 0000000..1fbc117 --- /dev/null +++ b/code-review-graph-vscode/src/features/navigation.ts @@ -0,0 +1,196 @@ +import * as vscode from 'vscode'; +import { SqliteReader, GraphNode } from '../backend/sqlite'; +import { resolveNodeAtCursor, navigateToNode } from './cursorResolver'; + +/** + * Register the navigation commands: findCallers, findTests, and search. + */ +export function registerNavigationCommands( + context: vscode.ExtensionContext, + getReader: () => SqliteReader | undefined, +): void { + // ----------------------------------------------------------------- + // codeReviewGraph.findCallers + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand('codeReviewGraph.findCallers', async () => { + const reader = getReader(); + if (!reader) { + vscode.window.showWarningMessage('Code Graph: No graph database loaded.'); + return; + } + + // Resolve node at cursor + const node = resolveNodeAtCursor(reader); + if (!node) { + vscode.window.showWarningMessage( + 'Code Graph: No graph node found at the current cursor position.', + ); + return; + } + + // Query incoming CALLS edges + const edges = reader.getEdgesByTarget(node.qualifiedName); + const callerEdges = edges.filter((e) => e.kind === 'CALLS'); + + if (callerEdges.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No callers found for "${node.name}".`, + ); + return; + } + + // Build QuickPick items, resolving each caller to its full node + const items: Array<{ + label: string; + description: string; + detail: string; + node: GraphNode | undefined; + }> = []; + + for (const edge of callerEdges) { + const callerNode = reader.getNode(edge.sourceQualified); + items.push({ + label: callerNode?.name ?? edge.sourceQualified, + description: callerNode?.filePath ?? edge.filePath, + detail: `Line ${callerNode?.lineStart ?? edge.line}`, + node: callerNode, + }); + } + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Callers of ${node.name}`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + }), + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.findTests + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand('codeReviewGraph.findTests', async () => { + const reader = getReader(); + if (!reader) { + vscode.window.showWarningMessage('Code Graph: No graph database loaded.'); + return; + } + + // Resolve node at cursor + const node = resolveNodeAtCursor(reader); + if (!node) { + vscode.window.showWarningMessage( + 'Code Graph: No graph node found at the current cursor position.', + ); + return; + } + + // --- Collect test qualified names from TESTED_BY edges (both directions) --- + const incomingEdges = reader.getEdgesByTarget(node.qualifiedName); + const incomingTestEdges = incomingEdges.filter((e) => e.kind === 'TESTED_BY'); + + const outgoingEdges = reader.getEdgesBySource(node.qualifiedName); + const outgoingTestEdges = outgoingEdges.filter((e) => e.kind === 'TESTED_BY'); + + const testQualifiedNames = new Set([ + ...incomingTestEdges.map((e) => e.sourceQualified), + ...outgoingTestEdges.map((e) => e.targetQualified), + ]); + + // --- Also search by naming convention: test_{name}, Test{name} --- + const conventionPatterns = [`test_${node.name}`, `Test${node.name}`]; + for (const pattern of conventionPatterns) { + const matches = reader.searchNodes(pattern, 10); + for (const match of matches) { + if (match.isTest || match.kind === 'Test') { + testQualifiedNames.add(match.qualifiedName); + } + } + } + + if (testQualifiedNames.size === 0) { + vscode.window.showInformationMessage( + `Code Graph: No tests found for "${node.name}".`, + ); + return; + } + + // --- Build QuickPick items --- + const items: Array<{ + label: string; + description: string; + detail: string; + node: GraphNode | undefined; + }> = []; + + for (const tqn of testQualifiedNames) { + const testNode = reader.getNode(tqn); + items.push({ + label: testNode?.name ?? tqn, + description: testNode?.filePath ?? '', + detail: `Line ${testNode?.lineStart ?? '?'}`, + node: testNode, + }); + } + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Tests for ${node.name}`, + }); + + if (selected?.node) { + await navigateToNode(selected.node); + } + }), + ); + + // ----------------------------------------------------------------- + // codeReviewGraph.search + // ----------------------------------------------------------------- + context.subscriptions.push( + vscode.commands.registerCommand('codeReviewGraph.search', async () => { + const reader = getReader(); + if (!reader) { + vscode.window.showWarningMessage('Code Graph: No graph database loaded.'); + return; + } + + const query = await vscode.window.showInputBox({ + prompt: 'Search the code graph', + placeHolder: 'Enter a function, class, or module name', + }); + + if (!query) { + return; + } + + const results = reader.searchNodes(query, 30); + + if (results.length === 0) { + vscode.window.showInformationMessage( + `Code Graph: No results found for "${query}".`, + ); + return; + } + + const items = results.map((r) => ({ + label: r.name, + description: r.kind, + detail: r.filePath + ? `${r.filePath}:${r.lineStart ?? ''}` + : undefined, + result: r, + })); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: `Results for "${query}"`, + }); + + if (selected?.result) { + await navigateToNode(selected.result); + } + }), + ); +} diff --git a/code-review-graph-vscode/src/features/reviewAssistant.ts b/code-review-graph-vscode/src/features/reviewAssistant.ts new file mode 100644 index 0000000..730d557 --- /dev/null +++ b/code-review-graph-vscode/src/features/reviewAssistant.ts @@ -0,0 +1,117 @@ +/** + * SCM integration for code review. + * + * Detects staged and unstaged changes via git, computes the blast radius + * for those files, and populates the Blast Radius tree view so the reviewer + * can see what is impacted before committing. + */ + +import * as vscode from 'vscode'; +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { SqliteReader } from '../backend/sqlite'; +import { BlastRadiusTreeProvider } from '../views/treeView'; + +const execFileAsync = promisify(execFile); + +/** Timeout for git commands (milliseconds). */ +const GIT_TIMEOUT_MS = 10_000; + +/** + * Run a git command in the given working directory and return trimmed stdout + * lines. Returns an empty array on any error (e.g. git not installed, not a + * git repo, etc.). + */ +async function gitLines( + args: string[], + cwd: string, +): Promise { + try { + const { stdout } = await execFileAsync('git', args, { + cwd, + timeout: GIT_TIMEOUT_MS, + }); + return stdout + .trim() + .split('\n') + .filter((line) => line.length > 0); + } catch { + return []; + } +} + +/** + * Register the `codeReviewGraph.reviewChanges` command. + * + * The command: + * 1. Runs `git diff --name-only HEAD` and `git diff --cached --name-only` + * to collect changed + staged files. + * 2. Computes the blast radius for those files. + * 3. Updates the BlastRadiusTreeProvider with the results. + * 4. Focuses the blast radius view. + */ +export function registerReviewCommand( + context: vscode.ExtensionContext, + reader: SqliteReader, + blastRadiusProvider: BlastRadiusTreeProvider, +): void { + const disposable = vscode.commands.registerCommand( + 'codeReviewGraph.reviewChanges', + async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showErrorMessage('No workspace folder is open.'); + return; + } + + const workspaceRoot = workspaceFolder.uri.fsPath; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Code Graph: Analyzing changes...', + cancellable: false, + }, + async () => { + // 1. Collect changed files (unstaged + staged, deduplicated) + const [unstaged, staged] = await Promise.all([ + gitLines(['diff', '--name-only', 'HEAD'], workspaceRoot), + gitLines(['diff', '--cached', '--name-only'], workspaceRoot), + ]); + + const changedFiles = [...new Set([...unstaged, ...staged])]; + + if (changedFiles.length === 0) { + vscode.window.showInformationMessage( + 'No changes detected.', + ); + return; + } + + // 2. Compute blast radius + const config = vscode.workspace.getConfiguration('codeReviewGraph'); + const depth = config.get('blastRadiusDepth', 2); + const impact = reader.getImpactRadius(changedFiles, depth); + + // 3. Update tree provider + blastRadiusProvider.setResults( + impact.changedNodes, + impact.impactedNodes, + ); + + // 4. Focus the blast radius view + await vscode.commands.executeCommand( + 'codeReviewGraph.blastRadius.focus', + ); + + // 5. Show summary + vscode.window.showInformationMessage( + `Review: ${changedFiles.length} changed file(s) impact ${impact.impactedNodes.length} additional file(s).`, + ); + }, + ); + }, + ); + + context.subscriptions.push(disposable); +} diff --git a/code-review-graph-vscode/src/features/scmDecorations.ts b/code-review-graph-vscode/src/features/scmDecorations.ts new file mode 100644 index 0000000..e51b181 --- /dev/null +++ b/code-review-graph-vscode/src/features/scmDecorations.ts @@ -0,0 +1,160 @@ +/** + * SCM file decoration provider. + * + * Adds badges to files in the Explorer and SCM views: + * - IMPACTED (orange) — file is in the blast radius of staged/unstaged changes + * - TESTED (green) — changed functions in this file have test coverage + * - UNTESTED (red) — changed functions lack test coverage + */ + +import * as vscode from 'vscode'; +import { SqliteReader } from '../backend/sqlite'; + +export class ScmDecorationProvider + implements vscode.FileDecorationProvider +{ + private readonly _onDidChange = new vscode.EventEmitter(); + readonly onDidChangeFileDecorations = this._onDidChange.event; + + /** Files directly changed (staged + unstaged). */ + private changedFiles = new Set(); + /** Files in the blast radius but not directly changed. */ + private impactedFiles = new Set(); + /** Changed files whose functions all have TESTED_BY edges. */ + private testedFiles = new Set(); + /** Changed files with at least one function lacking TESTED_BY edges. */ + private untestedFiles = new Set(); + + /** + * Recompute decorations from git state and the graph database. + */ + async update( + reader: SqliteReader, + workspaceRoot: string, + ): Promise { + const { execFile } = await import('node:child_process'); + const { promisify } = await import('node:util'); + const execFileAsync = promisify(execFile); + const path = await import('node:path'); + + // 1. Collect changed files + let unstaged: string[] = []; + let staged: string[] = []; + try { + const r1 = await execFileAsync('git', ['diff', '--name-only', 'HEAD'], { + cwd: workspaceRoot, + timeout: 10_000, + }); + unstaged = r1.stdout.trim().split('\n').filter(Boolean); + } catch { /* ignore */ } + try { + const r2 = await execFileAsync('git', ['diff', '--cached', '--name-only'], { + cwd: workspaceRoot, + timeout: 10_000, + }); + staged = r2.stdout.trim().split('\n').filter(Boolean); + } catch { /* ignore */ } + + const changedRelative = [...new Set([...unstaged, ...staged])]; + const changedAbsolute = changedRelative.map((f) => path.join(workspaceRoot, f)); + + // 2. Compute impact radius + const config = vscode.workspace.getConfiguration('codeReviewGraph'); + const depth = config.get('blastRadiusDepth', 2); + const impact = reader.getImpactRadius(changedAbsolute, depth); + + // 3. Classify files + this.changedFiles = new Set(changedAbsolute); + this.impactedFiles = new Set( + impact.impactedNodes + .map((n) => n.filePath) + .filter((f) => !this.changedFiles.has(f)), + ); + + // 4. Test coverage classification + this.testedFiles = new Set(); + this.untestedFiles = new Set(); + + for (const filePath of this.changedFiles) { + const nodes = reader.getNodesByFile(filePath); + const functions = nodes.filter( + (n) => n.kind === 'Function' && !n.isTest, + ); + if (functions.length === 0) { + continue; + } + + let allTested = true; + for (const fn of functions) { + const edges = reader.getEdgesByTarget(fn.qualifiedName); + const hasTest = edges.some((e) => e.kind === 'TESTED_BY'); + if (!hasTest) { + // Also check outgoing TESTED_BY (reverse direction) + const outEdges = reader.getEdgesBySource(fn.qualifiedName); + const hasOutTest = outEdges.some((e) => e.kind === 'TESTED_BY'); + if (!hasOutTest) { + allTested = false; + break; + } + } + } + + if (allTested) { + this.testedFiles.add(filePath); + } else { + this.untestedFiles.add(filePath); + } + } + + // 5. Fire change event + this._onDidChange.fire(undefined); + } + + /** Clear all decorations. */ + clear(): void { + this.changedFiles.clear(); + this.impactedFiles.clear(); + this.testedFiles.clear(); + this.untestedFiles.clear(); + this._onDidChange.fire(undefined); + } + + provideFileDecoration( + uri: vscode.Uri, + ): vscode.FileDecoration | undefined { + const filePath = uri.fsPath; + + if (this.untestedFiles.has(filePath)) { + return { + badge: '!', + color: new vscode.ThemeColor('editorError.foreground'), + tooltip: 'Code Graph: Changed functions lack test coverage', + propagate: false, + }; + } + + if (this.testedFiles.has(filePath)) { + return { + badge: '\u2713', + color: new vscode.ThemeColor('testing.iconPassed'), + tooltip: 'Code Graph: All changed functions have test coverage', + propagate: false, + }; + } + + if (this.impactedFiles.has(filePath)) { + return { + badge: '\u25CF', + color: new vscode.ThemeColor('editorWarning.foreground'), + tooltip: 'Code Graph: In blast radius of current changes', + propagate: false, + }; + } + + return undefined; + } + + dispose(): void { + this._onDidChange.dispose(); + } +} diff --git a/code-review-graph-vscode/src/features/search.ts b/code-review-graph-vscode/src/features/search.ts new file mode 100644 index 0000000..3da2b28 --- /dev/null +++ b/code-review-graph-vscode/src/features/search.ts @@ -0,0 +1,131 @@ +/** + * Quick search command with live filtering. + * + * Shows a QuickPick that queries the graph database as the user types, + * then navigates to the selected node's source location. + */ + +import * as vscode from 'vscode'; +import * as path from 'node:path'; +import { SqliteReader, GraphNode } from '../backend/sqlite'; + +// --------------------------------------------------------------------------- +// Kind-to-icon mapping (uses VS Code codicon identifiers) +// --------------------------------------------------------------------------- + +const KIND_ICON: Record = { + Function: '$(symbol-method)', + Class: '$(symbol-class)', + File: '$(file)', + Test: '$(beaker)', + Type: '$(symbol-interface)', +}; + +/** + * Build a QuickPickItem from a GraphNode. + */ +function nodeToQuickPickItem( + node: GraphNode, + workspaceRoot: string | undefined, +): vscode.QuickPickItem & { node: GraphNode } { + const icon = KIND_ICON[node.kind] ?? '$(symbol-misc)'; + const relativePath = workspaceRoot + ? path.relative(workspaceRoot, node.filePath) + : node.filePath; + const lineInfo = node.lineStart != null ? `:${node.lineStart}` : ''; + + return { + label: `${icon} ${node.name}`, + description: node.kind, + detail: `${relativePath}${lineInfo}`, + node, + }; +} + +/** + * Navigate to a node's source location. + */ +async function navigateToNode( + node: GraphNode, + workspaceRoot: string | undefined, +): Promise { + const filePath = workspaceRoot + ? path.join(workspaceRoot, node.filePath) + : node.filePath; + + const uri = vscode.Uri.file(filePath); + const doc = await vscode.workspace.openTextDocument(uri); + const line = Math.max(0, (node.lineStart ?? 1) - 1); + await vscode.window.showTextDocument(doc, { + selection: new vscode.Range(line, 0, line, 0), + }); +} + +/** + * Register the `codeReviewGraph.search` command. + * + * Opens a QuickPick with live filtering: + * - As the user types, `reader.searchNodes(value, 20)` is called. + * - Results are displayed with kind-specific icons. + * - On accept, the editor navigates to the selected node. + */ +export function registerSearchCommand( + context: vscode.ExtensionContext, + reader: SqliteReader, +): void { + const disposable = vscode.commands.registerCommand( + 'codeReviewGraph.search', + async () => { + const workspaceRoot = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + + const quickPick = vscode.window.createQuickPick< + vscode.QuickPickItem & { node: GraphNode } + >(); + quickPick.placeholder = 'Search for functions, classes, files, types...'; + quickPick.matchOnDescription = true; + quickPick.matchOnDetail = true; + + // Debounce timer to avoid querying on every keystroke + let debounceTimer: ReturnType | undefined; + + quickPick.onDidChangeValue((value) => { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + + if (!value) { + quickPick.items = []; + return; + } + + debounceTimer = setTimeout(() => { + const results = reader.searchNodes(value, 20); + quickPick.items = results.map((node) => + nodeToQuickPickItem(node, workspaceRoot), + ); + }, 100); + }); + + quickPick.onDidAccept(async () => { + const selected = quickPick.selectedItems[0]; + quickPick.dispose(); + + if (selected?.node) { + await navigateToNode(selected.node, workspaceRoot); + } + }); + + quickPick.onDidHide(() => { + if (debounceTimer) { + clearTimeout(debounceTimer); + } + quickPick.dispose(); + }); + + quickPick.show(); + }, + ); + + context.subscriptions.push(disposable); +} diff --git a/code-review-graph-vscode/src/onboarding/installer.ts b/code-review-graph-vscode/src/onboarding/installer.ts new file mode 100644 index 0000000..7cd5437 --- /dev/null +++ b/code-review-graph-vscode/src/onboarding/installer.ts @@ -0,0 +1,114 @@ +import * as vscode from 'vscode'; +import { CliWrapper } from '../backend/cli'; + +/** + * Handles auto-detection and installation of the Python backend. + * + * Checks whether the `code-review-graph` CLI is available and, if not, + * guides the user through installation via pip/pipx or manual instructions. + */ +export class Installer { + constructor(private cli: CliWrapper) {} + + /** + * Check whether the backend is installed and prompt the user if it is not. + * + * @returns `true` if the backend is installed (or was just installed + * successfully), `false` if the user dismissed the prompt or + * installation failed. + */ + async checkAndPrompt(): Promise { + const installed = await this.cli.isInstalled(); + if (installed) { + return true; + } + + const selection = await vscode.window.showInformationMessage( + 'Code Review Graph backend is not installed.', + 'Install Now', + 'Manual Instructions', + 'Dismiss', + ); + + if (selection === 'Install Now') { + return this.autoInstall(); + } + + if (selection === 'Manual Instructions') { + const terminal = vscode.window.createTerminal('Code Review Graph Setup'); + terminal.show(); + terminal.sendText('echo "=== Code Review Graph - Manual Installation ==="'); + terminal.sendText('echo ""'); + terminal.sendText('echo "Option 1: Install with pip"'); + terminal.sendText('echo " pip install code-review-graph"'); + terminal.sendText('echo ""'); + terminal.sendText('echo "Option 2: Install with pipx (recommended)"'); + terminal.sendText('echo " pipx install code-review-graph"'); + terminal.sendText('echo ""'); + terminal.sendText('echo "After installation, reload the VS Code window."'); + return false; + } + + // Dismissed + return false; + } + + /** + * Attempt to automatically install the backend using the first available + * Python package installer (pip or pipx). + * + * @returns `true` if installation succeeded, `false` otherwise. + */ + async autoInstall(): Promise { + const installer = await this.cli.detectPythonInstaller(); + + if (!installer) { + const openLink = 'Download Python'; + const response = await vscode.window.showErrorMessage( + 'Python 3.10+ is required. Install Python first.', + openLink, + ); + + if (response === openLink) { + vscode.env.openExternal( + vscode.Uri.parse('https://www.python.org/downloads/'), + ); + } + + return false; + } + + let success = false; + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: `Installing code-review-graph via ${installer}...`, + cancellable: false, + }, + async () => { + try { + await this.cli.installBackend(installer); + success = true; + } catch { + success = false; + } + }, + ); + + if (success) { + vscode.window.showInformationMessage( + 'Backend installed successfully!', + ); + return true; + } + + vscode.window.showErrorMessage( + `Failed to install code-review-graph via ${installer}. ` + + 'Check the terminal output for details or try installing manually: ' + + `\`${installer} install code-review-graph\``, + ); + + return false; + } +} diff --git a/code-review-graph-vscode/src/onboarding/welcome.ts b/code-review-graph-vscode/src/onboarding/welcome.ts new file mode 100644 index 0000000..8e9af56 --- /dev/null +++ b/code-review-graph-vscode/src/onboarding/welcome.ts @@ -0,0 +1,113 @@ +import * as vscode from 'vscode'; +import { Installer } from './installer'; +import { CliWrapper } from '../backend/cli'; + +/** + * Register command handlers for the walkthrough steps defined in + * `package.json` contributes.walkthroughs. + * + * Each walkthrough step button triggers one of these commands so the user + * can install the CLI, build the graph, and explore the sidebar without + * leaving the walkthrough. + */ +export function registerWalkthroughCommands( + context: vscode.ExtensionContext, + cli: CliWrapper, + installer: Installer, +): void { + context.subscriptions.push( + vscode.commands.registerCommand( + 'codeReviewGraph.walkthrough.install', + async () => { + await installer.autoInstall(); + }, + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'codeReviewGraph.walkthrough.build', + async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + if (!workspaceFolder) { + vscode.window.showWarningMessage( + 'No workspace folder is open. Open a folder first.', + ); + return; + } + + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: 'Code Graph: Building graph...', + cancellable: false, + }, + async () => { + await cli.buildGraph(workspaceFolder.uri.fsPath); + }, + ); + + vscode.window.showInformationMessage( + 'Code Graph: Build complete.', + ); + }, + ), + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'codeReviewGraph.walkthrough.explore', + async () => { + await vscode.commands.executeCommand( + 'codeReviewGraph.codeGraph.focus', + ); + }, + ), + ); +} + +/** + * Show a welcome notification if no graph database has been built yet + * in any of the open workspace folders. + * + * Checks for `.code-review-graph/graph.db` in every workspace folder. + * When none is found, a notification is shown with a button that opens + * the built-in walkthrough. + */ +export async function showWelcomeIfNeeded( + context: vscode.ExtensionContext, +): Promise { + const workspaceFolders = vscode.workspace.workspaceFolders; + if (!workspaceFolders || workspaceFolders.length === 0) { + return; + } + + for (const folder of workspaceFolders) { + const dbUri = vscode.Uri.joinPath( + folder.uri, + '.code-review-graph', + 'graph.db', + ); + + try { + await vscode.workspace.fs.stat(dbUri); + // Database exists in at least one folder -- no need to prompt. + return; + } catch { + // File does not exist in this folder -- continue checking. + } + } + + // No graph.db found in any workspace folder. + const selection = await vscode.window.showInformationMessage( + 'Welcome to Code Review Graph! Get started by building your code graph.', + 'Get Started', + ); + + if (selection === 'Get Started') { + await vscode.commands.executeCommand( + 'workbench.action.openWalkthrough', + 'tirth8205.code-review-graph#codeReviewGraph.welcome', + ); + } +} diff --git a/code-review-graph-vscode/src/views/graphWebview.ts b/code-review-graph-vscode/src/views/graphWebview.ts new file mode 100644 index 0000000..039d102 --- /dev/null +++ b/code-review-graph-vscode/src/views/graphWebview.ts @@ -0,0 +1,633 @@ +/** + * Webview panel for the interactive graph visualization. + * Uses D3.js (bundled via esbuild) to render a force-directed graph. + * + * Hosts the toolbar HTML, CSS, and manages communication with the + * browser-side graph.ts script. + */ + +import * as vscode from "vscode"; +import * as path from "node:path"; +import * as crypto from "node:crypto"; +import type { SqliteReader, ImpactRadius } from "../backend/sqlite"; + +export class GraphWebviewPanel { + private static currentPanel: GraphWebviewPanel | undefined; + private readonly panel: vscode.WebviewPanel; + private readonly reader: SqliteReader; + private readonly impactRadius?: ImpactRadius; + private readonly highlightQualifiedName?: string; + private disposables: vscode.Disposable[] = []; + + private constructor( + panel: vscode.WebviewPanel, + extensionUri: vscode.Uri, + reader: SqliteReader, + impactRadius?: ImpactRadius, + highlightQualifiedName?: string + ) { + this.panel = panel; + this.reader = reader; + this.impactRadius = impactRadius; + this.highlightQualifiedName = highlightQualifiedName; + + this.panel.onDidDispose(() => this.dispose(), null, this.disposables); + + this.panel.webview.html = this.getHtmlContent( + this.panel.webview, + extensionUri + ); + + this.panel.webview.onDidReceiveMessage( + (message) => this.handleMessage(message), + null, + this.disposables + ); + + // Listen for theme changes + this.disposables.push( + vscode.window.onDidChangeActiveColorTheme((theme) => { + const themeKind = + theme.kind === vscode.ColorThemeKind.Light || + theme.kind === vscode.ColorThemeKind.HighContrastLight + ? "light" + : "dark"; + this.panel.webview.postMessage({ + command: "setTheme", + theme: themeKind, + }); + }) + ); + } + + static createOrShow( + extensionUri: vscode.Uri, + reader: SqliteReader, + impactRadius?: ImpactRadius, + highlightQualifiedName?: string + ): void { + const column = vscode.ViewColumn.Beside; + + if (GraphWebviewPanel.currentPanel) { + GraphWebviewPanel.currentPanel.panel.reveal(column); + + // Re-send data if a new highlight is requested + if (highlightQualifiedName) { + GraphWebviewPanel.currentPanel.panel.webview.postMessage({ + command: "highlightNode", + qualifiedName: highlightQualifiedName, + }); + } + + return; + } + + const panel = vscode.window.createWebviewPanel( + "codeReviewGraph.graph", + "Code Graph", + column, + { + enableScripts: true, + retainContextWhenHidden: true, + localResourceRoots: [vscode.Uri.joinPath(extensionUri, "dist")], + } + ); + + GraphWebviewPanel.currentPanel = new GraphWebviewPanel( + panel, + extensionUri, + reader, + impactRadius, + highlightQualifiedName + ); + } + + private dispose(): void { + GraphWebviewPanel.currentPanel = undefined; + this.panel.dispose(); + for (const d of this.disposables) { + d.dispose(); + } + this.disposables = []; + } + + // ----------------------------------------------------------------------- + // Message handling + // ----------------------------------------------------------------------- + + private handleMessage(message: { + command: string; + [key: string]: unknown; + }): void { + switch (message.command) { + case "ready": + this.sendGraphData(); + break; + + case "nodeClicked": + this.openFileAtLine( + message.filePath as string, + message.lineStart as number + ); + // Bidirectional sync: reveal in tree view + if (message.qualifiedName) { + vscode.commands.executeCommand( + "codeReviewGraph.revealInTree", + message.qualifiedName as string + ); + } + break; + + case "exportSvg": + this.exportSvgToClipboard(message.svg as string); + break; + + case "exportPng": + this.savePngToFile(message.data as string); + break; + } + } + + /** + * Send full graph data to the webview. + * If an impact radius was provided, send only those nodes/edges. + * Otherwise send the full graph. + */ + private sendGraphData(): void { + let nodes; + let edges; + + if (this.impactRadius) { + nodes = [ + ...this.impactRadius.changedNodes, + ...this.impactRadius.impactedNodes, + ]; + edges = this.impactRadius.edges; + } else { + // Load all nodes and edges + const files = this.reader.getAllFiles(); + nodes = files.flatMap((f) => this.reader.getNodesByFile(f)); + const qualifiedNames = new Set(nodes.map((n) => n.qualifiedName)); + edges = this.reader.getEdgesAmong(qualifiedNames); + } + + // Enforce maxNodes setting + const config = vscode.workspace.getConfiguration("codeReviewGraph"); + const maxNodes = config.get("graph.maxNodes", 500); + let truncated = false; + if (nodes.length > maxNodes) { + truncated = true; + nodes = nodes.slice(0, maxNodes); + const nodeQns = new Set(nodes.map((n: { qualifiedName: string }) => n.qualifiedName)); + edges = edges.filter( + (e: { sourceQualified: string; targetQualified: string }) => + nodeQns.has(e.sourceQualified) && nodeQns.has(e.targetQualified) + ); + } + + this.panel.webview.postMessage({ + command: "setData", + nodes, + edges, + truncated, + maxNodes, + }); + + // Send theme + const themeKind = + vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Light || + vscode.window.activeColorTheme.kind === + vscode.ColorThemeKind.HighContrastLight + ? "light" + : "dark"; + this.panel.webview.postMessage({ + command: "setTheme", + theme: themeKind, + }); + + // Highlight node if requested + if (this.highlightQualifiedName) { + // Small delay to let the graph render first + setTimeout(() => { + this.panel.webview.postMessage({ + command: "highlightNode", + qualifiedName: this.highlightQualifiedName, + }); + }, 1000); + } + } + + /** + * Open a file in the editor at a specific line. + */ + private async openFileAtLine( + filePath: string, + lineStart: number + ): Promise { + const workspaceRoot = + vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; + const fullPath = workspaceRoot + ? path.join(workspaceRoot, filePath) + : filePath; + + try { + const doc = await vscode.workspace.openTextDocument(fullPath); + const line = Math.max(0, (lineStart ?? 1) - 1); + await vscode.window.showTextDocument(doc, { + viewColumn: vscode.ViewColumn.One, + selection: new vscode.Range(line, 0, line, 0), + preserveFocus: false, + }); + } catch { + vscode.window.showWarningMessage( + `Code Graph: Could not open file ${filePath}` + ); + } + } + + /** + * Copy SVG string to clipboard. + */ + private async exportSvgToClipboard(svgString: string): Promise { + await vscode.env.clipboard.writeText(svgString); + vscode.window.showInformationMessage( + "Code Graph: SVG copied to clipboard." + ); + } + + /** + * Save PNG data URL to a file. + */ + private async savePngToFile(dataUrl: string): Promise { + const uri = await vscode.window.showSaveDialog({ + defaultUri: vscode.Uri.file("code-graph.png"), + filters: { "PNG Image": ["png"] }, + }); + if (!uri) { return; } + + const base64 = dataUrl.replace(/^data:image\/png;base64,/, ""); + const buffer = Buffer.from(base64, "base64"); + await vscode.workspace.fs.writeFile(uri, buffer); + vscode.window.showInformationMessage("Code Graph: PNG saved."); + } + + /** + * Highlight a node by qualified name from external code (tree view click). + */ + static highlightNode(qualifiedName: string): void { + if (GraphWebviewPanel.currentPanel) { + GraphWebviewPanel.currentPanel.panel.webview.postMessage({ + command: "highlightNode", + qualifiedName, + }); + } + } + + // ----------------------------------------------------------------------- + // HTML content + // ----------------------------------------------------------------------- + + private getHtmlContent( + webview: vscode.Webview, + extensionUri: vscode.Uri + ): string { + const scriptUri = webview.asWebviewUri( + vscode.Uri.joinPath(extensionUri, "dist", "webview", "graph.js") + ); + + const nonce = getNonce(); + + return ` + + + + + + Code Graph + + + + +

+ +
+ +
+ +
+ + +
+ +
+ Calls + Imports + Inherits + Implements + Tested + Contains + Depends +
+
+ +
+ + +
+ Depth + + All +
+ +
+ + +
+ + + +
+ +
+ + +
+ + + +
+ + + + + + +`; + } +} + +function getNonce(): string { + return crypto.randomBytes(16).toString("hex"); +} diff --git a/code-review-graph-vscode/src/views/statusBar.ts b/code-review-graph-vscode/src/views/statusBar.ts new file mode 100644 index 0000000..41bcda4 --- /dev/null +++ b/code-review-graph-vscode/src/views/statusBar.ts @@ -0,0 +1,89 @@ +import * as vscode from 'vscode'; +import { SqliteReader } from '../backend/sqlite'; + +/** Number of milliseconds in one hour. */ +const ONE_HOUR_MS = 60 * 60 * 1000; + +/** + * Manages a status bar item that shows a summary of the code graph + * database and its staleness. + * + * Clicking the status bar item triggers `codeReviewGraph.updateGraph`. + */ +export class StatusBar implements vscode.Disposable { + private item: vscode.StatusBarItem; + + constructor() { + this.item = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 100, + ); + this.item.command = 'codeReviewGraph.updateGraph'; + } + + /** + * Update the status bar text, icon, and tooltip based on the current + * state of the graph database. + * + * @param reader The open SQLite reader, or `undefined` if no database + * is loaded. + */ + update(reader: SqliteReader | undefined): void { + if (!reader) { + this.item.text = '$(warning) Code Graph: Not built'; + this.item.tooltip = 'Click to build'; + return; + } + + const stats = reader.getStats(); + + const lastUpdated = stats.lastUpdated; + const isOutdated = this.isOlderThanOneHour(lastUpdated); + + if (isOutdated) { + this.item.text = '$(warning) Code Graph: Outdated'; + this.item.tooltip = + `Code Graph: ${stats.filesCount} files, ${stats.totalEdges} edges\n` + + `Last updated: ${lastUpdated || 'unknown'}`; + } else { + this.item.text = `$(database) ${stats.totalNodes} nodes`; + this.item.tooltip = + `Code Graph: ${stats.filesCount} files, ${stats.totalEdges} edges\n` + + `Last updated: ${lastUpdated || 'unknown'}`; + } + } + + /** Show the status bar item. */ + show(): void { + this.item.show(); + } + + /** Hide the status bar item. */ + hide(): void { + this.item.hide(); + } + + /** Dispose the status bar item. */ + dispose(): void { + this.item.dispose(); + } + + /** + * Determine whether `lastUpdated` is more than one hour in the past. + * + * Returns `true` if the timestamp is missing, unparseable, or older + * than one hour. + */ + private isOlderThanOneHour(lastUpdated: string | null): boolean { + if (!lastUpdated) { + return true; + } + + const updatedTime = new Date(lastUpdated).getTime(); + if (isNaN(updatedTime)) { + return true; + } + + return Date.now() - updatedTime > ONE_HOUR_MS; + } +} diff --git a/code-review-graph-vscode/src/views/treeItems.ts b/code-review-graph-vscode/src/views/treeItems.ts new file mode 100644 index 0000000..feaabc4 --- /dev/null +++ b/code-review-graph-vscode/src/views/treeItems.ts @@ -0,0 +1,234 @@ +import * as vscode from 'vscode'; +import * as path from 'path'; + +// --------------------------------------------------------------------------- +// FileTreeItem – represents a source file in the code graph +// --------------------------------------------------------------------------- + +export class FileTreeItem extends vscode.TreeItem { + public readonly filePath: string; + public readonly qualifiedName: string; + + constructor(filePath: string, workspaceRoot: string) { + const fileName = path.basename(filePath); + super(fileName, vscode.TreeItemCollapsibleState.Collapsed); + + this.filePath = filePath; + this.qualifiedName = filePath; + + const relativePath = path.relative(workspaceRoot, filePath); + this.description = relativePath !== fileName ? relativePath : ''; + this.iconPath = new vscode.ThemeIcon('file'); + this.contextValue = 'node-file'; + this.tooltip = filePath; + + this.command = { + title: 'Open File', + command: 'vscode.open', + arguments: [vscode.Uri.file(filePath)], + }; + } +} + +// --------------------------------------------------------------------------- +// SymbolTreeItem – represents a class, function, type, or test node +// --------------------------------------------------------------------------- + +const KIND_ICON_MAP: Record = { + Function: 'symbol-method', + Class: 'symbol-class', + Type: 'symbol-interface', + Test: 'testing-run-icon', +}; + +const KIND_CONTEXT_MAP: Record = { + Function: 'node-function', + Class: 'node-class', + Type: 'node-type', + Test: 'node-test', +}; + +function formatSymbolLabel(name: string, kind: string): string { + if (kind === 'Function' || kind === 'Test') { + return `${name}()`; + } + return name; +} + +function formatSymbolDescription(kind: string, lineStart: number | null, lineEnd: number | null): string { + const kindLower = kind.toLowerCase(); + if (lineStart != null && lineEnd != null) { + return `${kindLower} \u00b7 L${lineStart}\u2013${lineEnd}`; + } + if (lineStart != null) { + return `${kindLower} \u00b7 L${lineStart}`; + } + return kindLower; +} + +export class SymbolTreeItem extends vscode.TreeItem { + public readonly qualifiedName: string; + public readonly filePath: string; + public readonly lineStart: number | null; + public readonly kind: string; + + constructor( + qualifiedName: string, + name: string, + kind: string, + filePath: string, + lineStart: number | null, + lineEnd: number | null, + ) { + const label = formatSymbolLabel(name, kind); + super(label, vscode.TreeItemCollapsibleState.Collapsed); + + this.qualifiedName = qualifiedName; + this.filePath = filePath; + this.lineStart = lineStart; + this.kind = kind; + + this.description = formatSymbolDescription(kind, lineStart, lineEnd); + this.iconPath = new vscode.ThemeIcon(KIND_ICON_MAP[kind] ?? 'symbol-misc'); + this.contextValue = KIND_CONTEXT_MAP[kind] ?? 'node-function'; + this.tooltip = qualifiedName; + + const line = lineStart != null ? lineStart - 1 : 0; + this.command = { + title: 'Go to Symbol', + command: 'vscode.open', + arguments: [ + vscode.Uri.file(filePath), + { selection: new vscode.Range(line, 0, line, 0) } as vscode.TextDocumentShowOptions, + ], + }; + } +} + +// --------------------------------------------------------------------------- +// EdgeTreeItem – represents a relationship edge (leaf node) +// --------------------------------------------------------------------------- + +const OUTGOING_EDGE_LABELS: Record = { + CALLS: 'calls', + IMPORTS_FROM: 'imports', + INHERITS: 'inherits from', + IMPLEMENTS: 'implements', + TESTED_BY: 'tested by', + CONTAINS: 'contains', + DEPENDS_ON: 'depends on', +}; + +const INCOMING_EDGE_LABELS: Record = { + CALLS: 'called by', + IMPORTS_FROM: 'imported by', + INHERITS: 'inherited by', + IMPLEMENTS: 'implemented by', + TESTED_BY: 'tests', + CONTAINS: 'contained in', + DEPENDS_ON: 'depended on by', +}; + +const EDGE_ICON_MAP_OUTGOING: Record = { + CALLS: 'arrow-right', + IMPORTS_FROM: 'package', + INHERITS: 'type-hierarchy', + IMPLEMENTS: 'symbol-interface', + TESTED_BY: 'testing-run-icon', + CONTAINS: 'symbol-namespace', + DEPENDS_ON: 'references', +}; + +const EDGE_ICON_MAP_INCOMING: Record = { + CALLS: 'arrow-left', + IMPORTS_FROM: 'package', + INHERITS: 'type-hierarchy', + IMPLEMENTS: 'symbol-interface', + TESTED_BY: 'testing-run-icon', + CONTAINS: 'symbol-namespace', + DEPENDS_ON: 'references', +}; + +function extractShortName(qualifiedName: string): string { + // Qualified names are like "/path/to/file.py::ClassName.method" or "/path/to/file.py" + const colonIdx = qualifiedName.lastIndexOf('::'); + if (colonIdx >= 0) { + return qualifiedName.substring(colonIdx + 2); + } + return path.basename(qualifiedName); +} + +export class EdgeTreeItem extends vscode.TreeItem { + public readonly targetQualifiedName: string; + public readonly targetFilePath: string; + public readonly targetLine: number; + + constructor( + edgeKind: string, + direction: 'outgoing' | 'incoming', + targetQualifiedName: string, + targetFilePath: string, + targetLine: number, + ) { + const shortName = extractShortName(targetQualifiedName); + const verb = direction === 'outgoing' + ? (OUTGOING_EDGE_LABELS[edgeKind] ?? edgeKind.toLowerCase()) + : (INCOMING_EDGE_LABELS[edgeKind] ?? edgeKind.toLowerCase()); + const arrow = direction === 'outgoing' ? '\u2192' : '\u2190'; + const label = `${arrow} ${verb} ${shortName}`; + + super(label, vscode.TreeItemCollapsibleState.None); + + this.targetQualifiedName = targetQualifiedName; + this.targetFilePath = targetFilePath; + this.targetLine = targetLine; + + const iconMap = direction === 'outgoing' ? EDGE_ICON_MAP_OUTGOING : EDGE_ICON_MAP_INCOMING; + this.iconPath = new vscode.ThemeIcon(iconMap[edgeKind] ?? 'arrow-right'); + this.contextValue = 'edge'; + this.tooltip = `${arrow} ${verb} ${targetQualifiedName}`; + + const line = targetLine > 0 ? targetLine - 1 : 0; + this.command = { + title: 'Go to Target', + command: 'vscode.open', + arguments: [ + vscode.Uri.file(targetFilePath), + { selection: new vscode.Range(line, 0, line, 0) } as vscode.TextDocumentShowOptions, + ], + }; + } +} + +// --------------------------------------------------------------------------- +// BlastRadiusGroupItem – groups "Changed" and "Impacted" results +// --------------------------------------------------------------------------- + +export class BlastRadiusGroupItem extends vscode.TreeItem { + public readonly groupKind: 'changed' | 'impacted'; + + constructor(groupKind: 'changed' | 'impacted', count: number) { + const label = groupKind === 'changed' ? `Changed (${count})` : `Impacted (${count})`; + super(label, vscode.TreeItemCollapsibleState.Expanded); + + this.groupKind = groupKind; + this.iconPath = new vscode.ThemeIcon(groupKind === 'changed' ? 'flame' : 'broadcast'); + this.contextValue = `blast-radius-${groupKind}`; + this.tooltip = groupKind === 'changed' + ? `${count} directly changed node(s)` + : `${count} transitively impacted node(s)`; + } +} + +// --------------------------------------------------------------------------- +// StatsItem – displays a single statistic line (leaf node) +// --------------------------------------------------------------------------- + +export class StatsItem extends vscode.TreeItem { + constructor(label: string, value: string) { + super(label, vscode.TreeItemCollapsibleState.None); + this.description = value; + this.contextValue = 'stat'; + this.tooltip = `${label}: ${value}`; + } +} diff --git a/code-review-graph-vscode/src/views/treeView.ts b/code-review-graph-vscode/src/views/treeView.ts new file mode 100644 index 0000000..0ef5ab1 --- /dev/null +++ b/code-review-graph-vscode/src/views/treeView.ts @@ -0,0 +1,237 @@ +import * as vscode from 'vscode'; +import { SqliteReader, GraphNode, GraphEdge } from '../backend/sqlite'; +import { + FileTreeItem, + SymbolTreeItem, + EdgeTreeItem, + BlastRadiusGroupItem, + StatsItem, +} from './treeItems'; + +// --------------------------------------------------------------------------- +// CodeGraphTreeProvider -- main file > symbol > edge tree +// --------------------------------------------------------------------------- + +export class CodeGraphTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor( + private readonly reader: SqliteReader, + private readonly workspaceRoot: string, + ) {} + + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: vscode.TreeItem): vscode.ProviderResult { + if (!element) { + return this.getRootChildren(); + } + if (element instanceof FileTreeItem) { + return this.getFileChildren(element); + } + if (element instanceof SymbolTreeItem) { + return this.getSymbolChildren(element); + } + return []; + } + + // -- Root level: one FileTreeItem per file -------------------------------- + + private getRootChildren(): vscode.TreeItem[] { + const files = this.reader.getAllFiles(); + return files + .slice() + .sort((a, b) => a.localeCompare(b)) + .map((filePath) => new FileTreeItem(filePath, this.workspaceRoot)); + } + + // -- File level: symbols (non-File nodes) sorted by line ------------------ + + private getFileChildren(fileItem: FileTreeItem): vscode.TreeItem[] { + const nodes = this.reader.getNodesByFile(fileItem.filePath); + return nodes + .filter((n) => n.kind !== 'File') + .sort((a, b) => (a.lineStart ?? 0) - (b.lineStart ?? 0)) + .map( + (n) => + new SymbolTreeItem( + n.qualifiedName, + n.name, + n.kind, + n.filePath, + n.lineStart, + n.lineEnd, + ), + ); + } + + // -- Symbol level: outgoing + incoming edges (skip CONTAINS) -------------- + + private getSymbolChildren(symbolItem: SymbolTreeItem): vscode.TreeItem[] { + const items: vscode.TreeItem[] = []; + + // Outgoing edges + const outgoing = this.reader.getEdgesBySource(symbolItem.qualifiedName); + for (const edge of outgoing) { + if (edge.kind === 'CONTAINS') { + continue; + } + const targetNode = this.reader.getNode(edge.targetQualified); + const targetFile = targetNode?.filePath ?? edge.filePath; + const targetLine = targetNode?.lineStart ?? edge.line; + items.push( + new EdgeTreeItem( + edge.kind, + 'outgoing', + edge.targetQualified, + targetFile, + targetLine, + ), + ); + } + + // Incoming edges + const incoming = this.reader.getEdgesByTarget(symbolItem.qualifiedName); + for (const edge of incoming) { + if (edge.kind === 'CONTAINS') { + continue; + } + const sourceNode = this.reader.getNode(edge.sourceQualified); + const sourceFile = sourceNode?.filePath ?? edge.filePath; + const sourceLine = sourceNode?.lineStart ?? edge.line; + items.push( + new EdgeTreeItem( + edge.kind, + 'incoming', + edge.sourceQualified, + sourceFile, + sourceLine, + ), + ); + } + + return items; + } +} + +// --------------------------------------------------------------------------- +// BlastRadiusTreeProvider -- shows changed + impacted nodes +// --------------------------------------------------------------------------- + +export class BlastRadiusTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + private changedNodes: GraphNode[] = []; + private impactedNodes: GraphNode[] = []; + + setResults(changed: GraphNode[], impacted: GraphNode[]): void { + this.changedNodes = changed; + this.impactedNodes = impacted; + this._onDidChangeTreeData.fire(undefined); + } + + clear(): void { + this.changedNodes = []; + this.impactedNodes = []; + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + return element; + } + + getChildren(element?: vscode.TreeItem): vscode.ProviderResult { + if (!element) { + return this.getRootChildren(); + } + if (element instanceof BlastRadiusGroupItem) { + return this.getGroupChildren(element); + } + return []; + } + + private getRootChildren(): vscode.TreeItem[] { + if (this.changedNodes.length === 0 && this.impactedNodes.length === 0) { + return []; + } + const groups: vscode.TreeItem[] = []; + if (this.changedNodes.length > 0) { + groups.push(new BlastRadiusGroupItem('changed', this.changedNodes.length)); + } + if (this.impactedNodes.length > 0) { + groups.push(new BlastRadiusGroupItem('impacted', this.impactedNodes.length)); + } + return groups; + } + + private getGroupChildren(group: BlastRadiusGroupItem): vscode.TreeItem[] { + const nodes = group.groupKind === 'changed' ? this.changedNodes : this.impactedNodes; + return nodes.map( + (n) => + new SymbolTreeItem( + n.qualifiedName, + n.name, + n.kind, + n.filePath, + n.lineStart, + n.lineEnd, + ), + ); + } +} + +// --------------------------------------------------------------------------- +// StatsTreeProvider -- graph statistics overview +// --------------------------------------------------------------------------- + +export class StatsTreeProvider implements vscode.TreeDataProvider { + private readonly _onDidChangeTreeData = new vscode.EventEmitter(); + readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; + + constructor(private readonly reader: SqliteReader) {} + + refresh(): void { + this._onDidChangeTreeData.fire(undefined); + } + + getTreeItem(element: vscode.TreeItem): vscode.TreeItem { + return element; + } + + getChildren(): vscode.ProviderResult { + const stats = this.reader.getStats(); + const items: StatsItem[] = []; + + items.push(new StatsItem('Files', stats.filesCount.toLocaleString())); + items.push(new StatsItem('Total Nodes', stats.totalNodes.toLocaleString())); + items.push(new StatsItem('Total Edges', stats.totalEdges.toLocaleString())); + items.push( + new StatsItem( + 'Languages', + stats.languages.length > 0 ? stats.languages.join(', ') : 'none', + ), + ); + items.push( + new StatsItem( + 'Last Updated', + stats.lastUpdated ?? 'unknown', + ), + ); + items.push( + new StatsItem( + 'Embeddings', + stats.embeddingsCount > 0 ? stats.embeddingsCount.toLocaleString() : 'none', + ), + ); + + return items; + } +} diff --git a/code-review-graph-vscode/src/webview/graph.ts b/code-review-graph-vscode/src/webview/graph.ts new file mode 100644 index 0000000..6d214b0 --- /dev/null +++ b/code-review-graph-vscode/src/webview/graph.ts @@ -0,0 +1,990 @@ +/** + * Webview entry point for the D3.js force-directed graph visualization. + * Runs in the browser context inside the VS Code webview panel. + * + * Communicates with the extension host via postMessage / addEventListener. + * NO Node.js APIs are available here. + */ + +import * as d3 from "d3"; + +declare function acquireVsCodeApi(): { + postMessage(msg: unknown): void; + getState(): unknown; + setState(state: unknown): void; +}; + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +type NodeKind = "File" | "Class" | "Function" | "Test" | "Type"; + +type EdgeKind = + | "CALLS" + | "IMPORTS_FROM" + | "INHERITS" + | "IMPLEMENTS" + | "TESTED_BY" + | "CONTAINS" + | "DEPENDS_ON"; + +interface GraphNode { + id: number; + kind: NodeKind; + name: string; + qualifiedName: string; + filePath: string; + lineStart: number | null; + lineEnd: number | null; + language: string | null; + parentName: string | null; + params: string | null; + returnType: string | null; + modifiers: string | null; + isTest: boolean; + fileHash: string | null; +} + +interface GraphEdge { + id: number; + kind: EdgeKind; + sourceQualified: string; + targetQualified: string; + filePath: string; + line: number; +} + +/** D3 simulation node extends GraphNode with x/y/vx/vy. */ +interface SimNode extends d3.SimulationNodeDatum, GraphNode {} + +/** D3 simulation link with resolved source/target. */ +interface SimLink extends d3.SimulationLinkDatum { + kind: EdgeKind; + sourceQualified: string; + targetQualified: string; +} + +// --------------------------------------------------------------------------- +// Constants +// --------------------------------------------------------------------------- + +const NODE_RADIUS: Record = { + File: 18, + Class: 12, + Function: 6, + Test: 6, + Type: 5, +}; + +const NODE_COLOR: Record = { + File: "#58a6ff", + Class: "#f0883e", + Function: "#3fb950", + Test: "#d2a8ff", + Type: "#8b949e", +}; + +const NODE_SHAPE: Record = { + File: d3.symbolCircle, + Class: d3.symbolSquare, + Function: d3.symbolTriangle, + Test: d3.symbolDiamond, + Type: d3.symbolCross, +}; + +const NODE_AREA: Record = { + File: 616, + Class: 452, + Function: 314, + Test: 314, + Type: 314, +}; + +const EDGE_COLOR: Record = { + CALLS: "#3fb950", + IMPORTS_FROM: "#f0883e", + INHERITS: "#d2a8ff", + IMPLEMENTS: "#f9e2af", + TESTED_BY: "#f38ba8", + CONTAINS: "rgba(139,148,158,0.15)", + DEPENDS_ON: "#fab387", +}; + +const ALL_EDGE_KINDS: EdgeKind[] = [ + "CALLS", + "IMPORTS_FROM", + "INHERITS", + "IMPLEMENTS", + "TESTED_BY", + "CONTAINS", + "DEPENDS_ON", +]; + +// --------------------------------------------------------------------------- +// Global state +// --------------------------------------------------------------------------- + +const vscodeApi = acquireVsCodeApi(); + +let allNodes: SimNode[] = []; +let allEdges: SimLink[] = []; +let nodeMap = new Map(); + +let visibleEdgeKinds = new Set(ALL_EDGE_KINDS); +let selectedNode: SimNode | null = null; +let depthLimit = 0; // 0 = show all + +let simulation: d3.Simulation | null = null; +let svg: d3.Selection; +let container: d3.Selection; +let linkGroup: d3.Selection; +let nodeGroup: d3.Selection; +let labelGroup: d3.Selection; +let zoomBehavior: d3.ZoomBehavior; + +let linkSelection: d3.Selection; +let nodeSelection: d3.Selection; +let labelSelection: d3.Selection; + +let currentTheme: "dark" | "light" = "dark"; + +// --------------------------------------------------------------------------- +// Init +// --------------------------------------------------------------------------- + +function init(): void { + createSvg(); + bindToolbarEvents(); + bindExtensionMessages(); + + vscodeApi.postMessage({ command: "ready" }); +} + +// --------------------------------------------------------------------------- +// SVG setup +// --------------------------------------------------------------------------- + +function createSvg(): void { + const graphEl = document.getElementById("graph-area")!; + const width = graphEl.clientWidth || window.innerWidth; + const height = graphEl.clientHeight || window.innerHeight; + + svg = d3 + .select(graphEl) + .append("svg") + .attr("width", "100%") + .attr("height", "100%") + .attr("viewBox", `0 0 ${width} ${height}`); + + // Arrow marker definitions -- one per edge kind + const defs = svg.append("defs"); + for (const kind of ALL_EDGE_KINDS) { + defs + .append("marker") + .attr("id", `arrow-${kind}`) + .attr("viewBox", "0 -5 10 10") + .attr("refX", 20) + .attr("refY", 0) + .attr("markerWidth", 6) + .attr("markerHeight", 6) + .attr("orient", "auto") + .append("path") + .attr("d", "M0,-5L10,0L0,5") + .attr("fill", EDGE_COLOR[kind]); + } + + container = svg.append("g").attr("class", "graph-container"); + linkGroup = container.append("g").attr("class", "links"); + nodeGroup = container.append("g").attr("class", "nodes"); + labelGroup = container.append("g").attr("class", "labels"); + + // Initialize empty selections + linkSelection = linkGroup.selectAll("line"); + nodeSelection = nodeGroup.selectAll("path.node-shape"); + labelSelection = labelGroup.selectAll("text"); + + // Zoom + pan + zoomBehavior = d3 + .zoom() + .scaleExtent([0.05, 8]) + .on("zoom", (event: d3.D3ZoomEvent) => { + container.attr("transform", event.transform.toString()); + }); + + svg.call(zoomBehavior); + + // Resize handler + const resizeObserver = new ResizeObserver(() => { + const w = graphEl.clientWidth; + const h = graphEl.clientHeight; + svg.attr("viewBox", `0 0 ${w} ${h}`); + }); + resizeObserver.observe(graphEl); +} + +// --------------------------------------------------------------------------- +// Data ingestion +// --------------------------------------------------------------------------- + +function setData(nodes: GraphNode[], edges: GraphEdge[]): void { + // Build SimNodes + allNodes = nodes.map((n) => ({ ...n } as SimNode)); + nodeMap = new Map(allNodes.map((n) => [n.qualifiedName, n])); + + // Build SimLinks, filtering to edges where both endpoints exist + allEdges = []; + for (const e of edges) { + const src = nodeMap.get(e.sourceQualified); + const tgt = nodeMap.get(e.targetQualified); + if (src && tgt) { + allEdges.push({ + source: src, + target: tgt, + kind: e.kind, + sourceQualified: e.sourceQualified, + targetQualified: e.targetQualified, + }); + } + } + + // Reset depth filter + depthLimit = 0; + const slider = document.getElementById("depth-slider") as HTMLInputElement | null; + if (slider) { + slider.value = "0"; + } + const depthValue = document.getElementById("depth-value"); + if (depthValue) { + depthValue.textContent = "All"; + } + + // Show/hide empty state + const emptyState = document.getElementById("empty-state"); + const graphArea = document.getElementById("graph-area"); + if (nodes.length === 0) { + if (emptyState) emptyState.style.display = "block"; + if (graphArea) { + // Hide the SVG but keep the container + const svgHide = graphArea.querySelector("svg"); + if (svgHide) svgHide.style.display = "none"; + } + updateDepthSliderState(); + return; + } + if (emptyState) emptyState.style.display = "none"; + const svgEl = graphArea?.querySelector("svg"); + if (svgEl) svgEl.style.display = ""; + + buildGraph(); + + updateDepthSliderState(); +} + +// --------------------------------------------------------------------------- +// Graph construction +// --------------------------------------------------------------------------- + +function getVisibleData(): { nodes: SimNode[]; links: SimLink[] } { + // Filter edges by visible kinds + let links = allEdges.filter((e) => visibleEdgeKinds.has(e.kind)); + + let nodes: SimNode[]; + + if (selectedNode && depthLimit > 0) { + // BFS from selected node up to depthLimit + const reachable = new Set(); + reachable.add(selectedNode.qualifiedName); + let frontier = new Set([selectedNode.qualifiedName]); + + for (let d = 0; d < depthLimit; d++) { + const next = new Set(); + for (const qn of frontier) { + for (const link of links) { + const srcQn = + typeof link.source === "object" + ? (link.source as SimNode).qualifiedName + : link.sourceQualified; + const tgtQn = + typeof link.target === "object" + ? (link.target as SimNode).qualifiedName + : link.targetQualified; + + if (srcQn === qn && !reachable.has(tgtQn)) { + reachable.add(tgtQn); + next.add(tgtQn); + } + if (tgtQn === qn && !reachable.has(srcQn)) { + reachable.add(srcQn); + next.add(srcQn); + } + } + } + frontier = next; + if (frontier.size === 0) break; + } + + nodes = allNodes.filter((n) => reachable.has(n.qualifiedName)); + const reachableSet = reachable; + links = links.filter((l) => { + const srcQn = + typeof l.source === "object" + ? (l.source as SimNode).qualifiedName + : l.sourceQualified; + const tgtQn = + typeof l.target === "object" + ? (l.target as SimNode).qualifiedName + : l.targetQualified; + return reachableSet.has(srcQn) && reachableSet.has(tgtQn); + }); + } else { + nodes = [...allNodes]; + } + + // Apply search filter + const searchInput = document.getElementById("search-input") as HTMLInputElement | null; + const query = searchInput?.value?.trim().toLowerCase() ?? ""; + if (query.length > 0) { + const matchingQns = new Set( + nodes + .filter((n) => n.name.toLowerCase().includes(query) || n.qualifiedName.toLowerCase().includes(query)) + .map((n) => n.qualifiedName) + ); + // Keep matching nodes + their direct neighbors + const expanded = new Set(matchingQns); + for (const link of links) { + const srcQn = + typeof link.source === "object" + ? (link.source as SimNode).qualifiedName + : link.sourceQualified; + const tgtQn = + typeof link.target === "object" + ? (link.target as SimNode).qualifiedName + : link.targetQualified; + if (matchingQns.has(srcQn)) expanded.add(tgtQn); + if (matchingQns.has(tgtQn)) expanded.add(srcQn); + } + nodes = nodes.filter((n) => expanded.has(n.qualifiedName)); + links = links.filter((l) => { + const srcQn = + typeof l.source === "object" + ? (l.source as SimNode).qualifiedName + : l.sourceQualified; + const tgtQn = + typeof l.target === "object" + ? (l.target as SimNode).qualifiedName + : l.targetQualified; + return expanded.has(srcQn) && expanded.has(tgtQn); + }); + } + + return { nodes, links }; +} + +function buildGraph(): void { + const { nodes, links } = getVisibleData(); + + // Stop existing simulation + if (simulation) { + simulation.stop(); + } + + const graphEl = document.getElementById("graph-area")!; + const width = graphEl.clientWidth || window.innerWidth; + const height = graphEl.clientHeight || window.innerHeight; + + // --- Links --- + linkSelection = linkGroup + .selectAll("line") + .data(links, (d) => `${d.sourceQualified}-${d.targetQualified}-${d.kind}`) + .join("line") + .attr("stroke", (d) => EDGE_COLOR[d.kind]) + .attr("stroke-width", 1.5) + .attr("stroke-opacity", 0.4) + .attr("marker-end", (d) => `url(#arrow-${d.kind})`); + + // --- Nodes --- + nodeSelection = nodeGroup + .selectAll("path.node-shape") + .data(nodes, (d) => d.qualifiedName) + .join("path") + .attr("class", "node-shape") + .attr("d", (d) => d3.symbol().type(NODE_SHAPE[d.kind] ?? d3.symbolCircle).size(NODE_AREA[d.kind] ?? 314)()!) + .attr("fill", (d) => NODE_COLOR[d.kind] ?? "#cdd6f4") + .attr("stroke", "none") + .attr("stroke-width", 2) + .attr("cursor", "pointer") + .on("click", (_event, d) => { + selectNode(d); + vscodeApi.postMessage({ + command: "nodeClicked", + qualifiedName: d.qualifiedName, + filePath: d.filePath, + lineStart: d.lineStart ?? 1, + }); + }) + .on("dblclick", (_event, d) => { + // Center on node and expand depth by 1 + selectNode(d); + depthLimit = Math.min(depthLimit + 1, 10); + const slider = document.getElementById("depth-slider") as HTMLInputElement | null; + if (slider) slider.value = String(depthLimit); + const depthValue = document.getElementById("depth-value"); + if (depthValue) depthValue.textContent = String(depthLimit); + buildGraph(); + centerOnNode(d); + }) + .on("mouseenter", (_event, d) => { + showTooltip(d); + highlightConnected(d); + }) + .on("mouseleave", () => { + hideTooltip(); + unhighlightAll(); + }) + .call( + d3 + .drag() + .on("start", (event, d) => { + if (!event.active) simulation?.alphaTarget(0.3).restart(); + d.fx = d.x; + d.fy = d.y; + }) + .on("drag", (event, d) => { + d.fx = event.x; + d.fy = event.y; + }) + .on("end", (event, d) => { + if (!event.active) simulation?.alphaTarget(0); + d.fx = null; + d.fy = null; + }) + ) + .attr("tabindex", 0) + .attr("role", "button") + .attr("aria-label", (d) => `${d.kind}: ${d.name}`) + .on("keydown", (event: KeyboardEvent, d: SimNode) => { + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + selectNode(d); + vscodeApi.postMessage({ + command: "nodeClicked", + qualifiedName: d.qualifiedName, + filePath: d.filePath, + lineStart: d.lineStart ?? 1, + }); + } else if (event.key === "Escape") { + event.preventDefault(); + selectedNode = null; + unhighlightAll(); + nodeSelection.attr("stroke", "none"); + } else if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(event.key)) { + event.preventDefault(); + const visibleNodes = nodeSelection.data(); + let best: SimNode | null = null; + let bestDist = Infinity; + for (const n of visibleNodes) { + if (n.qualifiedName === d.qualifiedName || n.x == null || n.y == null || d.x == null || d.y == null) continue; + const dx = n.x - d.x; + const dy = n.y - d.y; + const dist = Math.sqrt(dx * dx + dy * dy); + let ok = false; + if (event.key === "ArrowRight" && dx > 0 && Math.abs(dy) < Math.abs(dx)) ok = true; + if (event.key === "ArrowLeft" && dx < 0 && Math.abs(dy) < Math.abs(dx)) ok = true; + if (event.key === "ArrowDown" && dy > 0 && Math.abs(dx) < Math.abs(dy)) ok = true; + if (event.key === "ArrowUp" && dy < 0 && Math.abs(dx) < Math.abs(dy)) ok = true; + if (ok && dist < bestDist) { + best = n; + bestDist = dist; + } + } + if (best) { + const target = nodeGroup.selectAll("path.node-shape") + .filter((n) => n.qualifiedName === best!.qualifiedName) + .node(); + if (target) (target as HTMLElement).focus(); + } + } + }) + .on("focus", (_event: FocusEvent, d: SimNode) => { + showTooltip(d); + highlightConnected(d); + }) + .on("blur", () => { + hideTooltip(); + unhighlightAll(); + }); + + // Highlight search matches + const searchInput = document.getElementById("search-input") as HTMLInputElement | null; + const query = searchInput?.value?.trim().toLowerCase() ?? ""; + if (query.length > 0) { + nodeSelection.attr("stroke", (d) => { + const matches = + d.name.toLowerCase().includes(query) || + d.qualifiedName.toLowerCase().includes(query); + return matches ? "#e6edf3" : "none"; + }); + } + + // Highlight selected node + if (selectedNode) { + nodeSelection.attr("stroke", (d) => { + if (d.qualifiedName === selectedNode!.qualifiedName) return "#e6edf3"; + if (query.length > 0) { + const matches = + d.name.toLowerCase().includes(query) || + d.qualifiedName.toLowerCase().includes(query); + return matches ? "#e6edf3" : "none"; + } + return "none"; + }); + } + + // --- Labels --- + labelSelection = labelGroup + .selectAll("text") + .data(nodes, (d) => d.qualifiedName) + .join("text") + .text((d) => d.name) + .attr("font-size", 10) + .attr("fill", currentTheme === "dark" ? "#cdd6f4" : "#4c4f69") + .attr("text-anchor", "middle") + .attr("dy", (d) => (NODE_RADIUS[d.kind] ?? 10) + 14) + .attr("pointer-events", "none"); + + // --- Force simulation --- + simulation = d3 + .forceSimulation(nodes) + .alphaDecay(0.02) + .force( + "link", + d3 + .forceLink(links) + .id((d) => d.qualifiedName) + .distance(100) + ) + .force("charge", d3.forceManyBody().strength(-200)) + .force("center", d3.forceCenter(width / 2, height / 2)) + .force( + "collide", + d3.forceCollide().radius((d) => (NODE_RADIUS[d.kind] ?? 10) + 5) + ) + .on("tick", () => { + linkSelection + .attr("x1", (d) => (d.source as SimNode).x!) + .attr("y1", (d) => (d.source as SimNode).y!) + .attr("x2", (d) => (d.target as SimNode).x!) + .attr("y2", (d) => (d.target as SimNode).y!); + + nodeSelection.attr("transform", (d) => `translate(${d.x},${d.y})`); + + labelSelection.attr("x", (d) => d.x!).attr("y", (d) => d.y!); + }); + + // Update node count display + const countEl = document.getElementById("node-count"); + if (countEl) { + countEl.textContent = `${nodes.length} nodes, ${links.length} edges`; + } +} + +// --------------------------------------------------------------------------- +// Selection & highlight +// --------------------------------------------------------------------------- + +function selectNode(node: SimNode): void { + selectedNode = node; + nodeSelection.attr("stroke", (d) => + d.qualifiedName === node.qualifiedName ? "#e6edf3" : "none" + ); + updateDepthSliderState(); +} + +function updateDepthSliderState(): void { + const slider = document.getElementById("depth-slider") as HTMLInputElement | null; + const depthValue = document.getElementById("depth-value"); + if (slider) { + if (selectedNode) { + slider.disabled = false; + } else { + slider.disabled = true; + if (depthValue) depthValue.textContent = "N/A"; + } + } +} + +function highlightConnected(node: SimNode): void { + const connectedQns = new Set(); + connectedQns.add(node.qualifiedName); + + linkSelection.attr("stroke-opacity", (d) => { + const srcQn = (d.source as SimNode).qualifiedName; + const tgtQn = (d.target as SimNode).qualifiedName; + if (srcQn === node.qualifiedName || tgtQn === node.qualifiedName) { + connectedQns.add(srcQn); + connectedQns.add(tgtQn); + return 0.8; + } + return 0.1; + }); + + nodeSelection.attr("opacity", (d) => + connectedQns.has(d.qualifiedName) ? 1 : 0.2 + ); + labelSelection.attr("opacity", (d) => + connectedQns.has(d.qualifiedName) ? 1 : 0.2 + ); +} + +function unhighlightAll(): void { + linkSelection.attr("stroke-opacity", 0.4); + nodeSelection.attr("opacity", 1); + labelSelection.attr("opacity", 1); +} + +// --------------------------------------------------------------------------- +// Tooltip +// --------------------------------------------------------------------------- + +function showTooltip(node: SimNode): void { + const tooltip = document.getElementById("tooltip")!; + tooltip.style.display = "block"; + + let html = `${escapeHtml(node.name)}
`; + html += `${escapeHtml(node.kind)}
`; + html += `${escapeHtml(node.filePath)}`; + if (node.lineStart != null) { + html += `
Lines ${node.lineStart}`; + if (node.lineEnd != null && node.lineEnd !== node.lineStart) { + html += `-${node.lineEnd}`; + } + } + if (node.params) { + html += `
${escapeHtml(node.params)}`; + } + if (node.returnType) { + html += ` → ${escapeHtml(node.returnType)}`; + } + + tooltip.innerHTML = html; + + // Position near cursor -- we'll update on mousemove too + document.addEventListener("mousemove", positionTooltip); +} + +function positionTooltip(event: MouseEvent): void { + const tooltip = document.getElementById("tooltip")!; + const x = event.clientX + 12; + const y = event.clientY + 12; + + // Keep tooltip in viewport + const rect = tooltip.getBoundingClientRect(); + const maxX = window.innerWidth - rect.width - 8; + const maxY = window.innerHeight - rect.height - 8; + + tooltip.style.left = `${Math.min(x, maxX)}px`; + tooltip.style.top = `${Math.min(y, maxY)}px`; +} + +function hideTooltip(): void { + const tooltip = document.getElementById("tooltip")!; + tooltip.style.display = "none"; + document.removeEventListener("mousemove", positionTooltip); +} + +function escapeHtml(text: string): string { + const div = document.createElement("div"); + div.textContent = text; + return div.innerHTML; +} + +// --------------------------------------------------------------------------- +// Highlight node (from extension message) +// --------------------------------------------------------------------------- + +function highlightNodeByName(qualifiedName: string): void { + const node = nodeMap.get(qualifiedName); + if (!node) return; + + selectNode(node); + centerOnNode(node); + + // Add pulsing ring animation + const ring = nodeGroup + .append("circle") + .attr("cx", node.x ?? 0) + .attr("cy", node.y ?? 0) + .attr("r", (NODE_RADIUS[node.kind] ?? 10) + 4) + .attr("fill", "none") + .attr("stroke", "#e6edf3") + .attr("stroke-width", 3) + .attr("class", "pulse-ring"); + + // Remove after animation completes + ring + .transition() + .duration(600) + .attr("r", (NODE_RADIUS[node.kind] ?? 10) + 20) + .attr("stroke-opacity", 0) + .on("end", function () { + d3.select(this).remove(); + }); + + // Second pulse + setTimeout(() => { + if (!node.x) return; + const ring2 = nodeGroup + .append("circle") + .attr("cx", node.x) + .attr("cy", node.y ?? 0) + .attr("r", (NODE_RADIUS[node.kind] ?? 10) + 4) + .attr("fill", "none") + .attr("stroke", "#e6edf3") + .attr("stroke-width", 3); + + ring2 + .transition() + .duration(600) + .attr("r", (NODE_RADIUS[node.kind] ?? 10) + 20) + .attr("stroke-opacity", 0) + .on("end", function () { + d3.select(this).remove(); + }); + }, 300); +} + +// --------------------------------------------------------------------------- +// Camera +// --------------------------------------------------------------------------- + +function centerOnNode(node: SimNode): void { + if (!node.x || !node.y) return; + + const graphEl = document.getElementById("graph-area")!; + const width = graphEl.clientWidth; + const height = graphEl.clientHeight; + + const transform = d3.zoomIdentity + .translate(width / 2, height / 2) + .scale(1.5) + .translate(-node.x, -node.y); + + svg + .transition() + .duration(500) + .call(zoomBehavior.transform, transform); +} + +function fitToView(): void { + const graphEl = document.getElementById("graph-area")!; + const width = graphEl.clientWidth; + const height = graphEl.clientHeight; + + if (allNodes.length === 0) return; + + // Find bounding box of visible nodes + const visibleNodes = nodeSelection.data(); + if (visibleNodes.length === 0) return; + + let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity; + for (const n of visibleNodes) { + if (n.x == null || n.y == null) continue; + const r = NODE_RADIUS[n.kind] ?? 10; + minX = Math.min(minX, n.x - r); + maxX = Math.max(maxX, n.x + r); + minY = Math.min(minY, n.y - r); + maxY = Math.max(maxY, n.y + r); + } + + if (!isFinite(minX)) return; + + const padding = 60; + const bboxWidth = maxX - minX + padding * 2; + const bboxHeight = maxY - minY + padding * 2; + const scale = Math.min(width / bboxWidth, height / bboxHeight, 2); + const cx = (minX + maxX) / 2; + const cy = (minY + maxY) / 2; + + const transform = d3.zoomIdentity + .translate(width / 2, height / 2) + .scale(scale) + .translate(-cx, -cy); + + svg + .transition() + .duration(500) + .call(zoomBehavior.transform, transform); +} + +// --------------------------------------------------------------------------- +// Toolbar events +// --------------------------------------------------------------------------- + +function bindToolbarEvents(): void { + // Search + const searchInput = document.getElementById("search-input") as HTMLInputElement | null; + if (searchInput) { + let debounceTimer: ReturnType; + searchInput.addEventListener("input", () => { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => { + buildGraph(); + }, 250); + }); + } + + // Edge toggle pills + for (const kind of ALL_EDGE_KINDS) { + const pill = document.getElementById(`edge-${kind}`); + if (pill) { + const toggle = () => { + if (visibleEdgeKinds.has(kind)) { + visibleEdgeKinds.delete(kind); + pill.classList.remove("active"); + pill.setAttribute("aria-pressed", "false"); + } else { + visibleEdgeKinds.add(kind); + pill.classList.add("active"); + pill.setAttribute("aria-pressed", "true"); + } + buildGraph(); + }; + pill.addEventListener("click", toggle); + pill.addEventListener("keydown", (ev) => { + if (ev.key === "Enter" || ev.key === " ") { ev.preventDefault(); toggle(); } + }); + } + } + + // Edge filter popover toggle + const edgeFilterBtn = document.getElementById("btn-edge-filter"); + const edgePopover = document.getElementById("edge-popover"); + if (edgeFilterBtn && edgePopover) { + edgeFilterBtn.addEventListener("click", (e) => { + e.stopPropagation(); + edgePopover.classList.toggle("visible"); + }); + document.addEventListener("click", (e) => { + if (!edgePopover.contains(e.target as Node) && e.target !== edgeFilterBtn) { + edgePopover.classList.remove("visible"); + } + }); + } + + // Depth slider + const depthSlider = document.getElementById("depth-slider") as HTMLInputElement | null; + if (depthSlider) { + depthSlider.addEventListener("input", () => { + depthLimit = parseInt(depthSlider.value, 10); + const depthValue = document.getElementById("depth-value"); + if (depthValue) { + depthValue.textContent = depthLimit === 0 ? "All" : String(depthLimit); + } + buildGraph(); + }); + } + + // Fit button + const fitBtn = document.getElementById("btn-fit"); + if (fitBtn) { + fitBtn.addEventListener("click", () => { + fitToView(); + }); + } + + // Export SVG button + const exportBtn = document.getElementById("btn-export"); + if (exportBtn) { + exportBtn.addEventListener("click", () => { + const svgEl = document.querySelector("#graph-area svg"); + if (svgEl) { + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(svgEl); + vscodeApi.postMessage({ + command: "exportSvg", + svg: svgString, + }); + } + }); + } + + // Export PNG button + const exportPngBtn = document.getElementById("btn-export-png"); + if (exportPngBtn) { + exportPngBtn.addEventListener("click", () => { + const svgEl = document.querySelector("#graph-area svg") as SVGSVGElement | null; + if (!svgEl) { return; } + + const serializer = new XMLSerializer(); + const svgString = serializer.serializeToString(svgEl); + const canvas = document.createElement("canvas"); + const bbox = svgEl.getBoundingClientRect(); + canvas.width = bbox.width * 2; // 2x for retina + canvas.height = bbox.height * 2; + const ctx = canvas.getContext("2d"); + if (!ctx) { return; } + ctx.scale(2, 2); + + const img = new Image(); + img.onload = () => { + ctx.drawImage(img, 0, 0); + const pngData = canvas.toDataURL("image/png"); + vscodeApi.postMessage({ command: "exportPng", data: pngData }); + }; + img.src = "data:image/svg+xml;base64," + btoa(unescape(encodeURIComponent(svgString))); + }); + } +} + +// --------------------------------------------------------------------------- +// Extension message handling +// --------------------------------------------------------------------------- + +function bindExtensionMessages(): void { + window.addEventListener("message", (event) => { + const message = event.data; + switch (message.command) { + case "setData": + setData( + message.nodes as GraphNode[], + message.edges as GraphEdge[] + ); + // Auto-fit after simulation settles a bit + setTimeout(() => fitToView(), 800); + // Show truncation warning if needed + if (message.truncated) { + const warn = document.getElementById("truncation-warning"); + if (warn) { + warn.style.display = "inline"; + warn.textContent = `\u26a0 Showing ${message.maxNodes} of more nodes. Increase maxNodes in settings.`; + } + } + break; + + case "highlightNode": + highlightNodeByName(message.qualifiedName as string); + break; + + case "setTheme": + currentTheme = message.theme as "dark" | "light"; + applyTheme(); + break; + } + }); +} + +// --------------------------------------------------------------------------- +// Theme +// --------------------------------------------------------------------------- + +function applyTheme(): void { + const textColor = currentTheme === "dark" ? "#cdd6f4" : "#4c4f69"; + labelSelection.attr("fill", textColor); +} + +// --------------------------------------------------------------------------- +// Start +// --------------------------------------------------------------------------- + +init(); diff --git a/code-review-graph-vscode/test/sqlite.test.ts b/code-review-graph-vscode/test/sqlite.test.ts new file mode 100644 index 0000000..f892358 --- /dev/null +++ b/code-review-graph-vscode/test/sqlite.test.ts @@ -0,0 +1,499 @@ +/** + * Tests for the SqliteReader module. + * + * Creates a temporary SQLite database with the exact schema used by the + * Python backend, inserts representative test data, and validates every + * public method of SqliteReader. + */ + +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import Database from 'better-sqlite3'; +import { SqliteReader, GraphNode, GraphEdge } from '../src/backend/sqlite'; + +// --------------------------------------------------------------------------- +// Schema (mirrors the Python backend exactly) +// --------------------------------------------------------------------------- + +const SCHEMA_SQL = ` +CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind TEXT NOT NULL, + name TEXT NOT NULL, + qualified_name TEXT NOT NULL UNIQUE, + file_path TEXT NOT NULL, + line_start INTEGER, + line_end INTEGER, + language TEXT, + parent_name TEXT, + params TEXT, + return_type TEXT, + modifiers TEXT, + is_test INTEGER DEFAULT 0, + file_hash TEXT, + extra TEXT DEFAULT '{}', + updated_at REAL NOT NULL +); + +CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind TEXT NOT NULL, + source_qualified TEXT NOT NULL, + target_qualified TEXT NOT NULL, + file_path TEXT NOT NULL, + line INTEGER DEFAULT 0, + extra TEXT DEFAULT '{}', + updated_at REAL NOT NULL +); + +CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_path); +CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind); +CREATE INDEX IF NOT EXISTS idx_nodes_qualified ON nodes(qualified_name); +CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_qualified); +CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_qualified); +CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind); +CREATE INDEX IF NOT EXISTS idx_edges_file ON edges(file_path); +`; + +// --------------------------------------------------------------------------- +// Test data +// --------------------------------------------------------------------------- + +const NOW = Date.now() / 1000; + +interface TestNode { + kind: string; + name: string; + qualified_name: string; + file_path: string; + line_start: number; + line_end: number; + language: string; + parent_name: string | null; + params: string | null; + return_type: string | null; + modifiers: string | null; + is_test: number; + file_hash: string; + extra: string; + updated_at: number; +} + +interface TestEdge { + kind: string; + source_qualified: string; + target_qualified: string; + file_path: string; + line: number; + extra: string; + updated_at: number; +} + +const TEST_NODES: TestNode[] = [ + // auth.py -- File node + 2 functions + { + kind: 'File', name: 'auth.py', qualified_name: 'src/auth.py', + file_path: 'src/auth.py', line_start: 1, line_end: 50, + language: 'python', parent_name: null, params: null, return_type: null, + modifiers: null, is_test: 0, file_hash: 'aaa', extra: '{}', updated_at: NOW, + }, + { + kind: 'Function', name: 'login', qualified_name: 'src/auth.py::login', + file_path: 'src/auth.py', line_start: 5, line_end: 20, + language: 'python', parent_name: null, params: '(username, password)', + return_type: 'bool', modifiers: null, is_test: 0, file_hash: 'aaa', + extra: '{}', updated_at: NOW, + }, + { + kind: 'Function', name: 'logout', qualified_name: 'src/auth.py::logout', + file_path: 'src/auth.py', line_start: 22, line_end: 35, + language: 'python', parent_name: null, params: '(session)', + return_type: 'None', modifiers: null, is_test: 0, file_hash: 'aaa', + extra: '{}', updated_at: NOW, + }, + + // routes.py -- File node + 1 function + { + kind: 'File', name: 'routes.py', qualified_name: 'src/routes.py', + file_path: 'src/routes.py', line_start: 1, line_end: 40, + language: 'python', parent_name: null, params: null, return_type: null, + modifiers: null, is_test: 0, file_hash: 'bbb', extra: '{}', updated_at: NOW, + }, + { + kind: 'Function', name: 'handle_login', qualified_name: 'src/routes.py::handle_login', + file_path: 'src/routes.py', line_start: 10, line_end: 30, + language: 'python', parent_name: null, params: '(request)', + return_type: 'Response', modifiers: null, is_test: 0, file_hash: 'bbb', + extra: '{}', updated_at: NOW, + }, + + // test_auth.py -- File node + 1 test function + { + kind: 'File', name: 'test_auth.py', qualified_name: 'tests/test_auth.py', + file_path: 'tests/test_auth.py', line_start: 1, line_end: 30, + language: 'python', parent_name: null, params: null, return_type: null, + modifiers: null, is_test: 0, file_hash: 'ccc', extra: '{}', updated_at: NOW, + }, + { + kind: 'Test', name: 'test_login', qualified_name: 'tests/test_auth.py::test_login', + file_path: 'tests/test_auth.py', line_start: 5, line_end: 25, + language: 'python', parent_name: null, params: '()', + return_type: 'None', modifiers: null, is_test: 1, file_hash: 'ccc', + extra: '{}', updated_at: NOW, + }, +]; + +const TEST_EDGES: TestEdge[] = [ + // routes.py::handle_login CALLS auth.py::login + { + kind: 'CALLS', source_qualified: 'src/routes.py::handle_login', + target_qualified: 'src/auth.py::login', file_path: 'src/routes.py', + line: 15, extra: '{}', updated_at: NOW, + }, + // routes.py IMPORTS_FROM auth.py + { + kind: 'IMPORTS_FROM', source_qualified: 'src/routes.py', + target_qualified: 'src/auth.py', file_path: 'src/routes.py', + line: 1, extra: '{}', updated_at: NOW, + }, + // auth.py CONTAINS login + { + kind: 'CONTAINS', source_qualified: 'src/auth.py', + target_qualified: 'src/auth.py::login', file_path: 'src/auth.py', + line: 5, extra: '{}', updated_at: NOW, + }, + // auth.py CONTAINS logout + { + kind: 'CONTAINS', source_qualified: 'src/auth.py', + target_qualified: 'src/auth.py::logout', file_path: 'src/auth.py', + line: 22, extra: '{}', updated_at: NOW, + }, + // test_auth.py::test_login TESTED_BY (reverse: login is tested by test_login) + { + kind: 'TESTED_BY', source_qualified: 'src/auth.py::login', + target_qualified: 'tests/test_auth.py::test_login', file_path: 'tests/test_auth.py', + line: 5, extra: '{}', updated_at: NOW, + }, +]; + +// --------------------------------------------------------------------------- +// Helper: create a populated temp database +// --------------------------------------------------------------------------- + +function createTestDb(): string { + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'crg-test-')); + const dbPath = path.join(tmpDir, 'graph.db'); + + const db = new Database(dbPath); + db.exec(SCHEMA_SQL); + + const insertNode = db.prepare(` + INSERT INTO nodes + (kind, name, qualified_name, file_path, line_start, line_end, + language, parent_name, params, return_type, modifiers, is_test, + file_hash, extra, updated_at) + VALUES + (@kind, @name, @qualified_name, @file_path, @line_start, @line_end, + @language, @parent_name, @params, @return_type, @modifiers, @is_test, + @file_hash, @extra, @updated_at) + `); + + const insertEdge = db.prepare(` + INSERT INTO edges + (kind, source_qualified, target_qualified, file_path, line, extra, updated_at) + VALUES + (@kind, @source_qualified, @target_qualified, @file_path, @line, @extra, @updated_at) + `); + + const insertMeta = db.prepare( + 'INSERT INTO metadata (key, value) VALUES (?, ?)' + ); + + const insertMany = db.transaction(() => { + for (const n of TEST_NODES) { insertNode.run(n); } + for (const e of TEST_EDGES) { insertEdge.run(e); } + insertMeta.run('last_updated', '2025-06-15T10:30:00Z'); + }); + insertMany(); + db.close(); + + return dbPath; +} + +function cleanup(dbPath: string): void { + try { + const dir = path.dirname(dbPath); + fs.rmSync(dir, { recursive: true, force: true }); + } catch { + // best effort + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +describe('SqliteReader', () => { + let dbPath: string; + let reader: SqliteReader; + + before(() => { + dbPath = createTestDb(); + reader = new SqliteReader(dbPath); + }); + + after(() => { + reader.close(); + cleanup(dbPath); + }); + + // -- isValid ------------------------------------------------------------ + + it('isValid() returns true for a properly initialised database', () => { + assert.strictEqual(reader.isValid(), true); + }); + + // -- getAllFiles --------------------------------------------------------- + + it('getAllFiles() returns file paths ordered alphabetically', () => { + const files = reader.getAllFiles(); + assert.deepStrictEqual(files, [ + 'src/auth.py', + 'src/routes.py', + 'tests/test_auth.py', + ]); + }); + + // -- getNodesByFile ----------------------------------------------------- + + it('getNodesByFile() returns nodes ordered by line_start', () => { + const nodes = reader.getNodesByFile('src/auth.py'); + assert.strictEqual(nodes.length, 3); // File + login + logout + + // Verify ordering + assert.strictEqual(nodes[0].name, 'auth.py'); + assert.strictEqual(nodes[1].name, 'login'); + assert.strictEqual(nodes[2].name, 'logout'); + + // Verify camelCase conversion + assert.strictEqual(nodes[1].qualifiedName, 'src/auth.py::login'); + assert.strictEqual(nodes[1].lineStart, 5); + assert.strictEqual(nodes[1].lineEnd, 20); + assert.strictEqual(nodes[1].returnType, 'bool'); + assert.strictEqual(nodes[1].isTest, false); + }); + + // -- getNode ------------------------------------------------------------ + + it('getNode() returns a single node by qualified name', () => { + const node = reader.getNode('src/auth.py::login'); + assert.ok(node); + assert.strictEqual(node.kind, 'Function'); + assert.strictEqual(node.name, 'login'); + assert.strictEqual(node.params, '(username, password)'); + }); + + it('getNode() returns undefined for non-existent qualified name', () => { + const node = reader.getNode('src/auth.py::nonexistent'); + assert.strictEqual(node, undefined); + }); + + // -- getNodeAtCursor ---------------------------------------------------- + + it('getNodeAtCursor() returns the innermost node at the cursor', () => { + // Line 10 is inside login (5-20) and inside auth.py (1-50). + // login has the smaller span so it should be returned. + const node = reader.getNodeAtCursor('src/auth.py', 10); + assert.ok(node); + assert.strictEqual(node.name, 'login'); + }); + + it('getNodeAtCursor() returns the File node when cursor is outside functions', () => { + // Line 45 is inside auth.py (1-50) but outside both functions. + const node = reader.getNodeAtCursor('src/auth.py', 45); + assert.ok(node); + assert.strictEqual(node.kind, 'File'); + assert.strictEqual(node.name, 'auth.py'); + }); + + it('getNodeAtCursor() returns undefined when no node covers the line', () => { + const node = reader.getNodeAtCursor('src/auth.py', 999); + assert.strictEqual(node, undefined); + }); + + // -- getEdgesBySource --------------------------------------------------- + + it('getEdgesBySource() returns outgoing edges', () => { + const edges = reader.getEdgesBySource('src/routes.py::handle_login'); + assert.strictEqual(edges.length, 1); + assert.strictEqual(edges[0].kind, 'CALLS'); + assert.strictEqual(edges[0].targetQualified, 'src/auth.py::login'); + assert.strictEqual(edges[0].line, 15); + }); + + // -- getEdgesByTarget --------------------------------------------------- + + it('getEdgesByTarget() returns incoming edges', () => { + const edges = reader.getEdgesByTarget('src/auth.py::login'); + // CALLS from handle_login + CONTAINS from auth.py + assert.strictEqual(edges.length, 2); + const kinds = edges.map((e) => e.kind).sort(); + assert.deepStrictEqual(kinds, ['CALLS', 'CONTAINS']); + }); + + // -- getEdgesAmong ------------------------------------------------------ + + it('getEdgesAmong() returns only edges within the given set', () => { + const qns = new Set([ + 'src/routes.py::handle_login', + 'src/auth.py::login', + 'src/auth.py', + ]); + const edges = reader.getEdgesAmong(qns); + // Should include: CALLS handle_login->login, CONTAINS auth.py->login, + // IMPORTS_FROM routes.py->auth.py only if routes.py is in set (it's not). + assert.ok(edges.length >= 2); + for (const e of edges) { + assert.ok(qns.has(e.sourceQualified), `source ${e.sourceQualified} should be in set`); + assert.ok(qns.has(e.targetQualified), `target ${e.targetQualified} should be in set`); + } + }); + + it('getEdgesAmong() returns empty array for empty set', () => { + const edges = reader.getEdgesAmong(new Set()); + assert.deepStrictEqual(edges, []); + }); + + // -- searchNodes -------------------------------------------------------- + + it('searchNodes() finds nodes by name substring', () => { + const results = reader.searchNodes('login'); + assert.ok(results.length >= 2); // login, handle_login, test_login + const names = results.map((n) => n.name); + assert.ok(names.includes('login')); + assert.ok(names.includes('handle_login')); + }); + + it('searchNodes() respects limit', () => { + const results = reader.searchNodes('login', 1); + assert.strictEqual(results.length, 1); + }); + + it('searchNodes() returns empty for no match', () => { + const results = reader.searchNodes('zzz_no_match_zzz'); + assert.deepStrictEqual(results, []); + }); + + // -- getStats ----------------------------------------------------------- + + it('getStats() returns correct aggregate counts', () => { + const stats = reader.getStats(); + assert.strictEqual(stats.totalNodes, TEST_NODES.length); + assert.strictEqual(stats.totalEdges, TEST_EDGES.length); + assert.strictEqual(stats.filesCount, 3); // 3 File nodes + assert.deepStrictEqual(stats.languages, ['python']); + assert.strictEqual(stats.lastUpdated, '2025-06-15T10:30:00Z'); + assert.strictEqual(stats.embeddingsCount, 0); // no embeddings table data + + // Nodes by kind + assert.strictEqual(stats.nodesByKind['File'], 3); + assert.strictEqual(stats.nodesByKind['Function'], 3); + assert.strictEqual(stats.nodesByKind['Test'], 1); + + // Edges by kind + assert.strictEqual(stats.edgesByKind['CALLS'], 1); + assert.strictEqual(stats.edgesByKind['IMPORTS_FROM'], 1); + assert.strictEqual(stats.edgesByKind['CONTAINS'], 2); + assert.strictEqual(stats.edgesByKind['TESTED_BY'], 1); + }); + + // -- getMetadata -------------------------------------------------------- + + it('getMetadata() returns stored value', () => { + const val = reader.getMetadata('last_updated'); + assert.strictEqual(val, '2025-06-15T10:30:00Z'); + }); + + it('getMetadata() returns undefined for missing key', () => { + const val = reader.getMetadata('nonexistent_key'); + assert.strictEqual(val, undefined); + }); + + // -- getImpactRadius ---------------------------------------------------- + + it('getImpactRadius() finds changed and impacted nodes', () => { + const result = reader.getImpactRadius(['src/auth.py'], 2); + + // Changed nodes: everything in auth.py (File + login + logout) + assert.strictEqual(result.changedNodes.length, 3); + const changedNames = result.changedNodes.map((n) => n.name).sort(); + assert.deepStrictEqual(changedNames, ['auth.py', 'login', 'logout']); + + // Impacted nodes: handle_login (calls login), test_login (tested_by), + // routes.py (imports_from auth.py), test_auth.py file (contains test_login) + assert.ok( + result.impactedNodes.length > 0, + 'should have impacted nodes' + ); + const impactedNames = result.impactedNodes.map((n) => n.name); + assert.ok( + impactedNames.includes('handle_login'), + 'handle_login should be impacted (calls login)' + ); + assert.ok( + impactedNames.includes('test_login'), + 'test_login should be impacted (tested_by)' + ); + + // Impacted files should include routes.py and/or tests/test_auth.py + assert.ok( + result.impactedFiles.length > 0, + 'should have impacted files' + ); + + // Edges among all involved nodes + assert.ok( + result.edges.length > 0, + 'should have connecting edges' + ); + }); + + it('getImpactRadius() with depth 0 returns only seeds, no impacted nodes', () => { + const result = reader.getImpactRadius(['src/auth.py'], 0); + assert.strictEqual(result.changedNodes.length, 3); + assert.strictEqual(result.impactedNodes.length, 0); + }); + + it('getImpactRadius() for non-existent file returns empty', () => { + const result = reader.getImpactRadius(['nonexistent.py']); + assert.strictEqual(result.changedNodes.length, 0); + assert.strictEqual(result.impactedNodes.length, 0); + assert.strictEqual(result.impactedFiles.length, 0); + }); + + // -- close / isValid after close ---------------------------------------- + + it('isValid() returns false after close()', () => { + const tmpPath = createTestDb(); + const tmpReader = new SqliteReader(tmpPath); + assert.strictEqual(tmpReader.isValid(), true); + tmpReader.close(); + assert.strictEqual(tmpReader.isValid(), false); + cleanup(tmpPath); + }); + + // -- constructor retry on bad path -------------------------------------- + + it('constructor throws after retries for a non-existent path', () => { + assert.throws(() => { + new SqliteReader('/nonexistent/path/to/database.db'); + }); + }); +}); diff --git a/code-review-graph-vscode/tsconfig.json b/code-review-graph-vscode/tsconfig.json new file mode 100644 index 0000000..f9ff681 --- /dev/null +++ b/code-review-graph-vscode/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "lib": ["ES2022"], + "types": ["node"] + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist", "test", "src/webview"] +} diff --git a/code_review_graph/__init__.py b/code_review_graph/__init__.py new file mode 100644 index 0000000..304f003 --- /dev/null +++ b/code_review_graph/__init__.py @@ -0,0 +1,20 @@ +"""Code Review Graph - MCP server for persistent incremental code knowledge graphs.""" + +from .context_savings import ( + attach_context_savings, + estimate_context_savings, + estimate_file_tokens, + estimate_tokens, + format_context_savings, +) + +__version__ = "2.3.6" + +__all__ = [ + "__version__", + "attach_context_savings", + "estimate_context_savings", + "estimate_file_tokens", + "estimate_tokens", + "format_context_savings", +] diff --git a/code_review_graph/__main__.py b/code_review_graph/__main__.py new file mode 100644 index 0000000..479b6c3 --- /dev/null +++ b/code_review_graph/__main__.py @@ -0,0 +1,4 @@ +"""Allow running as: python -m code_review_graph""" +from .cli import main + +main() diff --git a/code_review_graph/analysis.py b/code_review_graph/analysis.py new file mode 100644 index 0000000..1668a1a --- /dev/null +++ b/code_review_graph/analysis.py @@ -0,0 +1,410 @@ +"""Graph analysis: hub detection, bridge nodes, knowledge gaps, +surprise scoring, suggested questions.""" + +from __future__ import annotations + +import logging +from collections import Counter, defaultdict + +from .graph import GraphStore, _sanitize_name + +logger = logging.getLogger(__name__) + + +def find_hub_nodes(store: GraphStore, top_n: int = 10) -> list[dict]: + """Find the most connected nodes (highest in+out degree), excluding File nodes. + + Returns list of dicts with: name, qualified_name, kind, file, + in_degree, out_degree, total_degree, community_id + """ + # Build degree counts from all edges + edges = store.get_all_edges() + in_degree: dict[str, int] = Counter() + out_degree: dict[str, int] = Counter() + for e in edges: + out_degree[e.source_qualified] += 1 + in_degree[e.target_qualified] += 1 + + # Get all non-File nodes + nodes = store.get_all_nodes(exclude_files=True) + community_map = store.get_all_community_ids() + + scored = [] + for n in nodes: + qn = n.qualified_name + ind = in_degree.get(qn, 0) + outd = out_degree.get(qn, 0) + total = ind + outd + if total == 0: + continue + scored.append({ + "name": _sanitize_name(n.name), + "qualified_name": n.qualified_name, + "kind": n.kind, + "file": n.file_path, + "in_degree": ind, + "out_degree": outd, + "total_degree": total, + "community_id": community_map.get(qn), + }) + + scored.sort( + key=lambda x: x.get("total_degree", 0), # type: ignore[arg-type,return-value] + reverse=True, + ) + return scored[:top_n] + + +def find_bridge_nodes( + store: GraphStore, top_n: int = 10 +) -> list[dict]: + """Find nodes with highest betweenness centrality. + + These are architectural chokepoints that sit on shortest paths + between many node pairs. If they break, multiple communities + lose connectivity. + + Returns list of dicts with: name, qualified_name, kind, file, + betweenness, community_id + """ + import networkx as nx + + # Build the graph — use cached version if available + nxg = store._build_networkx_graph() + + # Compute betweenness centrality (approximate for large graphs) + n_nodes = nxg.number_of_nodes() + if n_nodes > 5000: + # Sample-based approximation for large graphs + k = min(500, n_nodes) + bc = nx.betweenness_centrality(nxg, k=k, normalized=True) + elif n_nodes > 0: + bc = nx.betweenness_centrality(nxg, normalized=True) + else: + return [] + + community_map = store.get_all_community_ids() + node_map = { + n.qualified_name: n + for n in store.get_all_nodes(exclude_files=True) + } + + results = [] + for qn, score in bc.items(): + if score <= 0 or qn not in node_map: + continue + n = node_map[qn] + if n.kind == "File": + continue + results.append({ + "name": _sanitize_name(n.name), + "qualified_name": n.qualified_name, + "kind": n.kind, + "file": n.file_path, + "betweenness": round(score, 6), + "community_id": community_map.get(qn), + }) + + results.sort( + key=lambda x: float(x.get("betweenness", 0)), # type: ignore[arg-type,return-value] + reverse=True, + ) + return results[:top_n] + + +def find_knowledge_gaps(store: GraphStore) -> dict[str, list[dict]]: + """Identify structural weaknesses in the codebase graph. + + Returns dict with categories: + - isolated_nodes: degree <= 1, disconnected from graph + - thin_communities: fewer than 3 members + - untested_hotspots: high-degree nodes with no TESTED_BY edges + - single_file_communities: entire community in one file + """ + edges = store.get_all_edges() + nodes = store.get_all_nodes(exclude_files=True) + community_map = store.get_all_community_ids() + + # Build degree map + degree: dict[str, int] = Counter() + tested_nodes: set[str] = set() + for e in edges: + degree[e.source_qualified] += 1 + degree[e.target_qualified] += 1 + if e.kind == "TESTED_BY": + tested_nodes.add(e.source_qualified) + + # 1. Isolated nodes (degree <= 1, not File) + isolated = [] + for n in nodes: + d = degree.get(n.qualified_name, 0) + if d <= 1: + isolated.append({ + "name": _sanitize_name(n.name), + "qualified_name": n.qualified_name, + "kind": n.kind, + "file": n.file_path, + "degree": d, + }) + + # 2. Build community sizes and file maps from node data + comm_sizes: Counter[int] = Counter() + comm_files: dict[int, set[str]] = defaultdict(set) + for n in nodes: + cid = community_map.get(n.qualified_name) + if cid is not None: + comm_sizes[cid] += 1 + comm_files[cid].add(n.file_path) + + # Thin communities (< 3 members) + communities = store.get_communities_list() + thin = [] + for c in communities: + cid = int(c["id"]) + size = comm_sizes.get(cid, 0) + if size < 3: + thin.append({ + "community_id": cid, + "name": str(c["name"]), + "size": size, + }) + + # 3. Untested hotspots (degree >= 5, no TESTED_BY) + untested_hotspots = [] + for n in nodes: + d = degree.get(n.qualified_name, 0) + if (d >= 5 + and n.qualified_name not in tested_nodes + and not n.is_test): + untested_hotspots.append({ + "name": _sanitize_name(n.name), + "qualified_name": n.qualified_name, + "kind": n.kind, + "file": n.file_path, + "degree": d, + }) + untested_hotspots.sort( + key=lambda x: x.get("degree", 0), # type: ignore[arg-type,return-value] + reverse=True, + ) + + # 4. Single-file communities + single_file = [] + for c in communities: + cid = int(c["id"]) + files = comm_files.get(cid, set()) + size = comm_sizes.get(cid, 0) + if len(files) == 1 and size >= 3: + single_file.append({ + "community_id": cid, + "name": str(c["name"]), + "size": size, + "file": next(iter(files)), + }) + + return { + "isolated_nodes": isolated[:50], + "thin_communities": thin, + "untested_hotspots": untested_hotspots[:20], + "single_file_communities": single_file, + } + + +def find_surprising_connections( + store: GraphStore, top_n: int = 15 +) -> list[dict]: + """Find edges with high surprise scores. + + Detects unexpected architectural coupling based on: + - Cross-community: source and target in different communities + - Cross-language: different file languages + - Peripheral-to-hub: low-degree node to high-degree node + - Cross-file-type: test calling production or vice versa + - Non-standard edge kind for the node types + """ + edges = store.get_all_edges() + nodes = store.get_all_nodes(exclude_files=True) + community_map = store.get_all_community_ids() + + node_map = {n.qualified_name: n for n in nodes} + + # Build degree map + degree: dict[str, int] = Counter() + for e in edges: + degree[e.source_qualified] += 1 + degree[e.target_qualified] += 1 + + # Median degree for peripheral detection + degrees = [d for d in degree.values() if d > 0] + if not degrees: + return [] + median_deg = sorted(degrees)[len(degrees) // 2] + high_deg_threshold = max(median_deg * 3, 10) + + scored_edges = [] + for e in edges: + src = node_map.get(e.source_qualified) + tgt = node_map.get(e.target_qualified) + if not src or not tgt: + continue + if src.kind == "File" or tgt.kind == "File": + continue + + score = 0.0 + reasons = [] + + # Cross-community (+0.3) + src_cid = community_map.get(e.source_qualified) + tgt_cid = community_map.get(e.target_qualified) + if (src_cid is not None + and tgt_cid is not None + and src_cid != tgt_cid): + score += 0.3 + reasons.append("cross-community") + + # Cross-language (+0.2) + src_lang = ( + src.file_path.rsplit(".", 1)[-1] + if "." in src.file_path else "" + ) + tgt_lang = ( + tgt.file_path.rsplit(".", 1)[-1] + if "." in tgt.file_path else "" + ) + if src_lang and tgt_lang and src_lang != tgt_lang: + score += 0.2 + reasons.append("cross-language") + + # Peripheral-to-hub (+0.2) + src_deg = degree.get(e.source_qualified, 0) + tgt_deg = degree.get(e.target_qualified, 0) + if ((src_deg <= 2 and tgt_deg >= high_deg_threshold) + or (tgt_deg <= 2 + and src_deg >= high_deg_threshold)): + score += 0.2 + reasons.append("peripheral-to-hub") + + # Cross-file-type: test <-> non-test (+0.15) + if src.is_test != tgt.is_test and e.kind == "CALLS": + score += 0.15 + reasons.append("cross-test-boundary") + + # Non-standard edge kind (+0.15) + if e.kind == "CALLS" and src.kind == "Type": + score += 0.15 + reasons.append("unusual-edge-kind") + + if score > 0: + scored_edges.append({ + "source": _sanitize_name(src.name), + "source_qualified": e.source_qualified, + "target": _sanitize_name(tgt.name), + "target_qualified": e.target_qualified, + "edge_kind": e.kind, + "surprise_score": round(score, 2), + "reasons": reasons, + "source_community": src_cid, + "target_community": tgt_cid, + }) + + scored_edges.sort( + key=lambda x: float(x.get("surprise_score", 0)), # type: ignore[arg-type,return-value] + reverse=True, + ) + return scored_edges[:top_n] + + +def generate_suggested_questions( + store: GraphStore, +) -> list[dict]: + """Auto-generate review questions from graph analysis. + + Categories: + - bridge_node: Why does X connect communities A and B? + - isolated_node: Is X dead code or dynamically invoked? + - low_cohesion: Should community X be split? + - hub_risk: Does hub node X have adequate test coverage? + - surprising: Why does A call B across community boundary? + """ + questions = [] + + # Bridge node questions + bridges = find_bridge_nodes(store, top_n=3) + for b in bridges: + questions.append({ + "category": "bridge_node", + "question": ( + f"'{b['name']}' is a critical connector " + f"between multiple code regions. Is it " + f"adequately tested and documented?" + ), + "target": b["qualified_name"], + "priority": "high", + }) + + # Hub risk questions + hubs = find_hub_nodes(store, top_n=3) + edges = store.get_all_edges() + tested = { + e.source_qualified + for e in edges if e.kind == "TESTED_BY" + } + for h in hubs: + if h["qualified_name"] not in tested: + questions.append({ + "category": "hub_risk", + "question": ( + f"Hub node '{h['name']}' has " + f"{h['total_degree']} connections but no " + f"direct test coverage. Should it be " + f"tested?" + ), + "target": h["qualified_name"], + "priority": "high", + }) + + # Surprising connection questions + surprises = find_surprising_connections(store, top_n=3) + for s in surprises: + if "cross-community" in s["reasons"]: + questions.append({ + "category": "surprising_connection", + "question": ( + f"'{s['source']}' (community " + f"{s['source_community']}) calls " + f"'{s['target']}' (community " + f"{s['target_community']}). Is this " + f"coupling intentional?" + ), + "target": s["source_qualified"], + "priority": "medium", + }) + + # Knowledge gap questions + gaps = find_knowledge_gaps(store) + + for c in gaps["thin_communities"][:2]: + questions.append({ + "category": "thin_community", + "question": ( + f"Community '{c['name']}' has only " + f"{c['size']} member(s). Should it be " + f"merged with a neighbor?" + ), + "target": f"community:{c['community_id']}", + "priority": "low", + }) + + for h in gaps["untested_hotspots"][:2]: + questions.append({ + "category": "untested_hotspot", + "question": ( + f"'{h['name']}' has {h['degree']} " + f"connections but no test coverage. " + f"Is this a risk?" + ), + "target": h["qualified_name"], + "priority": "medium", + }) + + return questions diff --git a/code_review_graph/changes.py b/code_review_graph/changes.py new file mode 100644 index 0000000..77dc25a --- /dev/null +++ b/code_review_graph/changes.py @@ -0,0 +1,409 @@ +"""Change impact analysis for code review. + +Maps git/svn diffs to affected functions, flows, communities, and test coverage +gaps. Produces risk-scored, priority-ordered review guidance. +""" + +from __future__ import annotations + +import logging +import os +import re +import subprocess +from pathlib import Path +from typing import Any + +from .constants import SECURITY_KEYWORDS as _SECURITY_KEYWORDS +from .flows import get_affected_flows +from .graph import GraphNode, GraphStore, _sanitize_name, node_to_dict + +logger = logging.getLogger(__name__) + +_GIT_TIMEOUT = int(os.environ.get("CRG_GIT_TIMEOUT", "30")) # seconds, configurable + +_SAFE_GIT_REF = re.compile(r"^[A-Za-z0-9_.~^/@{}\-]+$") +_SAFE_SVN_REV = re.compile(r"^r?\d+(:r?\d+|:HEAD|:BASE|:COMMITTED)?$", re.IGNORECASE) + + +# --------------------------------------------------------------------------- +# 1. parse_git_diff_ranges / parse_svn_diff_ranges +# --------------------------------------------------------------------------- + + +def parse_git_diff_ranges( + repo_root: str, + base: str = "HEAD~1", +) -> dict[str, list[tuple[int, int]]]: + """Run ``git diff --unified=0`` and extract changed line ranges per file. + + Args: + repo_root: Absolute path to the repository root. + base: Git ref to diff against (default: ``HEAD~1``). + + Returns: + Mapping of file paths to lists of ``(start_line, end_line)`` tuples. + Returns an empty dict on error. + """ + if not _SAFE_GIT_REF.match(base): + logger.warning("Invalid git ref rejected: %s", base) + return {} + try: + result = subprocess.run( + ["git", "diff", "--unified=0", base, "--"], + capture_output=True, + stdin=subprocess.DEVNULL, + text=True, + encoding="utf-8", + errors="replace", + cwd=repo_root, + timeout=_GIT_TIMEOUT, + ) + if result.returncode != 0: + logger.warning("git diff failed (rc=%d): %s", result.returncode, result.stderr[:200]) + return {} + except (OSError, subprocess.SubprocessError) as exc: + logger.warning("git diff error: %s", exc) + return {} + + return _parse_unified_diff(result.stdout) + + +def parse_svn_diff_ranges( + repo_root: str, + rev_range: str | None = None, +) -> dict[str, list[tuple[int, int]]]: + """Run ``svn diff`` and extract changed line ranges per file. + + Args: + repo_root: Absolute path to the SVN working copy root. + rev_range: Optional SVN revision range in ``rXXX:HEAD`` format. + When *None*, diffs the working copy against BASE (local changes). + + Returns: + Mapping of file paths to lists of ``(start_line, end_line)`` tuples. + Returns an empty dict on error. + """ + cmd = ["svn", "diff", "--non-interactive"] + if rev_range: + if not _SAFE_SVN_REV.match(rev_range): + logger.warning("Invalid SVN revision range rejected: %s", rev_range) + return {} + cmd.extend(["-r", rev_range]) + try: + result = subprocess.run( + cmd, + capture_output=True, + stdin=subprocess.DEVNULL, + text=True, + encoding="utf-8", + errors="replace", + cwd=repo_root, + timeout=_GIT_TIMEOUT, + ) + if result.returncode != 0: + logger.warning("svn diff failed (rc=%d): %s", result.returncode, result.stderr[:200]) + return {} + except (OSError, subprocess.SubprocessError) as exc: + logger.warning("svn diff error: %s", exc) + return {} + + return _parse_unified_diff(result.stdout) + + +def parse_diff_ranges( + repo_root: str, + base: str = "HEAD~1", +) -> dict[str, list[tuple[int, int]]]: + """Auto-detect VCS and return changed line ranges per file. + + Dispatches to :func:`parse_git_diff_ranges` for Git repositories and + :func:`parse_svn_diff_ranges` for SVN working copies. + + Args: + repo_root: Absolute path to the repository/working-copy root. + base: For Git: the ref to diff against (default ``HEAD~1``). + For SVN: an optional revision range (e.g. ``"r100:HEAD"``); + when *base* is not a valid SVN revision, working-copy changes + (``svn diff``) are used instead. + """ + root_path = Path(repo_root) + if (root_path / ".svn").exists(): + rev_range = base if _SAFE_SVN_REV.match(base) else None + return parse_svn_diff_ranges(repo_root, rev_range) + return parse_git_diff_ranges(repo_root, base) + + +def _parse_unified_diff(diff_text: str) -> dict[str, list[tuple[int, int]]]: + """Parse unified diff output into file -> line-range mappings. + + Handles the ``@@ -old,count +new,count @@`` hunk header format. + """ + ranges: dict[str, list[tuple[int, int]]] = {} + current_file: str | None = None + + # Match "+++ b/path/to/file" + file_pattern = re.compile(r"^\+\+\+ b/(.+)$") + # Match "@@ ... +start,count @@" or "@@ ... +start @@" + hunk_pattern = re.compile(r"^@@ .+? \+(\d+)(?:,(\d+))? @@") + + for line in diff_text.splitlines(): + file_match = file_pattern.match(line) + if file_match: + current_file = file_match.group(1) + continue + + hunk_match = hunk_pattern.match(line) + if hunk_match and current_file is not None: + start = int(hunk_match.group(1)) + count = int(hunk_match.group(2)) if hunk_match.group(2) else 1 + if count == 0: + # Pure deletion hunk (no lines added); still note the position. + end = start + else: + end = start + count - 1 + ranges.setdefault(current_file, []).append((start, end)) + + return ranges + + +# --------------------------------------------------------------------------- +# 2. map_changes_to_nodes +# --------------------------------------------------------------------------- + + +def map_changes_to_nodes( + store: GraphStore, + changed_ranges: dict[str, list[tuple[int, int]]], +) -> list[GraphNode]: + """Find graph nodes whose line ranges overlap the changed lines. + + Args: + store: The graph store. + changed_ranges: Mapping of file paths to ``(start, end)`` tuples. + + Returns: + Deduplicated list of overlapping graph nodes. + """ + seen: set[str] = set() + result: list[GraphNode] = [] + + for file_path, ranges in changed_ranges.items(): + # Try the path as-is, then also try all nodes to match relative paths. + nodes = store.get_nodes_by_file(file_path) + if not nodes: + # The graph may store absolute paths; try a suffix match. + matched_paths = store.get_files_matching(file_path) + for mp in matched_paths: + nodes.extend(store.get_nodes_by_file(mp)) + + for node in nodes: + if node.qualified_name in seen: + continue + if node.line_start is None or node.line_end is None: + continue + # Check overlap with any changed range. + for start, end in ranges: + if node.line_start <= end and node.line_end >= start: + result.append(node) + seen.add(node.qualified_name) + break + + return result + + +# --------------------------------------------------------------------------- +# 3. compute_risk_score +# --------------------------------------------------------------------------- + + +def compute_risk_score(store: GraphStore, node: GraphNode) -> float: + """Compute a risk score (0.0 - 1.0) for a single node. + + Scoring factors: + - Flow participation: 0.05 per flow membership, capped at 0.25 + - Community crossing: 0.05 per caller from a different community, capped at 0.15 + - Test coverage: 0.30 (untested) scaling down to 0.05 (5+ TESTED_BY edges) + - Security sensitivity: 0.20 if name matches security keywords + - Caller count: callers / 20, capped at 0.10 + """ + score = 0.0 + + # --- Flow participation (cap 0.25), weighted by criticality --- + flow_criticalities = store.get_flow_criticalities_for_node(node.id) + if flow_criticalities: + score += min(sum(flow_criticalities), 0.25) + else: + flow_count = store.count_flow_memberships(node.id) + score += min(flow_count * 0.05, 0.25) + + # --- Community crossing (cap 0.15) --- + callers = store.get_edges_by_target(node.qualified_name) + caller_edges = [e for e in callers if e.kind == "CALLS"] + + cross_community = 0 + node_cid = store.get_node_community_id(node.id) + + if node_cid is not None and caller_edges: + caller_qns = [edge.source_qualified for edge in caller_edges] + cid_map = store.get_community_ids_by_qualified_names(caller_qns) + for cid in cid_map.values(): + if cid is not None and cid != node_cid: + cross_community += 1 + score += min(cross_community * 0.05, 0.15) + + # --- Test coverage (direct + transitive) --- + transitive_tests = store.get_transitive_tests(node.qualified_name) + test_count = len(transitive_tests) + score += 0.30 - (min(test_count / 5.0, 1.0) * 0.25) + + # --- Security sensitivity --- + name_lower = node.name.lower() + qn_lower = node.qualified_name.lower() + if any(kw in name_lower or kw in qn_lower for kw in _SECURITY_KEYWORDS): + score += 0.20 + + # --- Caller count (cap 0.10) --- + caller_count = len(caller_edges) + score += min(caller_count / 20.0, 0.10) + + return round(min(max(score, 0.0), 1.0), 4) + + +# --------------------------------------------------------------------------- +# 4. analyze_changes +# --------------------------------------------------------------------------- + + +def analyze_changes( + store: GraphStore, + changed_files: list[str], + changed_ranges: dict[str, list[tuple[int, int]]] | None = None, + repo_root: str | None = None, + base: str = "HEAD~1", +) -> dict[str, Any]: + """Analyze changes and produce risk-scored review guidance. + + Args: + store: The graph store. + changed_files: List of changed file paths. + changed_ranges: Optional pre-parsed diff ranges. If not provided and + ``repo_root`` is given, they are computed via the detected VCS + (Git or SVN). + repo_root: Repository root (for git/svn diff). + base: Git ref or SVN revision range to diff against. + + Returns: + Dict with ``summary``, ``risk_score``, ``changed_functions``, + ``affected_flows``, ``test_gaps``, and ``review_priorities``. + """ + # Compute changed ranges if not provided. + if changed_ranges is None and repo_root is not None: + # Diff keys are forward-slash paths relative to the repo root, but + # the graph stores absolute native paths. Remap so lookups work on + # Windows, where the LIKE-suffix fallback cannot bridge + # "src/app.py" to "C:\repo\src\app.py" (#528). Keys that are + # already absolute pass through pathlib joining unchanged. The + # explicit changed_ranges path (MCP) is untouched — tools/review.py + # remaps before calling, and remapping twice would corrupt keys. + root_path = Path(repo_root) + changed_ranges = { + str(root_path / key): ranges + for key, ranges in parse_diff_ranges(repo_root, base).items() + } + + # Map changes to nodes. + if changed_ranges: + changed_nodes = map_changes_to_nodes(store, changed_ranges) + else: + # Fallback: all nodes in changed files. + changed_nodes = [] + for fp in changed_files: + changed_nodes.extend(store.get_nodes_by_file(fp)) + + # Filter to functions/tests for risk scoring (skip File nodes). + changed_funcs = [ + n for n in changed_nodes + if n.kind in ("Function", "Test", "Class") + ] + + # Cap to prevent O(N*M) query explosion on large PRs. + _max_funcs = int(os.environ.get("CRG_MAX_CHANGED_FUNCS", "500")) + funcs_truncated = len(changed_funcs) > _max_funcs + if funcs_truncated: + changed_funcs = changed_funcs[:_max_funcs] + + # Compute per-node risk scores. + node_risks: list[dict[str, Any]] = [] + for node in changed_funcs: + risk = compute_risk_score(store, node) + node_risks.append({ + **node_to_dict(node), + "risk_score": risk, + }) + + # Overall risk score: max of individual risks, or 0. + overall_risk = max((nr["risk_score"] for nr in node_risks), default=0.0) + + # Affected flows. + affected = get_affected_flows(store, changed_files) + + # Detect test gaps: changed functions without TESTED_BY edges. + test_gaps: list[dict[str, Any]] = [] + for node in changed_funcs: + if node.is_test: + continue + tested = store.get_edges_by_target(node.qualified_name) + if not any(e.kind == "TESTED_BY" for e in tested): + test_gaps.append({ + "name": _sanitize_name(node.name), + "qualified_name": _sanitize_name(node.qualified_name), + "file": node.file_path, + "line_start": node.line_start, + "line_end": node.line_end, + }) + + # Review priorities: top 10 by risk score. + review_priorities = sorted(node_risks, key=lambda x: x["risk_score"], reverse=True)[:10] + + # Build summary. + summary_parts = [ + f"Analyzed {len(changed_files)} changed file(s):", + f" - {len(changed_funcs)} changed function(s)/class(es)", + f" - {affected['total']} affected flow(s)", + f" - {len(test_gaps)} test gap(s)", + f" - Overall risk score: {overall_risk:.2f}", + ] + if test_gaps: + # Dedup by bare name in the human summary. The underlying test_gaps + # list keeps every entry (a downstream consumer needs precision via + # qualified_name), but a graph that ended up with the same function + # stored under two qualified_names (e.g. relative + absolute path + # variants) would otherwise print "X, X, Y, Y" — surfacing graph + # corruption as a UX bug. The root cause is path normalization; + # this is the defensive last line. + seen_names: set[str] = set() + gap_names: list[str] = [] + for g in test_gaps: + n = g["name"] + if n in seen_names: + continue + seen_names.add(n) + gap_names.append(n) + if len(gap_names) >= 5: + break + summary_parts.append(f" - Untested: {', '.join(gap_names)}") + if funcs_truncated: + summary_parts.append( + f" - Warning: analysis capped at {_max_funcs} functions " + f"(set CRG_MAX_CHANGED_FUNCS to adjust)" + ) + + return { + "summary": "\n".join(summary_parts), + "risk_score": overall_risk, + "changed_functions": node_risks, + "affected_flows": affected["affected_flows"], + "test_gaps": test_gaps, + "review_priorities": review_priorities, + "functions_truncated": funcs_truncated, + } diff --git a/code_review_graph/cli.py b/code_review_graph/cli.py new file mode 100644 index 0000000..102ad8a --- /dev/null +++ b/code_review_graph/cli.py @@ -0,0 +1,1243 @@ +"""CLI entry point for code-review-graph. + +Usage: + code-review-graph install + code-review-graph init + code-review-graph build [--base BASE] + code-review-graph update [--base BASE] + code-review-graph watch + code-review-graph status + code-review-graph serve [--auto-watch] [--http] [--host ADDR] [--port PORT] + code-review-graph mcp [--auto-watch] + code-review-graph visualize + code-review-graph wiki + code-review-graph detect-changes [--base BASE] [--brief] + code-review-graph register [--alias name] + code-review-graph unregister + code-review-graph repos + code-review-graph daemon start [--foreground] + code-review-graph daemon stop + code-review-graph daemon restart [--foreground] + code-review-graph daemon status + code-review-graph daemon logs [--repo ALIAS] [--follow] [--lines N] + code-review-graph daemon add [--alias NAME] + code-review-graph daemon remove +""" + +from __future__ import annotations + +import sys + +# Python version check — must come before any other imports +if sys.version_info < (3, 10): + print("code-review-graph requires Python 3.10 or higher.") + print(f" You are running Python {sys.version}") + print() + print("Install Python 3.10+: https://www.python.org/downloads/") + sys.exit(1) + +import argparse +import json +import logging +import os +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version +from pathlib import Path + +logger = logging.getLogger(__name__) + +# Shared platform choices for install and init commands +_PLATFORM_CHOICES = [ + "codex", "claude", "claude-code", "cursor", "windsurf", "zed", + "continue", "opencode", "antigravity", "gemini-cli", "qwen", "kiro", "qoder", + "copilot", "copilot-cli", "all", +] + + +def _get_version() -> str: + """Get the installed package version. + + Tries ``importlib.metadata`` first (canonical source from the installed + dist-info), falling back to the package's ``__version__`` attribute if + metadata is unavailable or corrupt. This matters for editable installs + on filesystems where iCloud / OneDrive can leave orphan dist-info dirs + behind that confuse importlib.metadata's lookup. + """ + try: + v = pkg_version("code-review-graph") + if v: + return v + except PackageNotFoundError as exc: + logger.debug("Package metadata unavailable: %s", exc) + # Fallback: read __version__ directly from the package. + try: + from . import __version__ as fallback_version + if fallback_version: + return fallback_version + except ImportError: + pass + return "dev" + + +def _supports_color() -> bool: + """Check if the terminal likely supports ANSI colors.""" + if os.environ.get("NO_COLOR"): + return False + if not hasattr(sys.stdout, "isatty"): + return False + return sys.stdout.isatty() + + +def _print_banner() -> None: + """Print the startup banner with graph art and available commands.""" + color = _supports_color() + version = _get_version() + + # ANSI escape codes + c = "\033[36m" if color else "" # cyan — graph art + y = "\033[33m" if color else "" # yellow — center node + b = "\033[1m" if color else "" # bold + d = "\033[2m" if color else "" # dim + g = "\033[32m" if color else "" # green — commands + r = "\033[0m" if color else "" # reset + + print(f""" +{c} ●──●──●{r} +{c} │╲ │ ╱│{r} {b}code-review-graph{r} {d}v{version}{r} +{c} ●──{y}◆{c}──●{r} +{c} │╱ │ ╲│{r} {d}Structural knowledge graph for{r} +{c} ●──●──●{r} {d}smarter code reviews{r} + + {b}Commands:{r} + {g}install{r} Set up MCP server for AI coding platforms + {g}init{r} Alias for install + {g}build{r} Full graph build {d}(parse all files){r} + {g}update{r} Incremental update {d}(changed files only){r} + {g}watch{r} Auto-update on file changes + {g}status{r} Show graph statistics + {g}visualize{r} Generate interactive HTML graph + {g}wiki{r} Generate markdown wiki from communities + {g}detect-changes{r} Analyze change impact {d}(risk-scored review){r} + {g}register{r} Register a repository in the multi-repo registry + {g}unregister{r} Remove a repository from the registry + {g}repos{r} List registered repositories + {g}postprocess{r} Run post-processing {d}(flows, communities, FTS){r} + {g}daemon{r} Multi-repo watch daemon management + {g}eval{r} Run evaluation benchmarks + {g}serve{r} Start MCP server {d}(stdio, or {g}--http{r} on localhost:5555){r} + + {d}Run{r} {b}code-review-graph --help{r} {d}for details{r} +""") + + +def _instruction_files_to_modify( + repo_root: Path, + target: str, +) -> list[str]: + """Return the list of instruction files that ``install`` would write + or modify, given the current state of the repo and the selected + platform target. Used for the dry-run / confirm preview (#173). + """ + from .skills import _CLAUDE_MD_SECTION_MARKER, _PLATFORM_INSTRUCTION_FILES + + targets: list[str] = [] + + if target in ("claude", "all"): + claude_md = repo_root / "CLAUDE.md" + if claude_md.exists(): + content = claude_md.read_text(encoding="utf-8") + if _CLAUDE_MD_SECTION_MARKER not in content: + targets.append("CLAUDE.md (append)") + else: + targets.append("CLAUDE.md (new)") + + for filename, owners in _PLATFORM_INSTRUCTION_FILES.items(): + if target != "all" and target not in owners: + continue + path = repo_root / filename + if path.exists(): + content = path.read_text(encoding="utf-8") + if _CLAUDE_MD_SECTION_MARKER not in content: + targets.append(f"{filename} (append)") + else: + targets.append(f"{filename} (new)") + + return targets + + +def _confirm_yes_no(prompt: str, default_yes: bool = True) -> bool: + """Prompt the user [Y/n] and return True for yes. + + Non-interactive environments (no TTY on stdin, e.g. an MCP wrapper + piping the CLI) return ``default_yes`` without blocking — the + stdio transport cannot safely read from stdin without corrupting + the JSON-RPC stream. See: #173, #174 + """ + if not sys.stdin.isatty(): + return default_yes + suffix = "[Y/n]" if default_yes else "[y/N]" + try: + answer = input(f"{prompt} {suffix} ").strip().lower() + except (EOFError, KeyboardInterrupt): + print() + return False + if not answer: + return default_yes + return answer in ("y", "yes") + + +def _handle_init(args: argparse.Namespace) -> None: + """Set up MCP config for detected AI coding platforms.""" + from .incremental import ensure_repo_gitignore_excludes_crg, find_repo_root + from .skills import install_platform_configs + + repo_root = Path(args.repo) if args.repo else find_repo_root() + if not repo_root: + repo_root = Path.cwd() + + dry_run = getattr(args, "dry_run", False) + target = getattr(args, "platform", "all") or "all" + if target == "claude-code": + target = "claude" + auto_yes = getattr(args, "yes", False) + skip_instructions = getattr(args, "no_instructions", False) + + print("Installing MCP server config...") + configured = install_platform_configs(repo_root, target=target, dry_run=dry_run) + + if not configured: + print("No platforms detected.") + else: + print(f"\nConfigured {len(configured)} platform(s): {', '.join(configured)}") + + # Preview the instruction files that would be touched (#173). + instr_targets = _instruction_files_to_modify(repo_root, target) + if instr_targets: + print() + print("Graph instructions will be injected into:") + for t in instr_targets: + print(f" {t}") + + if dry_run: + print("\n[dry-run] Would ensure .gitignore ignores .code-review-graph/.") + print("[dry-run] No files were modified.") + return + + gitignore_state = ensure_repo_gitignore_excludes_crg(repo_root) + if gitignore_state == "created": + print("Created .gitignore and added .code-review-graph/.") + elif gitignore_state == "updated": + print("Updated .gitignore with .code-review-graph/.") + else: + print(".gitignore already contains .code-review-graph/.") + + # Platform-native skills and hooks are installed by default where supported + # so the graph tools are used proactively. Use --no-skills / --no-hooks / + # --no-instructions to opt out. + skip_skills = getattr(args, "no_skills", False) + skip_hooks = getattr(args, "no_hooks", False) + # Legacy: --skills/--hooks/--all still accepted (no-op, everything is default) + + from .skills import ( + PLATFORMS, + generate_skills, + inject_claude_md, + inject_platform_instructions, + install_codex_hooks, + install_cursor_hooks, + install_gemini_cli_hooks, + install_gemini_cli_skills, + install_git_hook, + install_hooks, + install_opencode_plugin, + install_qoder_skills, + ) + + if not skip_skills: + # Claude Code skills are only relevant for Claude (or full install). + if target in ("claude", "all"): + skills_dir = generate_skills(repo_root) + print(f"Generated Claude Code skills in {skills_dir}") + + # Gemini CLI skills are workspace-scoped under .gemini/. + if target in ("gemini-cli", "all"): + gemini_skills_dir = install_gemini_cli_skills(repo_root) + print(f"Installed Gemini CLI skills in {gemini_skills_dir}") + + # Confirm before writing instruction files (#173). --yes skips the + # prompt; --no-instructions skips the whole block. + if not skip_instructions and instr_targets: + if auto_yes or _confirm_yes_no( + "Inject graph instructions into the files above?", + default_yes=True, + ): + if target in ("claude", "all"): + inject_claude_md(repo_root) + inject_platform_instructions(repo_root, target=target) + # Use the precomputed instr_targets list for the confirmation + # message; we don't need the fresh return value from + # inject_platform_instructions here. + names = [t.split(" ")[0] for t in instr_targets] + print(f"Injected graph instructions into: {', '.join(names)}") + else: + print("Skipped instruction injection (user declined).") + elif skip_instructions: + print("Skipped instruction injection (--no-instructions).") + + + # Install Qoder skills (global user-level skills directory) + if not skip_skills and target in ("qoder", "all"): + qoder_skills_dir = install_qoder_skills(repo_root) + if qoder_skills_dir: + print(f"Installed Qoder skills to {qoder_skills_dir}") + if not skip_hooks and target in ("codex", "all"): + hooks_path = install_codex_hooks(repo_root) + print(f"Installed Codex hooks in {hooks_path}") + git_hook = install_git_hook(repo_root) + if git_hook: + print(f"Installed git pre-commit hook in {git_hook}") + if not skip_hooks and target in ("claude", "qoder", "all"): + platforms_to_install = [target] if target != "all" else ["claude", "qoder"] + for plat in platforms_to_install: + install_hooks(repo_root, platform=plat) + print(f"Installed hooks in {repo_root / f'.{plat}' / 'settings.json'}") + git_hook = install_git_hook(repo_root) + if git_hook: + print(f"Installed git pre-commit hook in {git_hook}") + + # Cursor hooks (user-level, only if ~/.cursor exists — matching MCP detect) + if not skip_hooks and target in ("all", "cursor") and PLATFORMS["cursor"]["detect"](): + try: + hooks_path = install_cursor_hooks() + print(f"Installed Cursor hooks in {hooks_path}") + except Exception as exc: + logger.warning("Could not install Cursor hooks: %s", exc) + + if not skip_hooks and target in ("gemini-cli", "all"): + try: + gemini_settings = install_gemini_cli_hooks(repo_root) + print(f"Installed Gemini CLI hooks in {gemini_settings}") + except Exception as exc: + logger.warning("Could not install Gemini CLI hooks: %s", exc) + + # OpenCode plugin (user-level, gated by same detect() as MCP config) + if not skip_hooks and target in ("all", "opencode") and PLATFORMS["opencode"]["detect"](): + try: + plugin_path = install_opencode_plugin() + print(f"Installed OpenCode plugin in {plugin_path}") + except Exception as exc: + logger.warning("Could not install OpenCode plugin: %s", exc) + + print() + print("Next steps:") + print(" 1. code-review-graph build # build the knowledge graph") + print(" 2. Restart your AI coding tool to pick up the new config") + + +def _handle_data_dir_option(args, repo_root: Path) -> None: + """Handle --data-dir option by updating registry if specified.""" + if hasattr(args, "data_dir") and args.data_dir: + try: + from .registry import Registry + data_dir_path = Path(args.data_dir).expanduser().resolve() + data_dir_path.mkdir(parents=True, exist_ok=True) + Registry().set_data_dir(str(repo_root), str(data_dir_path)) + logging.info(f"Graph database will be stored at: {data_dir_path}") + except Exception as exc: + logging.error(f"Failed to set data directory: {exc}") + sys.exit(1) + + +def main() -> None: + """Main CLI entry point.""" + ap = argparse.ArgumentParser( + prog="code-review-graph", + description="Persistent incremental knowledge graph for code reviews", + ) + ap.add_argument("-v", "--version", action="store_true", help="Show version and exit") + sub = ap.add_subparsers(dest="command") + + # install (primary) + init (alias) + install_cmd = sub.add_parser("install", help="Register MCP server with AI coding platforms") + install_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + install_cmd.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without writing files", + ) + install_cmd.add_argument( + "--no-skills", + action="store_true", + help="Skip generating platform-native skill files", + ) + install_cmd.add_argument( + "--no-hooks", + action="store_true", + help="Skip installing platform-native hooks", + ) + install_cmd.add_argument( + "--no-instructions", + action="store_true", + help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.", + ) + install_cmd.add_argument( + "-y", + "--yes", + action="store_true", + help="Auto-confirm instruction injection without an interactive prompt", + ) + # Legacy flags (kept for backwards compat, now no-ops since all is default) + install_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS) + install_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS) + install_cmd.add_argument( + "--all", action="store_true", dest="install_all", help=argparse.SUPPRESS + ) + install_cmd.add_argument( + "--platform", + choices=_PLATFORM_CHOICES, + default="all", + help="Target platform for MCP config (default: all detected)", + ) + + init_cmd = sub.add_parser("init", help="Alias for install") + init_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + init_cmd.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without writing files", + ) + init_cmd.add_argument( + "--no-skills", + action="store_true", + help="Skip generating platform-native skill files", + ) + init_cmd.add_argument( + "--no-hooks", + action="store_true", + help="Skip installing platform-native hooks", + ) + init_cmd.add_argument( + "--no-instructions", + action="store_true", + help="Skip injecting graph instructions into CLAUDE.md / AGENTS.md / etc.", + ) + init_cmd.add_argument( + "-y", + "--yes", + action="store_true", + help="Auto-confirm instruction injection without an interactive prompt", + ) + init_cmd.add_argument("--skills", action="store_true", help=argparse.SUPPRESS) + init_cmd.add_argument("--hooks", action="store_true", help=argparse.SUPPRESS) + init_cmd.add_argument("--all", action="store_true", dest="install_all", help=argparse.SUPPRESS) + init_cmd.add_argument( + "--platform", + choices=_PLATFORM_CHOICES, + default="all", + help="Target platform for MCP config (default: all detected)", + ) + + # build + build_cmd = sub.add_parser("build", help="Full graph build (re-parse all files)") + build_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + build_cmd.add_argument( + "--skip-flows", + action="store_true", + help="Skip flow/community detection (signatures + FTS only)", + ) + build_cmd.add_argument( + "--skip-postprocess", + action="store_true", + help="Skip all post-processing (raw parse only)", + ) + build_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # update + update_cmd = sub.add_parser("update", help="Incremental update (only changed files)") + update_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)") + update_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + update_cmd.add_argument( + "--skip-flows", + action="store_true", + help="Skip flow/community detection (signatures + FTS only)", + ) + update_cmd.add_argument( + "--skip-postprocess", + action="store_true", + help="Skip all post-processing (raw parse only)", + ) + update_cmd.add_argument( + "--brief", + action="store_true", + help="After re-parsing changed files into the graph, also print the " + "risk summary + Token Savings panel that 'detect-changes --brief' " + "prints. Use this after a rebase or large change set when you " + "want to refresh the graph AND see the impact in one command; " + "use 'detect-changes --brief' alone when the graph is already " + "up to date (analysis only, no re-parse).", + ) + update_cmd.add_argument( + "--verify", + action="store_true", + help="Calibrate the estimated savings against tiktoken's " + "cl100k_base tokenizer (the GPT-4 family tokenizer). Adds a " + "second row to the panel with the real token counts. Requires " + "`pip install tiktoken`.", + ) + update_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # postprocess + pp_cmd = sub.add_parser( + "postprocess", + help="Run post-processing on existing graph (flows, communities, FTS)", + ) + pp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + pp_cmd.add_argument("--no-flows", action="store_true", help="Skip flow detection") + pp_cmd.add_argument("--no-communities", action="store_true", help="Skip community detection") + pp_cmd.add_argument("--no-fts", action="store_true", help="Skip FTS rebuild") + pp_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # embed + embed_cmd = sub.add_parser( + "embed", + help="Compute vector embeddings for semantic search", + ) + embed_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + embed_cmd.add_argument( + "--provider", + choices=["local", "openai", "google", "minimax"], + default=None, + help="Embedding provider (default: local, needs code-review-graph[embeddings])", + ) + embed_cmd.add_argument( + "--model", + default=None, + help="Embedding model. For local: HuggingFace ID (default all-MiniLM-L6-v2); " + "for openai/google/minimax: provider-specific model ID.", + ) + embed_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # watch + watch_cmd = sub.add_parser("watch", help="Watch for changes and auto-update") + watch_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + watch_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # status + status_cmd = sub.add_parser("status", help="Show graph statistics") + status_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + status_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # visualize + vis_cmd = sub.add_parser("visualize", help="Generate interactive HTML graph visualization") + vis_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + vis_cmd.add_argument( + "--mode", + choices=["auto", "full", "community", "file"], + default="auto", + help="Rendering mode: auto (default), full, community, or file", + ) + vis_cmd.add_argument( + "--serve", + action="store_true", + help="Start a local HTTP server to view the visualization (localhost:8765)", + ) + vis_cmd.add_argument( + "--format", + choices=["html", "graphml", "cypher", "obsidian", "svg"], + default="html", + help="Export format (default: html)", + ) + vis_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # wiki + wiki_cmd = sub.add_parser("wiki", help="Generate markdown wiki from community structure") + wiki_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + wiki_cmd.add_argument( + "--force", + action="store_true", + help="Regenerate all pages even if content unchanged", + ) + wiki_cmd.add_argument( + "--data-dir", + default=None, + help="External directory to store graph database (useful for network shares)" + ) + + # register + register_cmd = sub.add_parser( + "register", help="Register a repository in the multi-repo registry" + ) + register_cmd.add_argument("path", help="Path to the repository root") + register_cmd.add_argument("--alias", default=None, help="Short alias for the repository") + + # unregister + unregister_cmd = sub.add_parser( + "unregister", help="Remove a repository from the multi-repo registry" + ) + unregister_cmd.add_argument("path_or_alias", help="Repository path or alias to remove") + + # repos + sub.add_parser("repos", help="List registered repositories") + + # eval + eval_cmd = sub.add_parser("eval", help="Run evaluation benchmarks") + eval_cmd.add_argument( + "--benchmark", + default=None, + help="Comma-separated benchmarks to run (token_efficiency, impact_accuracy, " + "agent_baseline, flow_completeness, search_quality, build_performance, " + "multi_hop_retrieval)", + ) + eval_cmd.add_argument("--repo", default=None, help="Comma-separated repo config names") + eval_cmd.add_argument("--all", action="store_true", dest="run_all", help="Run all benchmarks") + eval_cmd.add_argument("--report", action="store_true", help="Generate report from results") + eval_cmd.add_argument("--output-dir", default=None, help="Output directory for results") + + # detect-changes + detect_cmd = sub.add_parser( + "detect-changes", + help="Analyze change impact against the existing graph (read-only). " + "Does NOT re-parse files — for that, use 'update --brief'.", + ) + detect_cmd.add_argument("--base", default="HEAD~1", help="Git diff base (default: HEAD~1)") + detect_cmd.add_argument( + "--brief", + action="store_true", + help="Show the risk summary + Token Savings panel instead of the " + "full JSON. Read-only against the existing graph.", + ) + detect_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + detect_cmd.add_argument( + "--verify", + action="store_true", + help="Calibrate the estimated savings against tiktoken's " + "cl100k_base tokenizer (the GPT-4 family tokenizer). Adds a " + "second row to the panel with the real token counts. Requires " + "`pip install tiktoken`.", + ) + + # serve / mcp + serve_cmd = sub.add_parser( + "serve", + help="Start MCP server (stdio by default, or HTTP on localhost with --http)", + ) + serve_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + serve_cmd.add_argument( + "--auto-watch", + action="store_true", + help="Start filesystem watch in a daemon thread while MCP server runs", + ) + serve_cmd.add_argument( + "--tools", default=None, + help=( + "Comma-separated list of tool names to expose " + "(e.g. query_graph_tool,semantic_search_nodes_tool). " + "Unlisted tools are removed. Falls back to CRG_TOOLS env var. " + "When unset, all tools are available." + ), + ) + serve_cmd.add_argument( + "--http", + action="store_true", + help="Listen for MCP over Streamable HTTP on localhost (default port 5555)", + ) + serve_cmd.add_argument( + "--host", + default=None, + metavar="ADDR", + help="Bind address for --http (default: 127.0.0.1)", + ) + serve_cmd.add_argument( + "--port", + type=int, + default=None, + metavar="PORT", + help="Port for --http (default: 5555)", + ) + + mcp_cmd = sub.add_parser("mcp", help="Alias for serve") + mcp_cmd.add_argument("--repo", default=None, help="Repository root (auto-detected)") + mcp_cmd.add_argument( + "--auto-watch", + action="store_true", + help="Start filesystem watch in a daemon thread while MCP server runs", + ) + + # daemon + daemon_cmd = sub.add_parser( + "daemon", + help="Multi-repo watch daemon (start/stop/status/add/remove)", + ) + daemon_sub = daemon_cmd.add_subparsers(dest="daemon_command") + + daemon_start = daemon_sub.add_parser( + "start", + help="Start the watch daemon", + ) + daemon_start.add_argument( + "--foreground", + action="store_true", + help="Run in foreground instead of daemonizing", + ) + + daemon_sub.add_parser( + "stop", + help="Stop the watch daemon", + ) + + daemon_restart = daemon_sub.add_parser( + "restart", + help="Restart the watch daemon", + ) + daemon_restart.add_argument( + "--foreground", + action="store_true", + help="Run in foreground instead of daemonizing", + ) + + daemon_sub.add_parser("status", help="Show daemon and watcher status") + + daemon_logs = daemon_sub.add_parser( + "logs", + help="View daemon or watcher logs", + ) + daemon_logs.add_argument( + "--repo", + default=None, + help="Show logs for a specific repo alias", + ) + daemon_logs.add_argument( + "--follow", + action="store_true", + help="Follow log output (tail -f)", + ) + daemon_logs.add_argument( + "--lines", + type=int, + default=50, + help="Number of lines to show (default: 50)", + ) + + daemon_add = daemon_sub.add_parser( + "add", + help="Add a repo to the watch config", + ) + daemon_add.add_argument("path", help="Path to the repository") + daemon_add.add_argument( + "--alias", + default=None, + help="Short alias for the repo", + ) + + daemon_remove = daemon_sub.add_parser( + "remove", + help="Remove a repo from the watch config", + ) + daemon_remove.add_argument( + "path_or_alias", + help="Repository path or alias to remove", + ) + + args = ap.parse_args() + + if args.version: + print(f"code-review-graph {_get_version()}") + return + + if not args.command: + _print_banner() + return + + if args.command in ("serve", "mcp"): + from .main import main as serve_main + + auto_watch = getattr(args, "auto_watch", False) + if args.command == "serve": + if args.port is not None and not args.http: + serve_cmd.error("--port requires --http") + if args.host is not None and not args.http: + serve_cmd.error("--host requires --http") + if args.http: + host = args.host if args.host is not None else "127.0.0.1" + port = args.port if args.port is not None else 5555 + serve_main( + repo_root=args.repo, + auto_watch=auto_watch, + transport="streamable-http", + host=host, + port=port, + tools=args.tools, + ) + else: + serve_main(repo_root=args.repo, auto_watch=auto_watch, tools=args.tools) + else: + serve_main(repo_root=args.repo, auto_watch=auto_watch) + return + + if args.command == "daemon": + if not args.daemon_command: + daemon_cmd.print_help() + return + from .daemon_cli import ( + _handle_add, + _handle_logs, + _handle_remove, + _handle_restart, + _handle_start, + _handle_status, + _handle_stop, + ) + + handlers = { + "start": _handle_start, + "stop": _handle_stop, + "restart": _handle_restart, + "status": _handle_status, + "logs": _handle_logs, + "add": _handle_add, + "remove": _handle_remove, + } + handler = handlers.get(args.daemon_command) + if handler: + handler(args) + return + + if args.command == "eval": + from .eval.reporter import generate_full_report, generate_readme_tables + from .eval.runner import run_eval + + if getattr(args, "report", False): + output_dir = Path(getattr(args, "output_dir", None) or "evaluate/results") + report = generate_full_report(output_dir) + report_path = Path("evaluate/reports/summary.md") + report_path.parent.mkdir(parents=True, exist_ok=True) + report_path.write_text(report, encoding="utf-8") + print(f"Report written to {report_path}") + + tables = generate_readme_tables(output_dir) + print("\n--- README Tables (copy-paste) ---\n") + print(tables) + else: + repos = ( + [r.strip() for r in args.repo.split(",")] if getattr(args, "repo", None) else None + ) + benchmarks = ( + [b.strip() for b in args.benchmark.split(",")] + if getattr(args, "benchmark", None) + else None + ) + + if not repos and not benchmarks and not getattr(args, "run_all", False): + print("Specify --all, --repo, or --benchmark. See --help.") + return + + results = run_eval( + repos=repos, + benchmarks=benchmarks, + output_dir=getattr(args, "output_dir", None), + ) + print(f"\nCompleted {len(results)} benchmark(s).") + print("Run 'code-review-graph eval --report' to generate tables.") + return + + if args.command in ("init", "install"): + _handle_init(args) + return + + if args.command in ("register", "unregister", "repos"): + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + from .registry import Registry + + registry = Registry() + if args.command == "register": + try: + entry = registry.register(args.path, alias=args.alias) + alias_info = f" (alias: {entry['alias']})" if entry.get("alias") else "" + print(f"Registered: {entry['path']}{alias_info}") + except ValueError as exc: + logging.error(str(exc)) + sys.exit(1) + elif args.command == "unregister": + if registry.unregister(args.path_or_alias): + print(f"Unregistered: {args.path_or_alias}") + else: + print(f"Not found: {args.path_or_alias}") + sys.exit(1) + elif args.command == "repos": + repos = registry.list_repos() + if not repos: + print("No repositories registered.") + print("Use: code-review-graph register [--alias name]") + else: + for entry in repos: + alias = entry.get("alias", "") + alias_str = f" ({alias})" if alias else "" + print(f" {entry['path']}{alias_str}") + return + + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + from .graph import GraphStore + from .incremental import ( + find_project_root, + find_repo_root, + get_db_path, + watch, + ) + + if args.command == "postprocess": + repo_root = Path(args.repo) if args.repo else find_project_root() + _handle_data_dir_option(args, repo_root) + db_path = get_db_path(repo_root) + store = GraphStore(db_path) + try: + from .tools.build import run_postprocess + + result = run_postprocess( + flows=not getattr(args, "no_flows", False), + communities=not getattr(args, "no_communities", False), + fts=not getattr(args, "no_fts", False), + repo_root=str(repo_root), + ) + parts = [] + if result.get("flows_detected"): + parts.append(f"{result['flows_detected']} flows") + if result.get("communities_detected"): + parts.append(f"{result['communities_detected']} communities") + if result.get("fts_indexed"): + parts.append(f"{result['fts_indexed']} FTS entries") + print(f"Post-processing: {', '.join(parts) or 'done'}") + finally: + store.close() + return + + if args.command == "embed": + repo_root = Path(args.repo) if args.repo else find_project_root() + _handle_data_dir_option(args, repo_root) + from .tools.docs import embed_graph + + result = embed_graph( + repo_root=str(repo_root), + model=args.model, + provider=args.provider, + ) + if result.get("status") == "error": + logging.error(result.get("error", "embed_graph failed")) + sys.exit(1) + print(result.get("summary", "Embedding done.")) + return + + if args.command in ("update", "detect-changes"): + # update and detect-changes require git for diffing + repo_root = Path(args.repo) if args.repo else find_repo_root() + if not repo_root: + logging.error( + "Not in a git repository. '%s' requires git for diffing.", + args.command, + ) + logging.error("Use 'build' for a full parse, or run 'git init' first.") + sys.exit(1) + else: + repo_root = Path(args.repo) if args.repo else find_project_root() + + # Handle --data-dir for commands that support it + _data_dir_cmds = ("build", "update", "detect-changes", "status", "watch", "visualize", "wiki") + if args.command in _data_dir_cmds: + _handle_data_dir_option(args, repo_root) + + db_path = get_db_path(repo_root) + store = GraphStore(db_path) + + try: + if args.command == "build": + pp = ( + "none" + if getattr(args, "skip_postprocess", False) + else ("minimal" if getattr(args, "skip_flows", False) else "full") + ) + from .tools.build import build_or_update_graph + + result = build_or_update_graph( + full_rebuild=True, + repo_root=str(repo_root), + postprocess=pp, + ) + parsed = result.get("files_parsed", 0) + nodes = result.get("total_nodes", 0) + edges = result.get("total_edges", 0) + print(f"Full build: {parsed} files, {nodes} nodes, {edges} edges (postprocess={pp})") + if result.get("errors"): + print(f"Errors: {len(result['errors'])}") + + elif args.command == "update": + pp = ( + "none" + if getattr(args, "skip_postprocess", False) + else ("minimal" if getattr(args, "skip_flows", False) else "full") + ) + from .tools.build import build_or_update_graph + + result = build_or_update_graph( + full_rebuild=False, + repo_root=str(repo_root), + base=args.base, + postprocess=pp, + ) + updated = result.get("files_updated", 0) + nodes = result.get("total_nodes", 0) + edges = result.get("total_edges", 0) + print( + f"Incremental: {updated} files updated, " + f"{nodes} nodes, {edges} edges" + f" (postprocess={pp})" + ) + + # --brief: append a one-line change-impact summary with the same + # estimated context-savings approximation that detect-changes uses. + # Same baseline (changed files vs analysis response), so the two + # commands are directly comparable. + if getattr(args, "brief", False): + from .changes import analyze_changes + from .context_savings import ( + attach_context_savings, + estimate_file_tokens, + format_context_savings_panel, + ) + from .incremental import ( + get_changed_files, + get_staged_and_unstaged, + ) + + changed = get_changed_files(repo_root, args.base) + if not changed: + changed = get_staged_and_unstaged(repo_root) + if changed: + impact = analyze_changes( + store, + changed, + repo_root=str(repo_root), + base=args.base, + ) + original_tokens = estimate_file_tokens(repo_root, changed) + attach_context_savings( + impact, + original_tokens=original_tokens, + ) + summary = impact.get("summary", "") + if summary: + print(summary) + verified = None + if getattr(args, "verify", False): + from .context_savings import verify_with_tiktoken + verified = verify_with_tiktoken( + repo_root, changed, impact, + ) + if verified is None: + print( + "Note: --verify requires tiktoken. " + "Install with `pip install tiktoken`.", + ) + panel = format_context_savings_panel( + impact.get("context_savings"), + original_tokens=original_tokens, + response=impact, + verified=verified, + ) + if panel: + print(panel) + + elif args.command == "status": + stats = store.get_stats() + print(f"Nodes: {stats.total_nodes}") + print(f"Edges: {stats.total_edges}") + print(f"Files: {stats.files_count}") + print(f"Languages: {', '.join(stats.languages)}") + print(f"Last updated: {stats.last_updated or 'never'}") + # Show branch info and warn if stale + stored_branch = store.get_metadata("git_branch") + stored_sha = store.get_metadata("git_head_sha") + if stored_branch: + print(f"Built on branch: {stored_branch}") + if stored_sha: + print(f"Built at commit: {stored_sha[:12]}") + from .incremental import _git_branch_info, detect_vcs + vcs = detect_vcs(repo_root) + if vcs == "git": + current_branch, current_sha = _git_branch_info(repo_root) + if stored_branch and current_branch and stored_branch != current_branch: + print( + f"WARNING: Graph was built on '{stored_branch}' " + f"but you are now on '{current_branch}'. " + f"Run 'code-review-graph build' to rebuild." + ) + elif vcs == "svn": + stored_rev = store.get_metadata("svn_revision") + stored_svn_branch = store.get_metadata("svn_branch") + if stored_svn_branch: + print(f"SVN branch: {stored_svn_branch}") + if stored_rev: + print(f"SVN revision at build: {stored_rev}") + + elif args.command == "watch": + from .postprocessing import run_post_processing + + try: + watch(repo_root, store, on_files_updated=run_post_processing) + except RuntimeError as exc: + print(f"Error: {exc}", file=sys.stderr) + sys.exit(1) + + elif args.command == "visualize": + from .incremental import get_data_dir + + data_dir = get_data_dir(repo_root) + fmt = getattr(args, "format", "html") or "html" + + if fmt == "graphml": + from .exports import export_graphml + + out = data_dir / "graph.graphml" + export_graphml(store, out) + print(f"GraphML exported: {out}") + elif fmt == "cypher": + from .exports import export_neo4j_cypher + + out = data_dir / "graph.cypher" + export_neo4j_cypher(store, out) + print(f"Neo4j Cypher exported: {out}") + elif fmt == "obsidian": + from .exports import export_obsidian_vault + + out = data_dir / "obsidian" + export_obsidian_vault(store, out) + print(f"Obsidian vault exported: {out}") + elif fmt == "svg": + from .exports import export_svg + + out = data_dir / "graph.svg" + export_svg(store, out) + print(f"SVG exported: {out}") + else: + from .visualization import generate_html + + html_path = data_dir / "graph.html" + vis_mode = getattr(args, "mode", "auto") or "auto" + generate_html(store, html_path, mode=vis_mode) + print(f"Visualization ({vis_mode}): {html_path}") + if getattr(args, "serve", False): + import functools + import http.server + + serve_dir = html_path.parent + port = 8765 + http_handler = functools.partial( + http.server.SimpleHTTPRequestHandler, + directory=str(serve_dir), + ) + print(f"Serving at http://localhost:{port}/graph.html") + print("Press Ctrl+C to stop.") + with http.server.HTTPServer(("localhost", port), http_handler) as httpd: + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nServer stopped.") + else: + print("Open in browser to explore.") + + elif args.command == "wiki": + from .incremental import get_data_dir + from .wiki import generate_wiki + + wiki_dir = get_data_dir(repo_root) / "wiki" + result = generate_wiki(store, wiki_dir, force=args.force) + total = result["pages_generated"] + result["pages_updated"] + result["pages_unchanged"] + print( + f"Wiki: {result['pages_generated']} new, " + f"{result['pages_updated']} updated, " + f"{result['pages_unchanged']} unchanged " + f"({total} total pages)" + ) + print(f"Output: {wiki_dir}") + + elif args.command == "detect-changes": + from .changes import analyze_changes + from .context_savings import ( + attach_context_savings, + estimate_file_tokens, + ) + from .incremental import get_changed_files, get_staged_and_unstaged + + base = args.base + changed = get_changed_files(repo_root, base) + if not changed: + changed = get_staged_and_unstaged(repo_root) + + if not changed: + print("No changes detected.") + else: + result = analyze_changes( + store, + changed, + repo_root=str(repo_root), + base=base, + ) + original_tokens = estimate_file_tokens(repo_root, changed) + attach_context_savings( + result, + original_tokens=original_tokens, + ) + if args.brief: + from .context_savings import ( + format_context_savings_panel, + verify_with_tiktoken, + ) + print(result.get("summary", "No summary available.")) + verified = None + if getattr(args, "verify", False): + verified = verify_with_tiktoken(repo_root, changed, result) + if verified is None: + print( + "Note: --verify requires tiktoken. " + "Install with `pip install tiktoken`.", + ) + panel = format_context_savings_panel( + result.get("context_savings"), + original_tokens=original_tokens, + response=result, + verified=verified, + ) + if panel: + print(panel) + else: + print(json.dumps(result, indent=2, default=str)) + + finally: + store.close() diff --git a/code_review_graph/communities.py b/code_review_graph/communities.py new file mode 100644 index 0000000..c8a3de8 --- /dev/null +++ b/code_review_graph/communities.py @@ -0,0 +1,874 @@ +"""Community/cluster detection for the code knowledge graph. + +Detects communities of related code nodes using the Leiden algorithm (via igraph, +optional) with a file-based grouping fallback when igraph is not installed. +""" + +from __future__ import annotations + +import logging +import random +import re +from collections import Counter, defaultdict +from typing import Any + +from .graph import GraphEdge, GraphNode, GraphStore, _sanitize_name + +# Fixed seed for igraph's RNG so Leiden community detection is reproducible +# across runs. Without this, two builds of the same graph produce different +# community IDs / sizes, breaking benchmark comparability. Override with +# CRG_LEIDEN_SEED env var if you need a different seed. +_LEIDEN_SEED = 42 + +logger = logging.getLogger(__name__) + +# Stay well under SQLite's default 999-variable limit per statement. +_SQL_BATCH = 450 + +# --------------------------------------------------------------------------- +# Optional igraph import +# --------------------------------------------------------------------------- + +try: + import igraph as ig # type: ignore[import-untyped] + + IGRAPH_AVAILABLE = True +except ImportError: + ig = None # type: ignore[assignment] + IGRAPH_AVAILABLE = False + +# --------------------------------------------------------------------------- +# Edge weight mapping +# --------------------------------------------------------------------------- + +EDGE_WEIGHTS: dict[str, float] = { + "CALLS": 1.0, + "IMPORTS_FROM": 0.5, + "INHERITS": 0.8, + "IMPLEMENTS": 0.7, + "CONTAINS": 0.3, + "TESTED_BY": 0.4, + "DEPENDS_ON": 0.6, +} + +# Common words to filter when generating community names +_COMMON_WORDS = frozenset({ + "get", "set", "self", "init", "new", "create", "update", "delete", + "add", "remove", "make", "build", "from", "to", "for", "with", + "the", "and", "test", "main", "run", "do", "is", "has", "on", + "of", "in", "at", "by", "my", "this", "that", "all", "none", +}) + + +# --------------------------------------------------------------------------- +# Community naming +# --------------------------------------------------------------------------- + + +def _generate_community_name(members: list[GraphNode]) -> str: + """Generate a meaningful name for a community of nodes. + + Algorithm: + 1. Find most common module/file prefix among members + 2. If a dominant class exists (>40% of nodes), use its name + 3. Fallback: most frequent keyword in function/class names + 4. Format: "{prefix}-{keyword}" + """ + if not members: + return "empty" + + # 1. Find common file prefix + file_paths = [m.file_path for m in members] + prefix = _extract_file_prefix(file_paths) + + # 2. Check for dominant class + class_names = [m.name for m in members if m.kind == "Class"] + if class_names: + class_counts = Counter(class_names) + top_class, top_count = class_counts.most_common(1)[0] + if top_count > len(members) * 0.4: + if prefix: + return f"{prefix}-{_to_slug(top_class)}" + return _to_slug(top_class) + + # 3. Most frequent keyword from function/class names + keywords = _extract_keywords(members) + keyword = keywords[0] if keywords else "" + + if prefix and keyword: + return f"{prefix}-{keyword}" + if prefix: + return prefix + if keyword: + return keyword + return "cluster" + + +def _extract_file_prefix(file_paths: list[str]) -> str: + """Find the most common short directory or module name from file paths.""" + if not file_paths: + return "" + # Extract the parent directory or file stem + parts: list[str] = [] + for fp in file_paths: + # Use the last directory component or file stem + segments = fp.replace("\\", "/").split("/") + # Take the parent dir if it exists, otherwise the file stem + if len(segments) >= 2: + parts.append(segments[-2]) + else: + stem = segments[-1].rsplit(".", 1)[0] + parts.append(stem) + + counts = Counter(parts) + top_part, _ = counts.most_common(1)[0] + return _to_slug(top_part) + + +def _extract_keywords(members: list[GraphNode]) -> list[str]: + """Extract the most frequent meaningful keywords from member names.""" + word_counts: Counter[str] = Counter() + for m in members: + if m.kind in ("Function", "Class", "Test", "Type"): + words = _split_name(m.name) + for w in words: + wl = w.lower() + if wl not in _COMMON_WORDS and len(wl) > 1: + word_counts[wl] += 1 + + if not word_counts: + return [] + return [w for w, _ in word_counts.most_common(5)] + + +def _split_name(name: str) -> list[str]: + """Split a camelCase or snake_case name into words.""" + # Insert boundary before uppercase letters for camelCase + s = re.sub(r"([a-z])([A-Z])", r"\1_\2", name) + # Split on underscores, hyphens, dots + return [p for p in re.split(r"[_\-.\s]+", s) if p] + + +def _to_slug(s: str) -> str: + """Convert a string to a short lowercase slug.""" + return re.sub(r"[^a-z0-9]+", "-", s.lower()).strip("-")[:30] + + +# --------------------------------------------------------------------------- +# Cohesion calculation +# --------------------------------------------------------------------------- + + +def _compute_cohesion_batch( + community_member_qns: list[set[str]], + all_edges: list[GraphEdge], +) -> list[float]: + """Compute cohesion for multiple communities in a single O(edges) pass. + + Builds a ``qualified_name -> community_index`` reverse map (each node + appears in at most one community since all callers produce partitions), + then walks every edge exactly once, bucketing it into internal/external + counters per community. + + Total work: O(edges + sum(|members|)) instead of + O(edges * communities) for naive per-community cohesion. + + Returns a list of cohesion scores aligned with ``community_member_qns``. + """ + qn_to_idx: dict[str, int] = {} + for idx, members in enumerate(community_member_qns): + for qn in members: + qn_to_idx[qn] = idx + + n = len(community_member_qns) + internal = [0] * n + external = [0] * n + + for e in all_edges: + sc = qn_to_idx.get(e.source_qualified) + tc = qn_to_idx.get(e.target_qualified) + if sc is None and tc is None: + continue + if sc == tc: + # Safe: sc is not None here (sc == tc and not both None). + assert sc is not None + internal[sc] += 1 + else: + if sc is not None: + external[sc] += 1 + if tc is not None: + external[tc] += 1 + + results: list[float] = [] + for i in range(n): + total = internal[i] + external[i] + results.append(internal[i] / total if total > 0 else 0.0) + return results + + +def _build_adjacency(edges: list[GraphEdge]) -> dict[str, list[str]]: + """Build adjacency list from edges (one pass over all edges).""" + adj: dict[str, list[str]] = defaultdict(list) + for e in edges: + adj[e.source_qualified].append(e.target_qualified) + adj[e.target_qualified].append(e.source_qualified) + return adj + + +def _compute_cohesion( + member_qns: set[str], + all_edges: list[GraphEdge], + adj: dict[str, list[str]] | None = None, +) -> float: + """Compute cohesion: internal_edges / (internal_edges + external_edges). + + For multiple communities, prefer :func:`_compute_cohesion_batch`, which + runs in O(edges) total instead of O(edges) per community. + """ + return _compute_cohesion_batch([member_qns], all_edges)[0] + + +# --------------------------------------------------------------------------- +# Leiden-based community detection (igraph) +# --------------------------------------------------------------------------- + + +def _detect_leiden( + nodes: list[GraphNode], + edges: list[GraphEdge], + min_size: int, + adj: dict[str, list[str]] | None = None, +) -> list[dict[str, Any]]: + """Detect communities using Leiden algorithm via igraph. + + Caps Leiden at ``n_iterations=2`` (sufficient for code dependency graphs) + and skips the recursive sub-community splitting pass that caused + exponential blow-up on large repos (>100k nodes). + """ + if ig is None: + return [] + + qn_to_idx: dict[str, int] = {} + idx_to_node: dict[int, GraphNode] = {} + for i, node in enumerate(nodes): + qn_to_idx[node.qualified_name] = i + idx_to_node[i] = node + + if not qn_to_idx: + return [] + + logger.info("Building igraph with %d nodes...", len(qn_to_idx)) + + g = ig.Graph(n=len(qn_to_idx), directed=False) + edge_list: list[tuple[int, int]] = [] + weights: list[float] = [] + seen_edges: set[tuple[int, int]] = set() + + for e in edges: + src_idx = qn_to_idx.get(e.source_qualified) + tgt_idx = qn_to_idx.get(e.target_qualified) + if src_idx is not None and tgt_idx is not None and src_idx != tgt_idx: + pair = (min(src_idx, tgt_idx), max(src_idx, tgt_idx)) + if pair not in seen_edges: + seen_edges.add(pair) + edge_list.append(pair) + weights.append(EDGE_WEIGHTS.get(e.kind, 0.5)) + + if not edge_list: + return _detect_file_based(nodes, edges, min_size, adj=adj) + + g.add_edges(edge_list) + g.es["weight"] = weights + + # Run Leiden -- scale resolution inversely with graph size to get + # coarser clusters on large repos. Default resolution=1.0 produces + # thousands of tiny communities for 30k+ node graphs. + import math + n_nodes = g.vcount() + resolution = max(0.05, 1.0 / math.log10(max(n_nodes, 10))) + + logger.info( + "Running Leiden on %d nodes, %d edges...", + g.vcount(), g.ecount(), + ) + + import os + seed = int(os.environ.get("CRG_LEIDEN_SEED", _LEIDEN_SEED)) + # Deterministic seeding for benchmark reproducibility — community + # detection is not a security-sensitive context. nosec B311. + ig.set_random_number_generator(random.Random(seed)) # nosec B311 + partition = g.community_leiden( + objective_function="modularity", + weights="weight", + resolution=resolution, + n_iterations=2, + ) + + logger.info( + "Leiden complete, found %d partitions. Computing cohesion...", + len(partition), + ) + + pending: list[tuple[list[GraphNode], set[str]]] = [] + for cluster_ids in partition: + if len(cluster_ids) < min_size: + continue + members = [idx_to_node[i] for i in cluster_ids if i in idx_to_node] + if len(members) < min_size: + continue + member_qns = {m.qualified_name for m in members} + pending.append((members, member_qns)) + + cohesions = _compute_cohesion_batch([p[1] for p in pending], edges) + + communities: list[dict[str, Any]] = [] + for (members, member_qns), cohesion in zip(pending, cohesions): + lang_counts = Counter(m.language for m in members if m.language) + dominant_lang = lang_counts.most_common(1)[0][0] if lang_counts else "" + name = _generate_community_name(members) + + communities.append({ + "name": name, + "level": 0, + "size": len(members), + "cohesion": round(cohesion, 4), + "dominant_language": dominant_lang, + "description": f"Community of {len(members)} nodes", + "members": [m.qualified_name for m in members], + "member_qns": member_qns, + }) + + logger.info("Community detection complete: %d communities", len(communities)) + return communities + + +# --------------------------------------------------------------------------- +# File-based fallback community detection +# --------------------------------------------------------------------------- + + +def _detect_file_based( + nodes: list[GraphNode], + edges: list[GraphEdge], + min_size: int, + adj: dict[str, list[str]] | None = None, +) -> list[dict[str, Any]]: + """Group nodes by directory when Leiden is unavailable or over-fragments. + + Strips the longest common directory prefix from all file paths, then + adaptively picks a grouping depth that yields 10-200 communities. + """ + # Collect all directory paths (normalized, without filename) + all_dir_parts: list[list[str]] = [] + for n in nodes: + parts = n.file_path.replace("\\", "/").split("/") + all_dir_parts.append([p for p in parts[:-1] if p]) + + # Find the longest common prefix among directory parts + prefix_len = 0 + if all_dir_parts: + shortest = min(len(p) for p in all_dir_parts) + for i in range(shortest): + seg = all_dir_parts[0][i] + if all(p[i] == seg for p in all_dir_parts): + prefix_len = i + 1 + else: + break + + def _group_at_depth(depth: int) -> dict[str, list[GraphNode]]: + groups: dict[str, list[GraphNode]] = defaultdict(list) + for n in nodes: + parts = n.file_path.replace("\\", "/").split("/") + dir_parts = [p for p in parts[:-1] if p] + remainder = dir_parts[prefix_len:] + if remainder: + key = "/".join(remainder[:depth]) + else: + key = parts[-1].rsplit(".", 1)[0] if parts else "root" + groups[key].append(n) + return groups + + # Try increasing depths until we get 10-200 qualifying groups + max_depth = max((len(p) - prefix_len for p in all_dir_parts), default=0) + best_groups = _group_at_depth(1) # depth=1 always works (file stem fallback) + for depth in range(1, max_depth + 1): + groups = _group_at_depth(depth) + qualifying = sum(1 for v in groups.values() if len(v) >= min_size) + best_groups = groups + if qualifying >= 10: + break + + by_dir = best_groups + + # Pre-filter to communities meeting min_size and collect their member + # sets so we can batch-compute all cohesions in a single O(edges) pass. + # Without this, per-community cohesion is O(edges * files), which makes + # community detection effectively hang on large repos. + pending: list[tuple[str, list[GraphNode], set[str]]] = [] + for dir_path, members in by_dir.items(): + if len(members) < min_size: + continue + member_qns = {m.qualified_name for m in members} + pending.append((dir_path, members, member_qns)) + + cohesions = _compute_cohesion_batch([p[2] for p in pending], edges) + + communities: list[dict[str, Any]] = [] + for (dir_path, members, member_qns), cohesion in zip(pending, cohesions): + lang_counts = Counter(m.language for m in members if m.language) + dominant_lang = lang_counts.most_common(1)[0][0] if lang_counts else "" + name = _generate_community_name(members) + + communities.append({ + "name": name, + "level": 0, + "size": len(members), + "cohesion": round(cohesion, 4), + "dominant_language": dominant_lang, + "description": f"Directory-based community: {dir_path}", + "members": [m.qualified_name for m in members], + "member_qns": member_qns, + }) + + return communities + + +# --------------------------------------------------------------------------- +# Oversized community splitting +# --------------------------------------------------------------------------- + + +def _split_oversized( + communities: list[dict], + nodes: list[GraphNode], + edges: list[GraphEdge], + threshold_pct: float = 0.25, + min_split_size: int = 10, +) -> list[dict]: + """Recursively split communities that exceed threshold_pct of total. + + Uses Leiden on the subgraph of oversized communities. If igraph is + not available, returns communities unchanged. + """ + if not IGRAPH_AVAILABLE: + return communities + + total = sum( + c.get("size", len(c.get("members", []))) + for c in communities + ) + if total == 0: + return communities + + threshold = max(int(total * threshold_pct), min_split_size) + result: list[dict] = [] + next_id = max( + (c.get("id", 0) for c in communities), default=0 + ) + 1 + + for comm in communities: + members = set(comm.get("members", [])) + if len(members) <= threshold: + result.append(comm) + continue + + # Build subgraph for this community + member_nodes = [ + n for n in nodes + if n.qualified_name in members + ] + member_edges = [ + e for e in edges + if ( + e.source_qualified in members + and e.target_qualified in members + ) + ] + + if len(member_nodes) < min_split_size: + result.append(comm) + continue + + # Run Leiden on subgraph + qn_to_idx = { + n.qualified_name: i + for i, n in enumerate(member_nodes) + } + ig_edges: list[tuple[int, int]] = [] + ig_weights: list[float] = [] + for e in member_edges: + si = qn_to_idx.get(e.source_qualified) + ti = qn_to_idx.get(e.target_qualified) + if si is not None and ti is not None and si != ti: + ig_edges.append((si, ti)) + ig_weights.append( + EDGE_WEIGHTS.get(e.kind, 0.5) + ) + + if not ig_edges: + result.append(comm) + continue + + try: + g = ig.Graph( + n=len(member_nodes), + edges=ig_edges, + directed=False, + ) + g.es["weight"] = ig_weights + import os + seed = int(os.environ.get("CRG_LEIDEN_SEED", _LEIDEN_SEED)) + # Deterministic seeding for benchmark reproducibility — community + # detection is not a security-sensitive context. nosec B311. + ig.set_random_number_generator(random.Random(seed)) # nosec B311 + partition = g.community_leiden( + objective_function="modularity", + weights="weight", + resolution=0.5, + ) + + sub_communities: dict[int, list[str]] = {} + for idx, cid in enumerate(partition.membership): + sub_communities.setdefault(cid, []).append( + member_nodes[idx].qualified_name + ) + + if len(sub_communities) <= 1: + result.append(comm) + continue + + parent_id = comm.get("id", 0) + comm_name = comm.get("name", "") + for sub_members in sub_communities.values(): + sub_comm = { + "id": next_id, + "name": comm_name + f"-sub{next_id}", + "level": comm.get("level", 0) + 1, + "parent_id": parent_id, + "members": sub_members, + "size": len(sub_members), + "cohesion": 0.0, + "dominant_language": comm.get( + "dominant_language" + ), + "description": ( + f"Split from {comm_name}" + ), + } + result.append(sub_comm) + next_id += 1 + + logger.info( + "Split oversized community '%s' " + "(%d members) into %d", + comm_name, + len(members), + len(sub_communities), + ) + except Exception: + logger.warning( + "Failed to split community '%s', " + "keeping as-is", + comm.get("name", ""), + exc_info=True, + ) + result.append(comm) + + return result + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- + + +def detect_communities( + store: GraphStore, min_size: int = 2 +) -> list[dict[str, Any]]: + """Detect communities in the code graph. + + Uses the Leiden algorithm via igraph if available, otherwise falls back to + file-based grouping. + + Args: + store: The GraphStore instance. + min_size: Minimum number of nodes for a community to be included. + + Returns: + List of community dicts with keys: name, level, size, cohesion, + dominant_language, description, members, member_qns. + """ + # Gather all nodes (exclude File nodes to focus on code entities) + all_edges = store.get_all_edges() + unique_nodes = store.get_all_nodes(exclude_files=True) + + # Build adjacency index once for fast cohesion computation + adj = _build_adjacency(all_edges) + + logger.info( + "Loaded %d unique nodes, %d edges", + len(unique_nodes), len(all_edges), + ) + + if IGRAPH_AVAILABLE: + logger.info("Detecting communities with Leiden algorithm (igraph)") + results = _detect_leiden(unique_nodes, all_edges, min_size, adj=adj) + else: + logger.info("igraph not available, using file-based community detection") + results = _detect_file_based(unique_nodes, all_edges, min_size, adj=adj) + + # Split oversized communities + results = _split_oversized( + results, unique_nodes, all_edges, + ) + + # Convert member_qns (internal set) to a list for serialization safety, + # then strip it from the returned dicts to avoid leaking internal state. + for comm in results: + if "member_qns" in comm: + comm["member_qns"] = list(comm["member_qns"]) + del comm["member_qns"] + + return results + + +def incremental_detect_communities( + store: GraphStore, + changed_files: list[str], + min_size: int = 2, +) -> int: + """Re-detect communities only if changed files affect existing communities. + + If no existing communities contain nodes from changed files, skips + re-detection entirely (the common case for small changes). Otherwise + re-runs full community detection. + + Args: + store: The GraphStore instance. + changed_files: List of file paths that have changed. + min_size: Minimum number of nodes for a community to be included. + + Returns: + Number of communities detected, or 0 if skipped. + """ + if not changed_files: + return 0 + + conn = store._conn + + # Check if any communities are affected (batch to stay under SQLite limit) + affected_count = 0 + for i in range(0, len(changed_files), _SQL_BATCH): + batch = changed_files[i:i + _SQL_BATCH] + placeholders = ",".join("?" * len(batch)) + row = conn.execute( + f"SELECT COUNT(DISTINCT community_id) FROM nodes " # nosec B608 + f"WHERE community_id IS NOT NULL AND file_path IN ({placeholders})", + batch, + ).fetchone() + if row: + affected_count += row[0] + affected = (affected_count,) if affected_count else None + + if not affected or affected[0] == 0: + return 0 # No communities affected, skip + + # Re-run full community detection (correct and fast enough) + communities = detect_communities(store, min_size=min_size) + return store_communities(store, communities) + + +def store_communities( + store: GraphStore, communities: list[dict[str, Any]] +) -> int: + """Store detected communities in the database. + + Clears existing communities and community_id assignments, then inserts + the new communities and updates node community_id references. + + Args: + store: The GraphStore instance. + communities: List of community dicts from detect_communities(). + + Returns: + Number of communities stored. + """ + # NOTE: store_communities uses _conn directly because it performs + # multi-statement batch writes (DELETE + INSERT loop + UPDATE loop) + # that are tightly coupled to the DB transaction lifecycle. + conn = store._conn + + if conn.in_transaction: + logger.warning("Rolling back uncommitted transaction before BEGIN IMMEDIATE") + conn.rollback() + # Wrap in explicit transaction so the DELETE + INSERT + UPDATE + # sequence is atomic — no partial community data on crash. + conn.execute("BEGIN IMMEDIATE") + try: + conn.execute("DELETE FROM communities") + conn.execute("UPDATE nodes SET community_id = NULL") + + count = 0 + for comm in communities: + cursor = conn.execute( + """INSERT INTO communities + (name, level, cohesion, size, dominant_language, description) + VALUES (?, ?, ?, ?, ?, ?)""", + ( + comm["name"], + comm.get("level", 0), + comm.get("cohesion", 0.0), + comm["size"], + comm.get("dominant_language", ""), + comm.get("description", ""), + ), + ) + community_id = cursor.lastrowid + + # Batch update community_id on member nodes + member_qns = comm.get("members", []) + for j in range(0, len(member_qns), _SQL_BATCH): + batch = member_qns[j:j + _SQL_BATCH] + placeholders = ",".join("?" * len(batch)) + conn.execute( + f"UPDATE nodes SET community_id = ? WHERE qualified_name IN ({placeholders})", # nosec B608 + [community_id] + batch, + ) + count += 1 + + conn.commit() + except BaseException: + conn.rollback() + raise + return count + + +def get_communities( + store: GraphStore, sort_by: str = "size", min_size: int = 0 +) -> list[dict[str, Any]]: + """Retrieve stored communities from the database. + + Args: + store: The GraphStore instance. + sort_by: Column to sort by ("size", "cohesion", "name"). + min_size: Minimum community size to include. + + Returns: + List of community dicts. + """ + valid_sorts = {"size", "cohesion", "name"} + if sort_by not in valid_sorts: + sort_by = "size" + + order = "DESC" if sort_by in ("size", "cohesion") else "ASC" + + # NOTE: get_communities reads the communities table which has no + # dedicated GraphStore method (it's a domain-specific table managed + # entirely by the communities module). We use _conn for this query. + rows = store._conn.execute( + f"SELECT * FROM communities WHERE size >= ? ORDER BY {sort_by} {order}", # nosec B608 + (min_size,), + ).fetchall() + + communities: list[dict[str, Any]] = [] + for row in rows: + # Fetch member qualified names for this community + member_qns = [ + _sanitize_name(qn) + for qn in store.get_community_member_qns(row["id"]) + ] + + communities.append({ + "id": row["id"], + "name": _sanitize_name(row["name"]), + "level": row["level"], + "cohesion": row["cohesion"], + "size": row["size"], + "dominant_language": row["dominant_language"] or "", + "description": _sanitize_name(row["description"] or ""), + "members": member_qns, + }) + + return communities + + +_TEST_COMMUNITY_RE = re.compile( + r"(^test[-/]|[-/]test([:/]|$)|it:should|describe:|spec[-/]|[-/]spec$)", + re.IGNORECASE, +) + + +def _is_test_community(name: str) -> bool: + """Return True if a community name indicates it is test-dominated.""" + return bool(_TEST_COMMUNITY_RE.search(name)) + + +def get_architecture_overview(store: GraphStore) -> dict[str, Any]: + """Generate an architecture overview based on community structure. + + Builds a node-to-community mapping, counts cross-community edges, + and generates warnings for high coupling. + + Args: + store: The GraphStore instance. + + Returns: + Dict with keys: communities, cross_community_edges, warnings. + """ + communities = get_communities(store) + + # Build node -> community_id mapping + node_to_community: dict[str, int] = {} + for comm in communities: + comm_id = comm.get("id", 0) + for qn in comm.get("members", []): + node_to_community[qn] = comm_id + + # Count cross-community edges + all_edges = store.get_all_edges() + cross_edges: list[dict[str, Any]] = [] + cross_counts: Counter[tuple[int, int]] = Counter() + + for e in all_edges: + # TESTED_BY edges are expected cross-community coupling (test → code), + # not an architectural smell. + if e.kind == "TESTED_BY": + continue + src_comm = node_to_community.get(e.source_qualified) + tgt_comm = node_to_community.get(e.target_qualified) + if ( + src_comm is not None + and tgt_comm is not None + and src_comm != tgt_comm + ): + pair = (min(src_comm, tgt_comm), max(src_comm, tgt_comm)) + cross_counts[pair] += 1 + cross_edges.append({ + "source_community": src_comm, + "target_community": tgt_comm, + "edge_kind": e.kind, + "source": _sanitize_name(e.source_qualified), + "target": _sanitize_name(e.target_qualified), + }) + + # Generate warnings for high coupling, skipping test-dominated pairs. + warnings: list[str] = [] + comm_name_map = {c.get("id", 0): c["name"] for c in communities} + for (c1, c2), count in cross_counts.most_common(): + if count > 10: + name1 = comm_name_map.get(c1, f"community-{c1}") + name2 = comm_name_map.get(c2, f"community-{c2}") + # Skip pairs where either community is test-dominated — coupling + # between test and production code is expected, not architectural. + if _is_test_community(name1) or _is_test_community(name2): + continue + warnings.append( + f"High coupling ({count} edges) between " + f"'{name1}' and '{name2}'" + ) + + return { + "communities": communities, + "cross_community_edges": cross_edges, + "warnings": warnings, + } diff --git a/code_review_graph/constants.py b/code_review_graph/constants.py new file mode 100644 index 0000000..2985a5e --- /dev/null +++ b/code_review_graph/constants.py @@ -0,0 +1,23 @@ +"""Shared constants for code-review-graph.""" + +from __future__ import annotations + +import os + +SECURITY_KEYWORDS: frozenset[str] = frozenset({ + "auth", "login", "password", "token", "session", "crypt", "secret", + "credential", "permission", "sql", "query", "execute", "connect", + "socket", "request", "http", "sanitize", "validate", "encrypt", + "decrypt", "hash", "sign", "verify", "admin", "privilege", +}) + +# --------------------------------------------------------------------------- +# Configurable limits (override via environment variables) +# --------------------------------------------------------------------------- +MAX_IMPACT_NODES = int(os.environ.get("CRG_MAX_IMPACT_NODES", "500")) +MAX_IMPACT_DEPTH = int(os.environ.get("CRG_MAX_IMPACT_DEPTH", "2")) +MAX_BFS_DEPTH = int(os.environ.get("CRG_MAX_BFS_DEPTH", "15")) +MAX_SEARCH_RESULTS = int(os.environ.get("CRG_MAX_SEARCH_RESULTS", "20")) + +# BFS engine: "sql" (SQLite recursive CTE) or "networkx" (Python-side BFS) +BFS_ENGINE = os.environ.get("CRG_BFS_ENGINE", "sql") diff --git a/code_review_graph/context_savings.py b/code_review_graph/context_savings.py new file mode 100644 index 0000000..eebeca9 --- /dev/null +++ b/code_review_graph/context_savings.py @@ -0,0 +1,317 @@ +"""Compact estimated context savings helpers. + +The project intentionally labels these values as estimates: the helper uses a +conservative character-count approximation instead of model-specific tokenizers. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any, Iterable + +CHARS_PER_TOKEN = 4 + + +def estimate_tokens(value: Any) -> int: + """Estimate token count with a conservative 4 chars/token approximation.""" + if value is None: + return 0 + if isinstance(value, str): + text = value + else: + text = json.dumps( + value, + default=str, + ensure_ascii=True, + separators=(",", ":"), + sort_keys=True, + ) + if not text: + return 0 + return max(1, (len(text) + CHARS_PER_TOKEN - 1) // CHARS_PER_TOKEN) + + +def estimate_file_tokens(repo_root: Path, files: Iterable[str]) -> int: + """Estimate tokens for changed files using file sizes, not file contents.""" + total = 0 + root = repo_root.resolve() + for file_name in files: + path = Path(file_name) + full_path = path if path.is_absolute() else root / path + try: + if full_path.is_file(): + total += max( + 1, + (full_path.stat().st_size + CHARS_PER_TOKEN - 1) + // CHARS_PER_TOKEN, + ) + except OSError: + continue + return total + + +def estimate_context_savings( + *, + original_context: Any | None = None, + returned_context: Any | None = None, + original_tokens: int | None = None, + returned_tokens: int | None = None, +) -> dict[str, int | bool] | None: + """Return tiny savings metadata, or None when no baseline is available.""" + baseline = ( + original_tokens + if original_tokens is not None + else estimate_tokens(original_context) + ) + returned = ( + returned_tokens + if returned_tokens is not None + else estimate_tokens(returned_context) + ) + + if baseline <= 0: + return None + + saved = max(0, baseline - returned) + percent = round((saved / baseline) * 100) if baseline else 0 + return { + "estimated": True, + "saved_tokens": int(saved), + "saved_percent": int(percent), + } + + +def attach_context_savings( + result: dict[str, Any], + *, + original_context: Any | None = None, + original_tokens: int | None = None, + returned_context: Any | None = None, + returned_tokens: int | None = None, +) -> dict[str, Any]: + """Attach compact ``context_savings`` metadata when it can be estimated.""" + estimate = estimate_context_savings( + original_context=original_context, + returned_context=result if returned_context is None else returned_context, + original_tokens=original_tokens, + returned_tokens=returned_tokens, + ) + if estimate is not None: + result["context_savings"] = estimate + return result + + +def format_context_savings(estimate: dict[str, Any] | None) -> str | None: + """Format a one-line human summary for CLI output.""" + if not estimate: + return None + saved = int(estimate.get("saved_tokens", 0)) + percent = int(estimate.get("saved_percent", 0)) + return f"Estimated context saved: ~{saved:,} tokens (~{percent}%)" + + +def _fmt_compact(n: int) -> str: + """Compact integer formatting: 1234 -> '1.2k', 9876 -> '9.9k', 500 -> '500'.""" + if n >= 10_000: + return f"{n // 1000:,}k" + if n >= 1000: + return f"{n / 1000:.1f}k" + return str(n) + + +def _breakdown_from_response(response: dict[str, Any]) -> dict[str, int]: + """Pull a per-category token estimate from a detect-changes / review response. + + Only fields that exist and have content are reported, so the breakdown + line stays meaningful instead of padding with zeros. + """ + # Friendly label -> response-dict key + fields = [ + ("Functions", "changed_functions"), + ("Flows", "affected_flows"), + ("Tests", "test_gaps"), + ("Risk", "review_priorities"), + ("Impact", "impacted_nodes"), + ("Edges", "edges"), + ("Source", "source_snippets"), + ("Imports", "imports"), + ] + out: dict[str, int] = {} + for label, key in fields: + value = response.get(key) + if not value: + continue + tokens = estimate_tokens(value) + if tokens > 0: + out[label] = tokens + return out + + +def verify_with_tiktoken( + repo_root: "Path | str", + changed_files: Iterable[str], + response: Any, + encoding_name: str = "cl100k_base", +) -> dict[str, int] | None: + """Calibrate the chars/4 estimate against a real model tokenizer. + + Returns ``{"verified_baseline": int, "verified_returned": int, + "verified_saved": int, "verified_percent": int}`` or ``None`` if + tiktoken is not installed. Reads every changed file's content (unlike + the stat-only ``estimate_file_tokens``) so the numbers reflect what + an agent would actually consume. + """ + try: + import tiktoken # type: ignore[import-untyped] + except ImportError: + return None + + enc = tiktoken.get_encoding(encoding_name) + root = Path(repo_root).resolve() + + naive_real = 0 + for f in changed_files: + p = root / f + try: + if p.is_file(): + naive_real += len(enc.encode(p.read_text(errors="replace"))) + except OSError: + continue + + if isinstance(response, str): + graph_real = len(enc.encode(response)) + else: + text = json.dumps( + response, default=str, ensure_ascii=True, + separators=(",", ":"), sort_keys=True, + ) + graph_real = len(enc.encode(text)) + + saved = max(0, naive_real - graph_real) + pct = round(saved * 100 / naive_real) if naive_real > 0 else 0 + return { + "verified_baseline": naive_real, + "verified_returned": graph_real, + "verified_saved": saved, + "verified_percent": pct, + } + + +def format_context_savings_panel( + estimate: dict[str, Any] | None, + *, + original_tokens: int | None = None, + returned_tokens: int | None = None, + response: dict[str, Any] | None = None, + breakdown: dict[str, int] | None = None, + verified: dict[str, int] | None = None, + title: str = "Token Savings", + width: int = 64, +) -> str | None: + """Format the savings estimate as a boxed multi-line CLI panel. + + Example output (width=60):: + + ┌──────────────── Token Savings ────────────────┐ + │ Full context would be: 12,932 tokens │ + │ Graph context used: 773 tokens │ + │ Saved: 12,159 tokens (~94%) │ + │ Breakdown: Functions 580 · Tests 120 · ... │ + └───────────────────────────────────────────────┘ + + All numbers are labelled as estimates upstream (``estimated: true`` in the + metadata dict) because the project uses a 4-chars-per-token approximation, + not model-specific tokenization. + + Args: + estimate: The ``context_savings`` dict from a tool response. + original_tokens: Optional override for the naive baseline. + returned_tokens: Optional override for the graph response size. + response: When provided, breakdown is auto-derived from common keys + (``changed_functions``, ``affected_flows``, ``test_gaps``, + ``review_priorities``, ``impacted_nodes``, ``edges``, + ``source_snippets``, ``imports``). + breakdown: Explicit ``{label: tokens}`` map; takes precedence over + ``response``-derived breakdown when both are provided. + title: Title centered in the top border. + width: Total panel width, capped at terminal width if larger. + + Returns: + The panel as a single ``\\n``-joined string, or ``None`` when there + is nothing meaningful to display. + """ + if not estimate: + return None + + saved = int(estimate.get("saved_tokens", 0)) + percent = int(estimate.get("saved_percent", 0)) + + # Derive baseline + returned from saved+percent if not provided + if original_tokens is None: + if percent > 0: + original_tokens = int(round(saved * 100 / percent)) + else: + original_tokens = saved + if returned_tokens is None: + returned_tokens = max(0, (original_tokens or 0) - saved) + + if breakdown is None and response is not None: + breakdown = _breakdown_from_response(response) + + # Top up the breakdown with an "Other" bucket so the parts sum to + # ``returned_tokens`` exactly. "Other" covers fields the breakdown + # doesn't enumerate (status, summary, risk_score, context_savings + # metadata, JSON envelope chars). Skip when there's no positive + # remainder — the breakdown already accounts for the whole response. + if breakdown and returned_tokens is not None: + labelled_sum = sum(breakdown.values()) + remainder = returned_tokens - labelled_sum + if remainder > 0: + breakdown = dict(breakdown) # copy before mutating + breakdown["Other"] = remainder + + # Lines that go inside the box (without borders) + inner_lines: list[str] = [ + f"Full context would be: {original_tokens:>9,} tokens", + f"Graph context used: {returned_tokens:>9,} tokens", + f"Saved: {saved:>9,} tokens (~{percent}%)", + ] + if verified: + vb = verified["verified_baseline"] + vr = verified["verified_returned"] + vs = verified["verified_saved"] + vp = verified["verified_percent"] + inner_lines.append( + f"Verified (tiktoken): {vs:>9,} tokens (~{vp}%) " + f"[{vb:,} → {vr:,}]" + ) + if breakdown: + parts = [f"{label} {_fmt_compact(tok)}" for label, tok in breakdown.items()] + bd_line = "Breakdown: " + " · ".join(parts) + inner_lines.append(bd_line) + + # Compute final width: at least wide enough for the longest inner line + padding + content_width = max(len(s) for s in inner_lines) + inner_w = max(width - 2, content_width + 2) # +2 for one space pad each side + # Title bar + title_str = f" {title} " + dash_total = inner_w - len(title_str) + if dash_total < 4: + dash_total = 4 + left_dash = dash_total // 2 + right_dash = dash_total - left_dash + top = "┌" + "─" * left_dash + title_str + "─" * right_dash + "┐" + bottom = "└" + "─" * inner_w + "┘" + + def _box_line(content: str) -> str: + pad = inner_w - 2 - len(content) + if pad < 0: + pad = 0 + return f"│ {content}{' ' * pad} │" + + lines = [top] + for s in inner_lines: + lines.append(_box_line(s)) + lines.append(bottom) + return "\n".join(lines) diff --git a/code_review_graph/custom_languages.py b/code_review_graph/custom_languages.py new file mode 100644 index 0000000..0f63a54 --- /dev/null +++ b/code_review_graph/custom_languages.py @@ -0,0 +1,322 @@ +"""Config-driven custom language support ("bring your own language"). + +Repos can teach the parser new tree-sitter languages without forking by +dropping a ``languages.toml`` file into ``.code-review-graph/``:: + + [languages.erlang] + extensions = [".erl", ".hrl"] + grammar = "erlang" # tree_sitter_language_pack name + function_node_types = ["function_clause"] + class_node_types = ["record_decl"] + import_node_types = ["import_attribute"] + call_node_types = ["call"] + comment = "Erlang via the bundled tree-sitter-erlang grammar" + +The loader is deliberately defensive: a broken config must never crash a +build. Invalid entries are skipped with a ``logger.warning``, and built-in +languages always win — custom entries can neither override built-in file +extensions nor reuse built-in language names. At most +``MAX_CUSTOM_LANGUAGES`` entries are honoured per repo. + +See docs/CUSTOM_LANGUAGES.md for the full schema reference (answers #320). +""" + +from __future__ import annotations + +import logging +import re +import sys +import threading +from collections.abc import Mapping +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import tree_sitter_language_pack as tslp + +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib # type: ignore[no-redef] + except ImportError: + tomllib = None # type: ignore[assignment] + +logger = logging.getLogger(__name__) + +#: Location of the config file, relative to the repo root. +CONFIG_RELATIVE_PATH = Path(".code-review-graph") / "languages.toml" + +#: Hard cap on the number of custom languages loaded from a single config. +MAX_CUSTOM_LANGUAGES = 20 + +#: Custom language names: short lowercase identifiers. The name becomes the +#: ``language`` field on every node parsed from matching files. +_NAME_RE = re.compile(r"^[a-z][a-z0-9_-]{0,31}$") + +#: Extensions: a leading dot followed by 1-15 safe characters (".erl", +#: ".cls", ".4gl"). Uppercase input is normalised to lowercase because the +#: parser lowercases file suffixes before lookup. +_EXTENSION_RE = re.compile(r"^\.[a-z0-9_+-]{1,15}$") + +#: The four node-type lists recognised in each ``[languages.]`` table. +_NODE_TYPE_KEYS = ( + "function_node_types", + "class_node_types", + "import_node_types", + "call_node_types", +) + + +@dataclass(frozen=True) +class CustomLanguage: + """One validated ``[languages.]`` entry from languages.toml.""" + + name: str + grammar: str + extensions: tuple[str, ...] + function_node_types: tuple[str, ...] = () + class_node_types: tuple[str, ...] = () + import_node_types: tuple[str, ...] = () + call_node_types: tuple[str, ...] = () + comment: str = "" + + +@dataclass(frozen=True) +class _CacheEntry: + mtime_ns: int + size: int + languages: dict[str, CustomLanguage] = field(default_factory=dict) + + +# Config files are re-read only when their mtime/size changes. This matters +# because full builds construct one CodeParser per worker task, and probing +# tree-sitter grammars on every file parse would be wasteful. +_cache_lock = threading.Lock() +_cache: dict[str, _CacheEntry] = {} + + +def clear_cache() -> None: + """Drop the loader cache (used by tests).""" + with _cache_lock: + _cache.clear() + + +def load_custom_languages( + repo_root: Path, + *, + builtin_extensions: Mapping[str, str], + builtin_languages: frozenset[str], +) -> dict[str, CustomLanguage]: + """Load and validate ``/.code-review-graph/languages.toml``. + + Returns a mapping of custom language name -> :class:`CustomLanguage`. + Always returns (possibly empty) — a broken config never raises. + + Args: + repo_root: Repository root containing ``.code-review-graph/``. + builtin_extensions: The parser's built-in extension map; custom + entries colliding with these are skipped (built-ins win). + builtin_languages: All built-in language identifiers; custom names + shadowing these are skipped. + """ + config_path = Path(repo_root) / CONFIG_RELATIVE_PATH + try: + stat = config_path.stat() + except OSError: + return {} # No config file — the common case; not worth a log line. + + cache_key = str(config_path) + with _cache_lock: + cached = _cache.get(cache_key) + if ( + cached is not None + and cached.mtime_ns == stat.st_mtime_ns + and cached.size == stat.st_size + ): + return dict(cached.languages) + + languages = _load_uncached(config_path, builtin_extensions, builtin_languages) + with _cache_lock: + _cache[cache_key] = _CacheEntry(stat.st_mtime_ns, stat.st_size, dict(languages)) + return languages + + +def _load_uncached( + config_path: Path, + builtin_extensions: Mapping[str, str], + builtin_languages: frozenset[str], +) -> dict[str, CustomLanguage]: + if tomllib is None: + logger.warning( + "%s found but TOML parsing requires the 'tomli' package on " + "Python < 3.11 — no custom languages loaded", + config_path, + ) + return {} + try: + raw = config_path.read_bytes() + except (OSError, PermissionError) as exc: + logger.warning("Cannot read %s: %s — no custom languages loaded", config_path, exc) + return {} + try: + data = tomllib.loads(raw.decode("utf-8", errors="replace")) + except tomllib.TOMLDecodeError as exc: + logger.warning("Malformed TOML in %s: %s — no custom languages loaded", config_path, exc) + return {} + + tables = data.get("languages") + if tables is None: + return {} + if not isinstance(tables, dict): + logger.warning( + "%s: [languages] must be a table of tables — no custom languages loaded", + config_path, + ) + return {} + + result: dict[str, CustomLanguage] = {} + claimed_extensions: set[str] = set() + for name, table in tables.items(): + if len(result) >= MAX_CUSTOM_LANGUAGES: + logger.warning( + "%s defines more than %d custom languages — ignoring the rest", + config_path, MAX_CUSTOM_LANGUAGES, + ) + break + lang = _validate_entry( + name, table, builtin_extensions, builtin_languages, + claimed_extensions, config_path, + ) + if lang is None: + continue + result[lang.name] = lang + claimed_extensions.update(lang.extensions) + return result + + +def _validate_entry( + name: object, + table: object, + builtin_extensions: Mapping[str, str], + builtin_languages: frozenset[str], + claimed_extensions: set[str], + config_path: Path, +) -> Optional[CustomLanguage]: + """Validate one ``[languages.]`` table; None (after a warning) on + any problem so a bad entry can never break a build.""" + label = name if isinstance(name, str) else repr(name) + if not isinstance(table, dict): + logger.warning("%s: [languages.%s] is not a table — skipping", config_path, label) + return None + if not isinstance(name, str) or not _NAME_RE.match(name): + logger.warning( + "%s: invalid custom language name %r (expected lowercase " + "letters/digits/_/-, max 32 chars) — skipping", + config_path, label, + ) + return None + if name in builtin_languages: + logger.warning( + "%s: custom language %r shadows a built-in language — skipping " + "(built-ins cannot be overridden)", + config_path, name, + ) + return None + + grammar = table.get("grammar") + if not isinstance(grammar, str) or not grammar.strip(): + logger.warning( + "%s: custom language %r needs a non-empty 'grammar' string — skipping", + config_path, name, + ) + return None + grammar = grammar.strip() + + raw_extensions = table.get("extensions") + if not isinstance(raw_extensions, list) or not raw_extensions: + logger.warning( + "%s: custom language %r needs a non-empty 'extensions' list — skipping", + config_path, name, + ) + return None + extensions: list[str] = [] + for ext in raw_extensions: + normalized = ext.strip().lower() if isinstance(ext, str) else "" + if not normalized.startswith("."): + logger.warning( + "%s: custom language %r: extension %r must start with a dot — skipping", + config_path, name, ext, + ) + return None + if not _EXTENSION_RE.match(normalized): + logger.warning( + "%s: custom language %r: extension %r is not a valid file " + "extension — skipping", + config_path, name, ext, + ) + return None + if normalized in builtin_extensions: + logger.warning( + "%s: custom language %r: extension %r is already handled by " + "the built-in %r parser — skipping (built-ins cannot be overridden)", + config_path, name, normalized, builtin_extensions[normalized], + ) + return None + if normalized in claimed_extensions: + logger.warning( + "%s: custom language %r: extension %r is already claimed by " + "an earlier custom language — skipping", + config_path, name, normalized, + ) + return None + if normalized not in extensions: + extensions.append(normalized) + + node_types: dict[str, tuple[str, ...]] = {} + for key in _NODE_TYPE_KEYS: + value = table.get(key, []) + if not isinstance(value, list) or any( + not isinstance(item, str) or not item.strip() for item in value + ): + logger.warning( + "%s: custom language %r: %s must be a list of non-empty " + "strings — skipping", + config_path, name, key, + ) + return None + node_types[key] = tuple(item.strip() for item in value) + if not any(node_types.values()): + logger.warning( + "%s: custom language %r defines no node types — nothing to " + "extract, skipping", + config_path, name, + ) + return None + + comment = table.get("comment", "") + if not isinstance(comment, str): + comment = "" + + # Probe the grammar last (it is the expensive check). Parser objects + # themselves are created lazily by CodeParser._get_parser. + try: + tslp.get_language(grammar) # type: ignore[arg-type] + except (LookupError, ValueError, ImportError, OSError) as exc: + logger.warning( + "%s: custom language %r: grammar %r is not available in " + "tree_sitter_language_pack (%s) — skipping", + config_path, name, grammar, exc, + ) + return None + + return CustomLanguage( + name=name, + grammar=grammar, + extensions=tuple(extensions), + function_node_types=node_types["function_node_types"], + class_node_types=node_types["class_node_types"], + import_node_types=node_types["import_node_types"], + call_node_types=node_types["call_node_types"], + comment=comment, + ) diff --git a/code_review_graph/daemon.py b/code_review_graph/daemon.py new file mode 100644 index 0000000..cdbdd54 --- /dev/null +++ b/code_review_graph/daemon.py @@ -0,0 +1,1009 @@ +"""Multi-repo watch daemon for code-review-graph. + +Reads ``~/.code-review-graph/watch.toml`` to configure which repositories +to watch, then spawns one ``code-review-graph watch`` child process per +repo. Monitors the config file for live changes (adding/removing repos) +and health-checks child processes, restarting any that die. + +No external dependencies beyond Python stdlib — no tmux required. +""" + +from __future__ import annotations + +import json +import logging +import os +import shutil +import signal +import subprocess +import sys +import threading +import time +from collections.abc import Callable +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomli as tomllib # type: ignore[no-redef] + except ImportError: + tomllib = None # type: ignore[assignment] + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Config file location +# --------------------------------------------------------------------------- + +CONFIG_PATH: Path = Path.home() / ".code-review-graph" / "watch.toml" +PID_PATH: Path = Path.home() / ".code-review-graph" / "daemon.pid" +STATE_PATH: Path = Path.home() / ".code-review-graph" / "daemon-state.json" +_HEALTH_CHECK_INTERVAL = 30 + +# --------------------------------------------------------------------------- +# Dataclasses +# --------------------------------------------------------------------------- + + +@dataclass +class WatchRepo: + """A single repository to watch.""" + + path: str + """Resolved absolute path to the repository root.""" + + alias: str + """Short name for this repo (derived from directory name when not specified).""" + + +@dataclass +class DaemonConfig: + """Top-level daemon configuration.""" + + session_name: str = "crg-watch" + """Logical daemon name (used in log messages and status output).""" + + log_dir: Path = field(default_factory=lambda: Path.home() / ".code-review-graph" / "logs") + """Directory for per-repo log files.""" + + poll_interval: int = 2 + """Seconds between file-system polls for config changes.""" + + repos: list[WatchRepo] = field(default_factory=list) + """Repositories the daemon watches.""" + + +# --------------------------------------------------------------------------- +# Loading +# --------------------------------------------------------------------------- + + +def load_config(path: Path | None = None) -> DaemonConfig: + """Load daemon configuration from a TOML file. + + Args: + path: Explicit config path. Falls back to :data:`CONFIG_PATH`. + + Returns: + A fully-validated :class:`DaemonConfig`. + + Raises: + RuntimeError: If ``tomllib`` / ``tomli`` is unavailable on Python < 3.11. + """ + if tomllib is None: + raise RuntimeError( + "TOML parsing requires the 'tomli' package on Python < 3.11. " + "Install it with: pip install tomli" + ) + + config_path = path or CONFIG_PATH + + if not config_path.exists(): + logger.info("Config file not found at %s — using defaults", config_path) + return DaemonConfig() + + with open(config_path, "rb") as fh: + raw: dict[str, Any] = tomllib.load(fh) + + # -- [daemon] section --------------------------------------------------- + daemon_section: dict[str, Any] = raw.get("daemon", {}) + session_name: str = daemon_section.get("session_name", "crg-watch") + log_dir = Path(daemon_section.get("log_dir", str(DaemonConfig().log_dir))) + poll_interval: int = int(daemon_section.get("poll_interval", 2)) + + # -- [[repos]] array ---------------------------------------------------- + repos: list[WatchRepo] = [] + seen_aliases: set[str] = set() + + for entry in raw.get("repos", []): + repo_path_str: str = entry.get("path", "") + if not repo_path_str: + logger.warning("Skipping repo entry with empty path") + continue + + repo_path = Path(repo_path_str).expanduser().resolve() + + if not repo_path.is_dir(): + logger.warning("Skipping repo %s — directory does not exist", repo_path) + continue + + has_repo_marker = ( + (repo_path / ".git").exists() + or (repo_path / ".svn").exists() + or (repo_path / ".code-review-graph").exists() + ) + if not has_repo_marker: + logger.warning( + "Skipping repo %s — no .git, .svn, or .code-review-graph directory found", + repo_path, + ) + continue + + alias: str = entry.get("alias", "") or repo_path.name + + if alias in seen_aliases: + logger.warning("Skipping duplicate alias '%s' for repo %s", alias, repo_path) + continue + + seen_aliases.add(alias) + repos.append(WatchRepo(path=str(repo_path), alias=alias)) + + return DaemonConfig( + session_name=session_name, + log_dir=log_dir, + poll_interval=poll_interval, + repos=repos, + ) + + +# --------------------------------------------------------------------------- +# Saving +# --------------------------------------------------------------------------- + + +def _serialize_toml(config: DaemonConfig) -> str: + """Serialize a :class:`DaemonConfig` to TOML text. + + ``tomllib`` is read-only, so we build the TOML manually. + """ + lines: list[str] = [ + "[daemon]", + f'session_name = "{config.session_name}"', + f'log_dir = "{config.log_dir}"', + f"poll_interval = {config.poll_interval}", + ] + for repo in config.repos: + lines.append("") + lines.append("[[repos]]") + lines.append(f'path = "{repo.path}"') + lines.append(f'alias = "{repo.alias}"') + lines.append("") # trailing newline + return "\n".join(lines) + + +def save_config(config: DaemonConfig, path: Path | None = None) -> None: + """Write *config* back to a TOML file. + + Creates parent directories if they do not exist. + + Args: + config: The daemon configuration to persist. + path: Explicit config path. Falls back to :data:`CONFIG_PATH`. + """ + config_path = path or CONFIG_PATH + config_path.parent.mkdir(parents=True, exist_ok=True) + config_path.write_text(_serialize_toml(config), encoding="utf-8") + logger.info("Config saved to %s", config_path) + + +# --------------------------------------------------------------------------- +# Convenience helpers (used by CLI commands) +# --------------------------------------------------------------------------- + + +def add_repo_to_config( + repo_path: str, + alias: str | None = None, + config_path: Path | None = None, +) -> DaemonConfig: + """Add a repository to the daemon config and persist the change. + + Args: + repo_path: Path to the repository (will be resolved to absolute). + alias: Optional short name. Derived from dirname if *None*. + config_path: Explicit config file path. Falls back to :data:`CONFIG_PATH`. + + Returns: + The updated :class:`DaemonConfig`. + + Raises: + ValueError: If the path is not a valid repository directory. + """ + resolved = Path(repo_path).expanduser().resolve() + + if not resolved.is_dir(): + raise ValueError(f"Not a directory: {resolved}") + + has_repo_marker = ( + (resolved / ".git").exists() + or (resolved / ".svn").exists() + or (resolved / ".code-review-graph").exists() + ) + if not has_repo_marker: + raise ValueError(f"No .git, .svn, or .code-review-graph directory in {resolved}") + + effective_alias = alias or resolved.name + + config = load_config(config_path) + + # Check for duplicate path or alias + for existing in config.repos: + if existing.path == str(resolved): + logger.warning("Repo %s is already configured — skipping", resolved) + return config + if existing.alias == effective_alias: + raise ValueError(f"Alias '{effective_alias}' is already in use by {existing.path}") + + config.repos.append(WatchRepo(path=str(resolved), alias=effective_alias)) + save_config(config, config_path) + return config + + +def remove_repo_from_config( + path_or_alias: str, + config_path: Path | None = None, +) -> DaemonConfig: + """Remove a repository from the daemon config by path or alias. + + Args: + path_or_alias: Either the absolute/relative repo path or its alias. + config_path: Explicit config file path. Falls back to :data:`CONFIG_PATH`. + + Returns: + The updated :class:`DaemonConfig`. + """ + config = load_config(config_path) + resolved = str(Path(path_or_alias).expanduser().resolve()) + + original_count = len(config.repos) + config.repos = [r for r in config.repos if r.path != resolved and r.alias != path_or_alias] + + if len(config.repos) == original_count: + logger.warning( + "No repo matching '%s' found in config — nothing removed", + path_or_alias, + ) + else: + save_config(config, config_path) + + return config + + +# --------------------------------------------------------------------------- +# PID file management +# --------------------------------------------------------------------------- + + +def write_pid(pid: int | None = None, path: Path | None = None) -> None: + """Write the current (or given) PID to the PID file.""" + pid_path = path or PID_PATH + pid_path.parent.mkdir(parents=True, exist_ok=True) + pid_path.write_text(str(pid or os.getpid()), encoding="utf-8") + + +def read_pid(path: Path | None = None) -> int | None: + """Read the daemon PID from disk. Returns None if missing/invalid.""" + pid_path = path or PID_PATH + if not pid_path.exists(): + return None + try: + return int(pid_path.read_text(encoding="utf-8").strip()) + except (ValueError, OSError): + return None + + +def clear_pid(path: Path | None = None) -> None: + """Remove the PID file.""" + pid_path = path or PID_PATH + try: + pid_path.unlink(missing_ok=True) + except OSError: + pass + + +# Win32 constants for the OpenProcess-based liveness check (#511). +_PROCESS_QUERY_LIMITED_INFORMATION = 0x1000 +_ERROR_ACCESS_DENIED = 5 +_WAIT_OBJECT_0 = 0x0 + + +def _pid_alive_windows( + pid: int, + kernel32: Any, + get_last_error: Callable[[], int] | None = None, +) -> bool: + """Win32 PID liveness check via OpenProcess/WaitForSingleObject. + + The kernel32 interface is injected so tests can drive handle/wait + outcomes on non-Windows platforms. *get_last_error* defaults to + ``kernel32.GetLastError``; the real caller passes + ``ctypes.get_last_error`` (reliable with ``use_last_error=True``). + """ + if get_last_error is None: + get_last_error = kernel32.GetLastError + handle = kernel32.OpenProcess(_PROCESS_QUERY_LIMITED_INFORMATION, False, pid) + if not handle: + # NULL handle: process is dead, or we lack access. ACCESS_DENIED + # means it exists but is owned by another user — treat as alive. + return get_last_error() == _ERROR_ACCESS_DENIED + try: + # WAIT_OBJECT_0 means the process handle is signaled (it exited). + return kernel32.WaitForSingleObject(handle, 0) != _WAIT_OBJECT_0 + finally: + kernel32.CloseHandle(handle) + + +def pid_alive(pid: int) -> bool: + """Cross-platform check whether a process with *pid* is running. + + On Windows ``os.kill(pid, 0)`` routes to GenerateConsoleCtrlEvent and + raises ``OSError`` (WinError 87) for alive PIDs outside the caller's + console process group (#511), so the Win32 API is used instead. + """ + if sys.platform == "win32": + import ctypes + + kernel32 = ctypes.WinDLL("kernel32", use_last_error=True) + return _pid_alive_windows(pid, kernel32, ctypes.get_last_error) + try: + os.kill(pid, 0) # signal 0 = existence check + return True + except ProcessLookupError: + return False + except PermissionError: + return True # process exists but owned by another user + except OSError as exc: + # Unexpected platform quirk — treat as not alive rather than crash. + logger.debug("PID %d liveness check failed: %s", pid, exc) + return False + + +def is_daemon_running(path: Path | None = None) -> bool: + """Check whether a daemon process is alive.""" + pid = read_pid(path) + if pid is None: + return False + if pid_alive(pid): + return True + # Stale PID file — clean up + clear_pid(path) + return False + + +# --------------------------------------------------------------------------- +# Child state persistence (for cross-process status queries) +# --------------------------------------------------------------------------- + + +def load_state(path: Path | None = None) -> dict[str, Any]: + """Load persisted child process state from disk. + + Returns a dict mapping alias to ``{"pid": int, "path": str}``. + Returns an empty dict if the file is missing or corrupt. + """ + state_path = path or STATE_PATH + if not state_path.exists(): + return {} + try: + return json.loads(state_path.read_text(encoding="utf-8")) # type: ignore[no-any-return] + except (json.JSONDecodeError, OSError): + return {} + + +def _is_pid_alive(pid: int) -> bool: + """Check whether a process with the given PID is running.""" + return pid_alive(pid) + + +# --------------------------------------------------------------------------- +# ConfigWatcher — monitors config file for live changes +# --------------------------------------------------------------------------- + + +class ConfigWatcher: + """Watches the daemon config file for changes and triggers reconciliation.""" + + def __init__( + self, + config_path: Path, + callback: Callable[[], None], + poll_interval: int = 2, + ) -> None: + self._config_path = config_path + self._callback = callback + self._poll_interval = poll_interval + self._observer: Any = None # watchdog Observer when available + self._last_mtime: float = 0.0 + self._poll_thread: threading.Thread | None = None + self._stop_event: threading.Event = threading.Event() + + # ------------------------------------------------------------------ + # Public + # ------------------------------------------------------------------ + + def start(self) -> None: + """Begin watching the config file for modifications.""" + try: + from watchdog.events import FileSystemEventHandler + from watchdog.observers import Observer + + watcher = self + + class _Handler(FileSystemEventHandler): # type: ignore[misc] + def on_modified(self, event: Any) -> None: + if Path(event.src_path).resolve() == watcher._config_path.resolve(): + watcher._on_config_changed() + + handler = _Handler() + self._observer = Observer() + self._observer.schedule( + handler, + str(self._config_path.parent), + recursive=False, + ) + self._observer.daemon = True + self._observer.start() + logger.info( + "Config watcher started (watchdog) for %s", + self._config_path, + ) + except ImportError: + # Fallback to polling when watchdog is unavailable + logger.info( + "watchdog not available — falling back to polling for %s", + self._config_path, + ) + self._start_polling() + + def stop(self) -> None: + """Stop watching the config file.""" + self._stop_event.set() + if self._observer is not None: + self._observer.stop() + self._observer.join(timeout=5) + self._observer = None + if self._poll_thread is not None: + self._poll_thread.join(timeout=5) + self._poll_thread = None + + # ------------------------------------------------------------------ + # Internal + # ------------------------------------------------------------------ + + def _start_polling(self) -> None: + """Poll the config file mtime in a background thread.""" + if self._config_path.exists(): + self._last_mtime = self._config_path.stat().st_mtime + + def _poll() -> None: + while not self._stop_event.is_set(): + self._stop_event.wait(self._poll_interval) + if self._stop_event.is_set(): + break + try: + if not self._config_path.exists(): + continue + mtime = self._config_path.stat().st_mtime + if mtime != self._last_mtime: + self._last_mtime = mtime + self._on_config_changed() + except OSError: + pass + + self._poll_thread = threading.Thread( + target=_poll, + daemon=True, + name="config-poller", + ) + self._poll_thread.start() + + def _on_config_changed(self) -> None: + """Handle a detected config file modification.""" + logger.info("Config file changed, triggering reconciliation") + try: + self._callback() + except Exception: + logger.exception("Error during config-change reconciliation") + + +# --------------------------------------------------------------------------- +# WatchDaemon — manages child processes for multi-repo watching +# --------------------------------------------------------------------------- + + +class WatchDaemon: + """Manages child processes for multi-repo file watching. + + Each watched repository gets a ``code-review-graph watch`` child process + managed via :mod:`subprocess`. No external tools (tmux, screen, etc.) + are required. + """ + + def __init__( + self, + config: DaemonConfig | None = None, + config_path: Path | None = None, + ) -> None: + self._config: DaemonConfig = config or load_config(config_path) + self._config_path: Path = config_path or CONFIG_PATH + self._state_path: Path = STATE_PATH + self._children: dict[str, subprocess.Popen[bytes]] = {} + self._current_repos: dict[str, WatchRepo] = {} + self._config_watcher: ConfigWatcher | None = None + self._health_thread: threading.Thread | None = None + self._health_stop: threading.Event = threading.Event() + self._lock: threading.Lock = threading.Lock() + + # ------------------------------------------------------------------ + # Public interface + # ------------------------------------------------------------------ + + def start(self) -> None: + """Spawn a watcher child process for each configured repo.""" + logger.info("Starting daemon '%s'", self._config.session_name) + + # Auto-register repos in the central registry + from .registry import Registry + + registry = Registry() + for repo in self._config.repos: + registry.register(repo.path, alias=repo.alias) + + # Build initial graph for repos that lack a database + for repo in self._config.repos: + db_path = Path(repo.path) / ".code-review-graph" / "graph.db" + if not db_path.exists(): + self._initial_build(repo) + + # Spawn a watcher child for every repo + for repo in self._config.repos: + self._start_watcher(repo) + + # Track current state + self._current_repos = {r.alias: r for r in self._config.repos} + + # Persist child PIDs to disk for cross-process status queries + self._save_state() + + # Start watching the config file for live changes + self.start_config_watcher() + + # Start health checker to auto-restart dead watchers + self.start_health_checker() + + msg = f"Daemon started — watching {len(self._config.repos)} repo(s)" + logger.info(msg) + print(msg) # noqa: T201 + + def stop(self) -> None: + """Tear down the daemon: stop watchers, terminate children.""" + self.stop_config_watcher() + self.stop_health_checker() + + with self._lock: + for alias, proc in list(self._children.items()): + self._terminate_child(alias, proc) + self._children.clear() + + self._current_repos.clear() + self._clear_state() + clear_pid() + logger.info("Daemon stopped") + + def reconcile(self, new_config: DaemonConfig | None = None) -> None: + """Reconcile running watchers with the (possibly updated) config. + + Child processes are started, stopped, or restarted to match the + desired state. New repos are registered in the central registry + and their graphs are built automatically (mirroring ``start()``). + """ + if new_config is not None: + self._config = new_config + + desired: dict[str, WatchRepo] = {r.alias: r for r in self._config.repos} + current: set[str] = set(self._current_repos.keys()) + + to_add: set[str] = desired.keys() - current + to_remove: set[str] = current - desired.keys() + to_update: set[str] = { + alias + for alias in desired.keys() & current + if desired[alias].path != self._current_repos[alias].path + } + + # Register new/updated repos and build graphs *before* acquiring + # the lock so that long-running builds don't block health checks. + if to_add or to_update: + from .registry import Registry + + registry = Registry() + + repos_needing_build: list[WatchRepo] = [] + for alias in to_add | to_update: + repo = desired[alias] + registry.register(repo.path, alias=repo.alias) + db_path = Path(repo.path) / ".code-review-graph" / "graph.db" + if not db_path.exists(): + repos_needing_build.append(repo) + + for repo in repos_needing_build: + self._initial_build(repo) + + with self._lock: + # Remove stale watchers + for alias in to_remove: + proc = self._children.pop(alias, None) + if proc is not None: + self._terminate_child(alias, proc) + del self._current_repos[alias] + + # Add new watchers + for alias in to_add: + repo = desired[alias] + self._start_watcher(repo) + self._current_repos[alias] = repo + + # Update changed watchers (path changed for same alias) + for alias in to_update: + proc = self._children.pop(alias, None) + if proc is not None: + self._terminate_child(alias, proc) + repo = desired[alias] + self._start_watcher(repo) + self._current_repos[alias] = repo + + # Persist updated state + self._save_state() + + logger.info( + "Reconcile complete — added: %d, removed: %d, updated: %d", + len(to_add), + len(to_remove), + len(to_update), + ) + + def status(self) -> dict[str, Any]: + """Return a summary of daemon state. + + When called from the daemon process itself, uses the in-memory + ``_children`` dict. When called from a separate process (e.g. the + CLI ``status`` command), falls back to the persisted state file and + checks liveness via ``os.kill(pid, 0)``. + """ + repos: list[dict[str, Any]] = [] + with self._lock: + if self._children: + # In-process: we have live Popen handles + for alias, repo in self._current_repos.items(): + proc = self._children.get(alias) + alive = proc is not None and proc.poll() is None + repos.append( + { + "alias": alias, + "path": repo.path, + "alive": alive, + "pid": proc.pid if proc is not None else None, + } + ) + else: + # Cross-process: read persisted state from disk + state = load_state(self._state_path) + for repo in self._config.repos: + entry = state.get(repo.alias, {}) + pid: int | None = entry.get("pid") + alive = pid is not None and _is_pid_alive(pid) + repos.append( + { + "alias": repo.alias, + "path": repo.path, + "alive": alive, + "pid": pid, + } + ) + return { + "session_name": self._config.session_name, + "running": True, + "repos": repos, + } + + # ------------------------------------------------------------------ + # Config watching + # ------------------------------------------------------------------ + + def start_config_watcher(self) -> None: + """Begin watching the config file for live edits.""" + self._config_watcher = ConfigWatcher( + config_path=self._config_path, + callback=self._on_config_change, + poll_interval=self._config.poll_interval, + ) + self._config_watcher.start() + + def _on_config_change(self) -> None: + """Reload configuration and reconcile running watchers.""" + try: + new_config = load_config(self._config_path) + except Exception: + logger.warning( + "Failed to parse config file — keeping last good config", + exc_info=True, + ) + return + self.reconcile(new_config) + + def stop_config_watcher(self) -> None: + """Stop the config file watcher if running.""" + if self._config_watcher is not None: + self._config_watcher.stop() + self._config_watcher = None + + # ------------------------------------------------------------------ + # Health checking + # ------------------------------------------------------------------ + + def start_health_checker(self) -> None: + """Start the background health-check thread.""" + self._health_stop = threading.Event() + self._health_thread = threading.Thread( + target=self._health_loop, + daemon=True, + name="health-checker", + ) + self._health_thread.start() + logger.info( + "Health checker started (interval=%ds)", + _HEALTH_CHECK_INTERVAL, + ) + + def stop_health_checker(self) -> None: + """Stop the health-check thread.""" + if hasattr(self, "_health_stop"): + self._health_stop.set() + if hasattr(self, "_health_thread") and self._health_thread is not None: + self._health_thread.join(timeout=5) + self._health_thread = None + + def _health_loop(self) -> None: + """Periodically check child processes and restart dead ones.""" + while not self._health_stop.is_set(): + self._health_stop.wait(_HEALTH_CHECK_INTERVAL) + if self._health_stop.is_set(): + break + self._check_health() + + def _check_health(self) -> None: + """Check each watcher child and restart if dead.""" + restarted = False + with self._lock: + for alias, repo in list(self._current_repos.items()): + proc = self._children.get(alias) + if proc is None or proc.poll() is not None: + logger.warning("Watcher for '%s' is dead — restarting", alias) + # Clean up dead process entry + self._children.pop(alias, None) + self._start_watcher(repo) + restarted = True + if restarted: + self._save_state() + + # ------------------------------------------------------------------ + # Daemonization + # ------------------------------------------------------------------ + + def daemonize(self) -> None: + """Fork to background using the double-fork pattern. + + Redirects stdout/stderr to the daemon log file. Writes PID file. + Sets up SIGTERM handler for graceful shutdown. + + On Windows, forking is not supported — the daemon runs in the + foreground and a warning is logged. + """ + if sys.platform == "win32": + logger.warning("Forking is not supported on Windows — running in foreground") + write_pid() + self._setup_signal_handlers() + return + + # First fork + pid = os.fork() + if pid > 0: + # Parent exits + sys.exit(0) + + # Become session leader + os.setsid() + + # Second fork (prevent acquiring a controlling terminal) + pid = os.fork() + if pid > 0: + sys.exit(0) + + # Redirect file descriptors + sys.stdout.flush() + sys.stderr.flush() + + self._config.log_dir.mkdir(parents=True, exist_ok=True) + log_file = self._config.log_dir / "daemon.log" + + # Open log file for stdout/stderr + fd = os.open( + str(log_file), + os.O_WRONLY | os.O_CREAT | os.O_APPEND, + 0o644, + ) + os.dup2(fd, sys.stdout.fileno()) + os.dup2(fd, sys.stderr.fileno()) + + # Redirect stdin from /dev/null + devnull = os.open(os.devnull, os.O_RDONLY) + os.dup2(devnull, sys.stdin.fileno()) + os.close(devnull) + if fd > 2: + os.close(fd) + + # Write PID file + write_pid() + + # Set up signal handlers + self._setup_signal_handlers() + + logger.info("Daemonized (PID %d)", os.getpid()) + + def _setup_signal_handlers(self) -> None: + """Install SIGTERM/SIGHUP handlers for graceful shutdown.""" + + def _handle_sigterm(signum: int, frame: Any) -> None: + logger.info("Received signal %d — shutting down", signum) + self.stop() + sys.exit(0) + + signal.signal(signal.SIGTERM, _handle_sigterm) + if sys.platform != "win32": + signal.signal(signal.SIGHUP, _handle_sigterm) + + def run_forever(self) -> None: + """Block forever, keeping the daemon alive. + + The config watcher and health checker run in background threads. + This method sleeps in the main thread until interrupted. + """ + try: + while True: + time.sleep(1) + except KeyboardInterrupt: + logger.info("Keyboard interrupt — stopping daemon") + self.stop() + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _save_state(self) -> None: + """Persist child PIDs and repo paths to disk for cross-process queries. + + Called after any mutation of ``_children`` so that ``status`` commands + running in a separate process can determine which watchers are alive. + """ + state: dict[str, dict[str, Any]] = {} + for alias, proc in self._children.items(): + repo = self._current_repos.get(alias) + state[alias] = { + "pid": proc.pid, + "path": repo.path if repo else "", + } + try: + self._state_path.parent.mkdir(parents=True, exist_ok=True) + self._state_path.write_text(json.dumps(state), encoding="utf-8") + except OSError: + logger.warning("Failed to persist daemon state to %s", self._state_path) + + def _clear_state(self) -> None: + """Remove the state file from disk.""" + try: + self._state_path.unlink(missing_ok=True) + except OSError: + pass + + def _start_watcher(self, repo: WatchRepo) -> None: + """Spawn a child process running ``code-review-graph watch`` for *repo*.""" + self._config.log_dir.mkdir(parents=True, exist_ok=True) + log_path = self._config.log_dir / f"{repo.alias}.log" + + crg_bin = shutil.which("code-review-graph") + if crg_bin: + cmd: list[str] = [crg_bin, "watch", "--repo", repo.path] + else: + cmd = [ + sys.executable, + "-m", + "code_review_graph", + "watch", + "--repo", + repo.path, + ] + + log_fd = open(log_path, "ab") # noqa: SIM115 + try: + proc = subprocess.Popen( + cmd, + cwd=repo.path, + stdout=log_fd, + stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, + ) + except Exception: + log_fd.close() + logger.exception("Failed to start watcher for '%s'", repo.alias) + return + + # The log fd is inherited by the child; we can close our copy. + # The child keeps the fd open via its own reference. + log_fd.close() + + self._children[repo.alias] = proc + logger.info( + "Started watcher for '%s' (PID %d) — log: %s", + repo.alias, + proc.pid, + log_path, + ) + + @staticmethod + def _terminate_child(alias: str, proc: subprocess.Popen[bytes]) -> None: + """Gracefully terminate a child process (SIGTERM, then SIGKILL).""" + if proc.poll() is not None: + return # already dead + + logger.info("Terminating watcher '%s' (PID %d)", alias, proc.pid) + proc.terminate() + try: + proc.wait(timeout=5) + except subprocess.TimeoutExpired: + logger.warning("Watcher '%s' did not stop — sending SIGKILL", alias) + proc.kill() + proc.wait(timeout=5) + + def _initial_build(self, repo: WatchRepo) -> None: + """Run a one-off graph build for a repo that has no database yet.""" + logger.info("Building initial graph for %s...", repo.alias) + + crg_bin = shutil.which("code-review-graph") + if crg_bin: + cmd: list[str] = [crg_bin, "build", "--repo", repo.path] + else: + cmd = [ + sys.executable, + "-m", + "code_review_graph", + "build", + "--repo", + repo.path, + ] + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + logger.warning( + "Initial build for '%s' failed (rc=%d): %s", + repo.alias, + result.returncode, + result.stderr.strip(), + ) diff --git a/code_review_graph/daemon_cli.py b/code_review_graph/daemon_cli.py new file mode 100644 index 0000000..194b9a7 --- /dev/null +++ b/code_review_graph/daemon_cli.py @@ -0,0 +1,320 @@ +"""CLI entry point for the crg-daemon multi-repo watcher. + +Usage: + crg-daemon start [--foreground] + crg-daemon stop + crg-daemon restart [--foreground] + crg-daemon status + crg-daemon logs [--repo ALIAS] [--follow] [--lines N] + crg-daemon add [--alias ALIAS] + crg-daemon remove +""" + +from __future__ import annotations + +import argparse +import logging +import os +import signal +import subprocess +import sys +import time + +logger = logging.getLogger(__name__) + + +# --------------------------------------------------------------------------- +# Subcommand handlers +# --------------------------------------------------------------------------- + + +def _handle_start(args: argparse.Namespace) -> None: + """Start the daemon process.""" + from .daemon import WatchDaemon, is_daemon_running, load_config + + if is_daemon_running(): + print("Error: Daemon is already running.") + sys.exit(1) + + config = load_config() + daemon = WatchDaemon(config=config) + daemon.start() + + if not args.foreground: + daemon.daemonize() + + daemon.run_forever() + + +def _handle_stop(_args: argparse.Namespace) -> None: + """Stop the running daemon process.""" + from .daemon import clear_pid, is_daemon_running, read_pid + + if not is_daemon_running(): + print("Daemon is not running.") + sys.exit(1) + + pid = read_pid() + if pid is None: + print("Error: Could not read daemon PID.") + sys.exit(1) + + print(f"Stopping daemon (PID {pid})...") + try: + os.kill(pid, signal.SIGTERM) + except ProcessLookupError: + clear_pid() + print("Daemon stopped (process already gone).") + return + except PermissionError: + print(f"Error: Permission denied sending signal to PID {pid}.") + sys.exit(1) + + # Wait up to 5 seconds for process to die + for _ in range(50): + try: + os.kill(pid, 0) + except ProcessLookupError: + break + time.sleep(0.1) + else: + # Still alive after 5s — send SIGKILL + print("Daemon did not stop gracefully, sending SIGKILL...") + try: + os.kill(pid, signal.SIGKILL) + except ProcessLookupError: + pass + + clear_pid() + print("Daemon stopped.") + + +def _handle_restart(args: argparse.Namespace) -> None: + """Restart the daemon (stop + start).""" + from .daemon import is_daemon_running + + if is_daemon_running(): + _handle_stop(args) + else: + print("Daemon is not running, starting fresh.") + + _handle_start(args) + + +def _handle_status(_args: argparse.Namespace) -> None: + """Show daemon status and configuration.""" + from .daemon import is_daemon_running, load_config, load_state, pid_alive, read_pid + + config = load_config() + running = is_daemon_running() + + if running: + pid = read_pid() + print(f"Daemon: running (PID {pid})") + else: + print("Daemon: not running") + + print(f"Name: {config.session_name}") + print(f"Log dir: {config.log_dir}") + print(f"Poll: {config.poll_interval}s") + print() + + if not config.repos: + print("No repositories configured.") + print("Use: crg-daemon add [--alias NAME]") + return + + # Header + alias_width = max(len(r.alias) for r in config.repos) + alias_width = max(alias_width, 5) # minimum "Alias" header width + + if running: + state = load_state() + print(f" {'Alias':<{alias_width}} {'Status':<8} {'PID':<8} Path") + print(f" {'-' * alias_width} {'-' * 8} {'-' * 8} {'-' * 40}") + for repo in config.repos: + entry = state.get(repo.alias, {}) + child_pid: int | None = entry.get("pid") + alive = child_pid is not None and pid_alive(child_pid) + status_str = "alive" if alive else "dead" + pid_str = str(child_pid) if child_pid is not None else "-" + print(f" {repo.alias:<{alias_width}} {status_str:<8} {pid_str:<8} {repo.path}") + else: + print(f" {'Alias':<{alias_width}} Path") + print(f" {'-' * alias_width} {'-' * 40}") + for repo in config.repos: + print(f" {repo.alias:<{alias_width}} {repo.path}") + + +def _handle_logs(args: argparse.Namespace) -> None: + """Show daemon or per-repo log files.""" + from .daemon import load_config + + config = load_config() + + if args.repo: + log_file = config.log_dir / f"{args.repo}.log" + else: + log_file = config.log_dir / "daemon.log" + + if not log_file.exists(): + print(f"Log file not found: {log_file}") + sys.exit(1) + + if args.follow: + try: + subprocess.run(["tail", "-f", str(log_file)], check=False) + except KeyboardInterrupt: + pass + return + + # Read last N lines + lines_count = args.lines + try: + text = log_file.read_text(encoding="utf-8", errors="replace") + except OSError as exc: + print(f"Error reading log file: {exc}") + sys.exit(1) + + lines = text.splitlines() + tail = lines[-lines_count:] if len(lines) > lines_count else lines + for line in tail: + print(line) + + +def _handle_add(args: argparse.Namespace) -> None: + """Add a repository to the daemon config.""" + from .daemon import add_repo_to_config, is_daemon_running + + try: + add_repo_to_config(args.path, alias=args.alias) + except ValueError as exc: + print(f"Error: {exc}") + sys.exit(1) + + # Find the repo we just added to show confirmation + alias = args.alias or os.path.basename(os.path.abspath(args.path)) + print(f"Added repository: {args.path} (alias: {alias})") + + if is_daemon_running(): + print("Daemon will pick up the change automatically.") + + +def _handle_remove(args: argparse.Namespace) -> None: + """Remove a repository from the daemon config.""" + from .daemon import is_daemon_running, load_config, remove_repo_from_config + + config_before = load_config() + count_before = len(config_before.repos) + + config_after = remove_repo_from_config(args.path_or_alias) + count_after = len(config_after.repos) + + if count_before == count_after: + print(f"No repository matching '{args.path_or_alias}' found in config.") + sys.exit(1) + + print(f"Removed repository: {args.path_or_alias}") + + if is_daemon_running(): + print("Daemon will pick up the change automatically.") + + +# --------------------------------------------------------------------------- +# Main entry point +# --------------------------------------------------------------------------- + + +def main() -> None: + """Entry point for the crg-daemon CLI.""" + logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s") + + ap = argparse.ArgumentParser( + prog="crg-daemon", + description="Multi-repo watch daemon for code-review-graph", + ) + sub = ap.add_subparsers(dest="command") + + # start + start_cmd = sub.add_parser("start", help="Start the daemon") + start_cmd.add_argument( + "--foreground", + action="store_true", + help="Run in the foreground instead of daemonizing", + ) + + # stop + sub.add_parser("stop", help="Stop the daemon") + + # restart + restart_cmd = sub.add_parser("restart", help="Restart the daemon") + restart_cmd.add_argument( + "--foreground", + action="store_true", + help="Run in the foreground instead of daemonizing", + ) + + # status + sub.add_parser("status", help="Show daemon status and configuration") + + # logs + logs_cmd = sub.add_parser("logs", help="Show daemon or per-repo logs") + logs_cmd.add_argument( + "--repo", + default=None, + metavar="ALIAS", + help="Show logs for a specific repo (by alias)", + ) + logs_cmd.add_argument( + "--follow", + "-f", + action="store_true", + help="Follow log output (tail -f)", + ) + logs_cmd.add_argument( + "--lines", + "-n", + type=int, + default=50, + help="Number of lines to show (default: 50)", + ) + + # add + add_cmd = sub.add_parser("add", help="Add a repository to the daemon config") + add_cmd.add_argument("path", help="Path to the repository") + add_cmd.add_argument( + "--alias", + default=None, + help="Short alias for the repository (default: directory name)", + ) + + # remove + remove_cmd = sub.add_parser("remove", help="Remove a repository from the daemon config") + remove_cmd.add_argument("path_or_alias", help="Repository path or alias to remove") + + args = ap.parse_args() + + if not args.command: + ap.print_help() + sys.exit(0) + + handlers: dict[str, object] = { + "start": _handle_start, + "stop": _handle_stop, + "restart": _handle_restart, + "status": _handle_status, + "logs": _handle_logs, + "add": _handle_add, + "remove": _handle_remove, + } + + handler = handlers.get(args.command) + if handler is None: + ap.print_help() + sys.exit(1) + + handler(args) # type: ignore[operator] + + +if __name__ == "__main__": + main() diff --git a/code_review_graph/embeddings.py b/code_review_graph/embeddings.py new file mode 100644 index 0000000..ccb1350 --- /dev/null +++ b/code_review_graph/embeddings.py @@ -0,0 +1,1006 @@ +"""Vector embedding support for semantic code search. + +Supports multiple providers: +1. Local (sentence-transformers) - Private, fast, offline. +2. Google Gemini - High-quality, cloud-based. Requires explicit opt-in. +3. MiniMax (embo-01) - High-quality 1536-dim cloud embeddings. Requires MINIMAX_API_KEY. +4. OpenAI-compatible - Any endpoint speaking OpenAI /v1/embeddings (real OpenAI, + Azure OpenAI, self-hosted gateways like new-api / LiteLLM / vLLM / LocalAI / Ollama). +""" + +from __future__ import annotations + +import hashlib +import logging +import os +import re +import sqlite3 +import struct +import sys +import time +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Any +from urllib.parse import urlparse + +from . import __version__ as _crg_version +from .graph import GraphNode, GraphStore, node_to_dict + +logger = logging.getLogger(__name__) + +# Sent on every cloud-provider HTTP request. Some providers (e.g. Fireworks) +# sit behind Cloudflare and reject the urllib default ``Python-urllib/X.Y`` +# UA with HTTP 403 / error 1010 ("browser signature banned"). A real UA gets +# us through and gives upstream a way to identify CRG-driven traffic. +_USER_AGENT = ( + f"code-review-graph/{_crg_version} " + "(+https://github.com/tirth8205/code-review-graph)" +) + +# --------------------------------------------------------------------------- +# Provider Interface and Implementations +# --------------------------------------------------------------------------- + + +class EmbeddingProvider(ABC): + @abstractmethod + def embed(self, texts: list[str]) -> list[list[float]]: + pass + + @abstractmethod + def embed_query(self, text: str) -> list[float]: + """Embed a search query (may use a different task type than indexing).""" + pass + + @property + @abstractmethod + def dimension(self) -> int: + pass + + @property + @abstractmethod + def name(self) -> str: + pass + + +LOCAL_DEFAULT_MODEL = "all-MiniLM-L6-v2" + + +# Process-wide cache of loaded sentence-transformer models, keyed by model name. +# Populated by ``prewarm_local_embeddings()`` at server startup (see ``main.main``) +# and by ``LocalEmbeddingProvider._get_model`` on first lazy load. Sharing the +# loaded model across ``LocalEmbeddingProvider`` instances avoids re-importing +# ``sentence_transformers`` + ``torch`` from worker threads, which deadlocks +# ``semantic_search_nodes_tool`` on Windows stdio MCP (#385 fixed the peer +# tools via ``asyncio.to_thread``; this cache fixes the remaining case where +# torch DLL / OpenMP init runs inside an executor thread). +_MODEL_CACHE: dict[str, Any] = {} + + +def prewarm_local_embeddings(model_name: str | None = None) -> None: + """Eagerly load the local sentence-transformer model on the calling thread. + + Call this from the **main thread** before entering an asyncio event loop + (e.g. before ``mcp.run()``) on Windows to prevent a deadlock where lazy- + loading ``sentence_transformers`` + ``torch`` inside a FastMCP executor + worker thread blocks indefinitely on DLL init / OpenMP thread-pool + registration. + + No-op when ``sentence-transformers`` is not installed (cloud-provider + setups remain unaffected) or when the configured model is already cached. + + Args: + model_name: Optional override; falls back to the ``CRG_EMBEDDING_MODEL`` + environment variable and then to ``LOCAL_DEFAULT_MODEL``. + """ + try: + from sentence_transformers import SentenceTransformer # noqa: F401 + except ImportError: + return # cloud-only setup: nothing to pre-warm + + resolved = model_name or os.environ.get( + "CRG_EMBEDDING_MODEL", LOCAL_DEFAULT_MODEL + ) + if resolved in _MODEL_CACHE: + return + + try: + _MODEL_CACHE[resolved] = LocalEmbeddingProvider(resolved)._get_model() + except Exception as exc: # pragma: no cover — best-effort startup hook + logger.warning("prewarm_local_embeddings(%s) skipped: %s", resolved, exc) + + +class LocalEmbeddingProvider(EmbeddingProvider): + def __init__(self, model_name: str | None = None) -> None: + self._model_name = model_name or os.environ.get( + "CRG_EMBEDDING_MODEL", LOCAL_DEFAULT_MODEL + ) + self._model = None # Lazy-loaded + + def _get_model(self): + if self._model is None: + # Check the process-wide cache first — populated either by a prior + # provider instance or by ``prewarm_local_embeddings`` at startup. + cached = _MODEL_CACHE.get(self._model_name) + if cached is not None: + self._model = cached + return self._model + try: + from sentence_transformers import SentenceTransformer + # Check environment variable, default to False to prevent RCE + _rce_val = os.environ.get("CRG_ALLOW_REMOTE_CODE", "0") + allow_remote_code = _rce_val.lower() in ("1", "true", "yes") + + self._model = SentenceTransformer( + self._model_name, + trust_remote_code=allow_remote_code, + ) + _MODEL_CACHE[self._model_name] = self._model + except ImportError: + raise ImportError( + "sentence-transformers not installed. " + "Run: pip install code-review-graph[embeddings]" + ) + return self._model + + def embed(self, texts: list[str]) -> list[list[float]]: + model = self._get_model() + vectors = model.encode(texts, show_progress_bar=False) + return [v.tolist() for v in vectors] + + def embed_query(self, text: str) -> list[float]: + return self.embed([text])[0] + + @property + def dimension(self) -> int: + model = self._get_model() + return model.get_sentence_embedding_dimension() + + @property + def name(self) -> str: + return f"local:{self._model_name}" + + +class GoogleEmbeddingProvider(EmbeddingProvider): + def __init__(self, api_key: str, model: str = "gemini-embedding-001") -> None: + try: + from google import genai + self._client = genai.Client(api_key=api_key) + self.model = model + self._dimension: int | None = None + except ImportError: + raise ImportError( + "google-generativeai not installed. " + "Run: pip install code-review-graph[google-embeddings]" + ) + + def embed(self, texts: list[str]) -> list[list[float]]: + batch_size = 100 + results = [] + for i in range(0, len(texts), batch_size): + batch = texts[i:i + batch_size] + response = self._call_with_retry( + lambda b=batch: self._client.models.embed_content( + model=self.model, + contents=b, + config={"task_type": "RETRIEVAL_DOCUMENT"}, + ) + ) + results.extend([e.values for e in response.embeddings]) + if self._dimension is None and results: + self._dimension = len(results[0]) + return results + + @staticmethod + def _call_with_retry(fn, max_retries: int = 3): + """Call fn with exponential backoff on transient API errors.""" + for attempt in range(max_retries): + try: + return fn() + except Exception as e: + # Retry on rate-limit (429) or server errors (5xx) + err_str = str(e) + is_retryable = "429" in err_str or "500" in err_str or "503" in err_str + if not is_retryable or attempt == max_retries - 1: + raise + wait = 2 ** attempt + logger.warning("Gemini API error (attempt %d/%d), retrying in %ds: %s", + attempt + 1, max_retries, wait, e) + time.sleep(wait) + + def embed_query(self, text: str) -> list[float]: + response = self._call_with_retry( + lambda: self._client.models.embed_content( + model=self.model, + contents=[text], + config={"task_type": "RETRIEVAL_QUERY"}, + ) + ) + vec = response.embeddings[0].values + if self._dimension is None: + self._dimension = len(vec) + return vec + + @property + def dimension(self) -> int: + if self._dimension is not None: + return self._dimension + # Default for gemini-embedding-001; updated dynamically after first call + return 768 + + @property + def name(self) -> str: + return f"google:{self.model}" + + +class MiniMaxEmbeddingProvider(EmbeddingProvider): + """MiniMax embo-01 embedding provider (1536 dimensions). + + Uses the MiniMax Embeddings API (https://api.minimax.io/v1/embeddings) + with the embo-01 model. Requires the MINIMAX_API_KEY environment variable. + """ + + _ENDPOINT = "https://api.minimax.io/v1/embeddings" + _MODEL = "embo-01" + _DIMENSION = 1536 + + def __init__(self, api_key: str) -> None: + self._api_key = api_key + + def _call_api(self, texts: list[str], task_type: str) -> list[list[float]]: + import json as _json + import urllib.request + + payload = _json.dumps({ + "model": self._MODEL, + "texts": texts, + "type": task_type, + }).encode("utf-8") + + req = urllib.request.Request( + self._ENDPOINT, + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self._api_key}", + "User-Agent": _USER_AGENT, + "Accept": "application/json", + }, + ) + + max_retries = 3 + for attempt in range(max_retries): + try: + import ssl + _ssl_ctx = ssl.create_default_context() + with urllib.request.urlopen(req, timeout=60, context=_ssl_ctx) as resp: # nosec B310 + body = _json.loads(resp.read().decode("utf-8")) + + base_resp = body.get("base_resp", {}) + if base_resp.get("status_code", 0) != 0: + raise RuntimeError( + f"MiniMax API error: {base_resp.get('status_msg', 'unknown')}" + ) + + return body["vectors"] + except Exception as e: + err_str = str(e) + is_retryable = "429" in err_str or "500" in err_str or "503" in err_str + if not is_retryable or attempt == max_retries - 1: + raise + wait = 2 ** attempt + logger.warning( + "MiniMax API error (attempt %d/%d), retrying in %ds: %s", + attempt + 1, max_retries, wait, e, + ) + time.sleep(wait) + + return [] # unreachable, but keeps mypy happy + + def embed(self, texts: list[str]) -> list[list[float]]: + batch_size = 100 + results: list[list[float]] = [] + for i in range(0, len(texts), batch_size): + batch = texts[i:i + batch_size] + results.extend(self._call_api(batch, "db")) + return results + + def embed_query(self, text: str) -> list[float]: + return self._call_api([text], "query")[0] + + @property + def dimension(self) -> int: + return self._DIMENSION + + @property + def name(self) -> str: + return f"minimax:{self._MODEL}" + + +class OpenAIEmbeddingProvider(EmbeddingProvider): + """OpenAI-compatible embedding provider. + + Works with any endpoint that speaks the OpenAI ``/v1/embeddings`` schema: + - Real OpenAI API (``https://api.openai.com/v1``) + - Azure OpenAI + - Self-hosted gateways: new-api, LiteLLM, vLLM, LocalAI, Ollama (openai mode) + + Provider identity in ``name`` includes both the model and the endpoint + host (``openai:{model}@{host}``), so switching base URL while keeping the + same model ID re-partitions the embeddings table and forces a clean + re-embed. This is the only defense against silently mixing vector spaces + from different backends (e.g. real OpenAI vs. an OpenAI-compatible + gateway that ships different weights under the same model name). + + Dimension is detected from the first response and frozen; switching the + ``model`` in the environment also changes ``provider.name`` and triggers + re-embed via the same isolation key. + """ + + _DEFAULT_BATCH_SIZE = 100 + + # Default ports by scheme; stripped from the host_key so the user can't + # accidentally force a re-embed by toggling an explicit default port. + _DEFAULT_PORTS = {"http": 80, "https": 443} + + def __init__( + self, + api_key: str, + base_url: str, + model: str, + dimension: int | None = None, + timeout: int = 120, + batch_size: int | None = None, + ) -> None: + self._api_key = api_key + self._base_url = base_url.rstrip("/") + self._model = model + self._dimension = dimension + self._timeout = timeout + self._batch_size = batch_size or self._DEFAULT_BATCH_SIZE + self._host_key = self._make_host_key(self._base_url) + + @classmethod + def _make_host_key(cls, base_url: str) -> str: + """Normalize the identity key used in ``provider.name``. + + Codex review pushed this well past naive ``netloc`` because that + alone has three leaks: + + 1. ``netloc`` preserves ``userinfo`` (``user:pass@host``) — we'd + persist credentials into the DB's ``embeddings.provider`` column. + Use ``hostname`` instead. + 2. Default ports (``:80`` for http, ``:443`` for https) are + semantically identical to omitting the port; keeping them would + cause spurious re-embeds when the user just spelled the URL + differently. + 3. Path is part of the backend identity for path-routed gateways: + ``https://gw/openai/v1`` and ``https://gw/vendor-b/v1`` front + different models and must not share cached vectors. + """ + parsed = urlparse(base_url) + hostname = (parsed.hostname or "").lower() + scheme = (parsed.scheme or "").lower() + port = parsed.port + if port and port != cls._DEFAULT_PORTS.get(scheme): + # Bracket IPv6 literals when appending a port. + host_part = f"[{hostname}]:{port}" if ":" in hostname else f"{hostname}:{port}" + else: + host_part = hostname + # Preserve path routing. Trim any trailing slash and any + # ``/embeddings`` suffix that callers may have included — we append + # that ourselves when building the request URL. + path = (parsed.path or "").rstrip("/") + if path.endswith("/embeddings"): + path = path[: -len("/embeddings")].rstrip("/") + # Include scheme: http and https to the same host+path front + # different endpoints in practice (plaintext vs TLS, dev vs prod + # gateway), and sharing cached vectors across them is the same + # silent-mixing failure mode as switching base URL entirely. + return f"{scheme}://{host_part}{path}" if path else f"{scheme}://{host_part}" + + def _call_api(self, texts: list[str]) -> list[list[float]]: + import http.client + import json as _json + import socket + import ssl + import urllib.error + import urllib.request + + body: dict[str, Any] = {"model": self._model, "input": texts} + # OpenAI v3 models (text-embedding-3-*) support dimension reduction; + # only forward the param when the user explicitly pinned one. + if self._dimension is not None: + body["dimensions"] = self._dimension + + payload = _json.dumps(body).encode("utf-8") + req = urllib.request.Request( + f"{self._base_url}/embeddings", + data=payload, + headers={ + "Content-Type": "application/json", + "Authorization": f"Bearer {self._api_key}", + "User-Agent": _USER_AGENT, + "Accept": "application/json", + }, + ) + + max_retries = 3 + for attempt in range(max_retries): + try: + _ssl_ctx = ssl.create_default_context() + try: + with urllib.request.urlopen( # nosec B310 + req, timeout=self._timeout, context=_ssl_ctx, + ) as resp: + raw = resp.read().decode("utf-8") + except urllib.error.HTTPError as http_err: + # 429 / 5xx: re-raise and let the outer retry loop handle it. + # (We must not convert to RuntimeError here or retry below + # can't tell it was a transient HTTP failure.) + if http_err.code == 429 or 500 <= http_err.code < 600: + raise + # Other 4xx: surface the API error body instead of a bare + # "400 Bad Request" — gateways like new-api return JSON + # with the real reason (batch size limits, invalid model, + # etc.) which is far more actionable. + try: + err_body = http_err.read().decode("utf-8", errors="replace") + except Exception: + err_body = "" + err_msg = err_body or str(http_err) + try: + parsed = _json.loads(err_body) + if isinstance(parsed, dict) and "error" in parsed: + err_obj = parsed["error"] + err_msg = ( + err_obj.get("message", err_msg) + if isinstance(err_obj, dict) else str(err_obj) + ) + except Exception: # nosec B110 + # Non-JSON error body is fine: we already seeded + # err_msg with the raw body above, so fall through. + pass + raise RuntimeError( + f"OpenAI API HTTP {http_err.code}: {err_msg}" + ) from http_err + + response = _json.loads(raw) + + if "error" in response: + err = response["error"] + msg = err.get("message", "unknown") if isinstance(err, dict) else str(err) + raise RuntimeError(f"OpenAI API error: {msg}") + + data = response.get("data", []) + if not data: + raise RuntimeError("OpenAI API returned empty data") + # OpenAI spec: data[i].index maps to input[i], but some + # compatible gateways re-order results or drop entries on + # partial failure, and others omit `index` entirely. Three + # disjoint cases: + # 1. All items have a valid int ``index``: must form a + # permutation of 0..N-1, then sort and use. + # 2. NO item carries an ``index`` field: trust server + # order, only verify count matches. + # 3. Anything in between (partial indices, str indices, + # missing on some): refuse. Zipping server order in + # that case would happily misalign the indexed items. + any_has_index = any("index" in item for item in data) + all_int_index = all( + isinstance(item.get("index"), int) for item in data + ) + if all_int_index: + expected = set(range(len(texts))) + indices = [int(item["index"]) for item in data] + if len(set(indices)) != len(indices) or set(indices) != expected: + raise RuntimeError( + "OpenAI API returned malformed indices " + f"(got {indices}, expected permutation of " + f"0..{len(texts) - 1}) — refusing to misalign vectors." + ) + data = sorted(data, key=lambda item: int(item["index"])) + elif not any_has_index: + if len(data) != len(texts): + raise RuntimeError( + f"OpenAI API returned {len(data)} embeddings for " + f"{len(texts)} inputs with no index field — " + "refusing to misalign vectors." + ) + else: + # Mixed: some items have index, others don't (or carry + # non-int index). Server order would silently misplace + # the indexed items, so we refuse. + raise RuntimeError( + "OpenAI API returned mixed indexed/unindexed data — " + "refusing to misalign vectors." + ) + + vectors = [item["embedding"] for item in data] + if vectors and self._dimension is None: + self._dimension = len(vectors[0]) + return vectors + + except Exception as e: + # Retryable = HTTP 429/5xx, network/timeout/TLS issues. + # Non-retryable = HTTP 4xx (other), malformed responses, + # misaligned data length — those are caller-side bugs that + # will keep failing on retry. + is_retryable = False + if isinstance(e, urllib.error.HTTPError): + is_retryable = e.code == 429 or 500 <= e.code < 600 + elif isinstance(e, ( + urllib.error.URLError, + socket.timeout, + TimeoutError, + ConnectionError, + ssl.SSLError, + # Reverse proxies and edge gateways surface transient + # disconnects as these stdlib classes. Real incidents + # have been observed on Cloudflare-fronted endpoints + # and on LiteLLM when upstream providers hiccup. + http.client.IncompleteRead, + http.client.BadStatusLine, + http.client.RemoteDisconnected, + )): + is_retryable = True + if not is_retryable or attempt == max_retries - 1: + raise + wait = 2 ** attempt + logger.warning( + "OpenAI embeddings API error (attempt %d/%d), retrying in %ds: %s", + attempt + 1, max_retries, wait, e, + ) + time.sleep(wait) + + return [] # unreachable + + def embed(self, texts: list[str]) -> list[list[float]]: + if not texts: + return [] + results: list[list[float]] = [] + for i in range(0, len(texts), self._batch_size): + results.extend(self._call_api(texts[i:i + self._batch_size])) + return results + + def embed_query(self, text: str) -> list[float]: + return self._call_api([text])[0] + + @property + def dimension(self) -> int: + if self._dimension is not None: + return self._dimension + # Default for text-embedding-3-small; updated after first call. + return 1536 + + @property + def name(self) -> str: + # Endpoint-aware identity: model alone is NOT enough — two backends + # can serve the same model ID with different weights or dimensions, + # and re-using cached embeddings across them silently corrupts + # semantic ranking. Including the host partitions the embeddings + # table so switching CRG_OPENAI_BASE_URL triggers a safe re-embed. + return f"openai:{self._model}@{self._host_key}" + + +CLOUD_PROVIDERS = {"google", "minimax", "openai"} + + +def _is_localhost_url(url: str) -> bool: + """Return True if url points to a localhost host (never treat as cloud egress). + + Uses urlparse.hostname so we compare the actual host, not a substring + match that could be fooled by e.g. ``https://my-openai.127.0.0.1.nip.io``. + """ + try: + host = (urlparse(url).hostname or "").lower() + except Exception: + return False + # nosec B104: we're *matching* a URL hostname, not binding a listener. + return host in {"127.0.0.1", "localhost", "0.0.0.0", "::1"} # nosec B104 + + +def _warn_cloud_egress(provider_name: str) -> None: + """Print a stderr warning before a cloud embedding provider is used. + + The warning is suppressed when ``CRG_ACCEPT_CLOUD_EMBEDDINGS=1`` is + set in the environment, so scripted / CI workloads can acknowledge + once and move on. Use stderr (never stdin/input) to stay compatible + with the MCP stdio transport — anything we write to stdout would + corrupt the JSON-RPC stream. See: #174 + """ + if os.environ.get("CRG_ACCEPT_CLOUD_EMBEDDINGS", "").strip() == "1": + return + print( + f"\n⚠️ code-review-graph: about to embed code via the '{provider_name}' " + "cloud provider.\n" + " Your source code (function names, docstrings, file paths) will be " + "sent to an external API.\n" + " This is necessary for semantic search with the cloud provider you " + "selected.\n" + " To skip this warning in future runs, set " + "CRG_ACCEPT_CLOUD_EMBEDDINGS=1 in your environment.\n" + " To stay fully offline, use the default 'local' provider instead " + "(no API key needed).\n", + file=sys.stderr, + ) + + +_VALID_PROVIDERS = {"local", "openai", "google", "minimax"} + + +def get_provider( + provider: str | None = None, + model: str | None = None, +) -> EmbeddingProvider | None: + """Get an embedding provider by name. + + Args: + provider: Provider name. One of "local", "google", "minimax", "openai", + or None for local. Names are case-insensitive and surrounding + whitespace is ignored; unknown names raise ValueError instead + of silently falling back to the local provider. + Google requires GOOGLE_API_KEY env var and explicit opt-in. + MiniMax requires MINIMAX_API_KEY env var and explicit opt-in. + OpenAI requires CRG_OPENAI_API_KEY + CRG_OPENAI_BASE_URL + + CRG_OPENAI_MODEL env vars (or the ``model`` arg). The egress + warning is skipped when the base URL points to localhost. + Cloud providers emit a one-time stderr warning before use + unless ``CRG_ACCEPT_CLOUD_EMBEDDINGS=1`` is set. See: #174 + model: Model name/path to use. For local provider this is any + sentence-transformers compatible model. Falls back to + CRG_EMBEDDING_MODEL env var, then to all-MiniLM-L6-v2. + For Google provider this is a Gemini model ID. + For OpenAI provider this overrides CRG_OPENAI_MODEL. + + Raises: + ValueError: If the provider name is not one of the known providers, + or if required environment variables are missing. + """ + name = provider.strip().lower() if provider else "" + if name and name not in _VALID_PROVIDERS: + raise ValueError( + f"Unknown embedding provider '{name}'. " + "Valid: local, openai, google, minimax" + ) + + if name == "openai": + api_key = os.environ.get("CRG_OPENAI_API_KEY") + base_url = os.environ.get("CRG_OPENAI_BASE_URL") + resolved_model = model or os.environ.get("CRG_OPENAI_MODEL") + if not api_key or not base_url or not resolved_model: + missing = [ + name for name, val in [ + ("CRG_OPENAI_API_KEY", api_key), + ("CRG_OPENAI_BASE_URL", base_url), + ("CRG_OPENAI_MODEL", resolved_model), + ] if not val + ] + raise ValueError( + "Missing required environment variable(s) for the OpenAI " + f"embedding provider: {', '.join(missing)}." + ) + dim_env = os.environ.get("CRG_OPENAI_DIMENSION") + dimension = int(dim_env) if dim_env else None + batch_env = os.environ.get("CRG_OPENAI_BATCH_SIZE") + batch_size = int(batch_env) if batch_env else None + if not _is_localhost_url(base_url): + _warn_cloud_egress("openai") + return OpenAIEmbeddingProvider( + api_key=api_key, + base_url=base_url, + model=resolved_model, + dimension=dimension, + batch_size=batch_size, + ) + + if name == "minimax": + api_key = os.environ.get("MINIMAX_API_KEY") + if not api_key: + raise ValueError( + "MINIMAX_API_KEY environment variable is required for " + "the MiniMax embedding provider." + ) + _warn_cloud_egress("minimax") + return MiniMaxEmbeddingProvider(api_key=api_key) + + if name == "google": + api_key = os.environ.get("GOOGLE_API_KEY") + if not api_key: + raise ValueError( + "GOOGLE_API_KEY environment variable is required for " + "the Google embedding provider." + ) + _warn_cloud_egress("google") + try: + return GoogleEmbeddingProvider( + api_key=api_key, + **({"model": model} if model else {}), + ) + except ImportError: + return None + + # Default: local + if not _check_available(): + return None + try: + return LocalEmbeddingProvider(model_name=model) + except ImportError: + return None + + +def _check_available() -> bool: + """Check whether local embedding support is available.""" + try: + import sentence_transformers # noqa: F401 + return True + except ImportError: + return False + + +# --------------------------------------------------------------------------- +# SQLite vector storage +# --------------------------------------------------------------------------- + +_EMBEDDINGS_SCHEMA = """ +CREATE TABLE IF NOT EXISTS embeddings ( + qualified_name TEXT PRIMARY KEY, + vector BLOB NOT NULL, + text_hash TEXT NOT NULL, + provider TEXT NOT NULL DEFAULT 'unknown' +); +""" + + +def _encode_vector(vec: list[float]) -> bytes: + """Encode a float vector as a compact binary blob.""" + return struct.pack(f"{len(vec)}f", *vec) + + +def _decode_vector(blob: bytes) -> list[float]: + """Decode a binary blob back to a float vector.""" + n = len(blob) // 4 # 4 bytes per float32 + return list(struct.unpack(f"{n}f", blob)) + + +def _cosine_similarity(a: list[float], b: list[float]) -> float: + """Compute cosine similarity between two vectors.""" + if len(a) != len(b): + return 0.0 + dot = sum(x * y for x, y in zip(a, b)) + norm_a = sum(x * x for x in a) ** 0.5 + norm_b = sum(x * x for x in b) ** 0.5 + if norm_a == 0 or norm_b == 0: + return 0.0 + return dot / (norm_a * norm_b) + + +_IDENTIFIER_SPLIT_RE = re.compile(r"([a-z])([A-Z])|[_./\-]+") + + +def _split_identifier(name: str) -> str: + """Split snake_case / camelCase / PascalCase / dotted into space-separated words. + + Examples: + get_route_handler -> "get route handler" + APIRoute -> "API Route" + dispatch_request -> "dispatch request" + full_dispatch_request -> "full dispatch request" + """ + if not name: + return "" + # Insert space between lowercase->uppercase transitions, then collapse + # snake_case / dotted / hyphenated separators. + spaced = re.sub(r"([a-z])([A-Z])", r"\1 \2", name) + spaced = re.sub(r"[_./\-]+", " ", spaced) + return " ".join(spaced.split()) + + +def _node_to_text(node: GraphNode) -> str: + """Convert a node to a searchable text representation. + + Designed so natural-language queries land on the right node, not just on + the enclosing class. We include the dotted ``Parent.name`` form, the + identifier split into words, an explicit ``"in "`` phrase, the + enclosing module directory, and the language. Tested by the + ``multi_hop_retrieval`` benchmark — see ``docs/REPRODUCING.md``. + """ + parts: list[str] = [] + + # 1. Dotted form first — strongest lexical signal for "method in class" + if node.parent_name and node.kind != "File": + parts.append(f"{node.parent_name}.{node.name}") + + # 2. Bare name (always present) + parts.append(node.name) + + # 3. Split-words form of the name (only if it differs from the bare name) + name_split = _split_identifier(node.name) + if name_split and name_split.lower() != node.name.lower(): + parts.append(name_split) + + # 4. Kind ("function", "class", "test", ...) + if node.kind != "File": + parts.append(node.kind.lower()) + + # 5. Parent context with the split form too + if node.parent_name: + parts.append(f"in {node.parent_name}") + parent_split = _split_identifier(node.parent_name) + if parent_split and parent_split.lower() != node.parent_name.lower(): + parts.append(parent_split) + + # 6. Signature bits + if node.params: + parts.append(node.params) + if node.return_type: + parts.append(f"returns {node.return_type}") + + # 7. Module / directory context from the file path — gives queries a + # term like "routing" or "client" to anchor against. + if node.file_path: + parent_dir = Path(node.file_path).parent.name + if parent_dir and parent_dir not in (".", "src", "lib"): + parts.append(parent_dir) + + # 8. Language + if node.language: + parts.append(node.language) + + return " ".join(parts) + + +class EmbeddingStore: + """Manages vector embeddings for graph nodes in SQLite.""" + + def __init__( + self, + db_path: str | Path, + provider: str | None = None, + model: str | None = None, + ) -> None: + self.provider = get_provider(provider, model=model) + self.available = self.provider is not None + self.db_path = Path(db_path) + self._conn = sqlite3.connect( + str(self.db_path), timeout=30, check_same_thread=False, + isolation_level=None, + ) + self._conn.row_factory = sqlite3.Row + self._conn.executescript(_EMBEDDINGS_SCHEMA) + + # Migration for existing DBs missing the provider column + try: + self._conn.execute("SELECT provider FROM embeddings LIMIT 1") + except sqlite3.OperationalError: + self._conn.execute( + "ALTER TABLE embeddings ADD COLUMN provider " + "TEXT NOT NULL DEFAULT 'unknown'" + ) + + self._conn.commit() + + def __enter__(self) -> "EmbeddingStore": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: # type: ignore[no-untyped-def] + self.close() + + def close(self) -> None: + self._conn.close() + + def embed_nodes(self, nodes: list[GraphNode], batch_size: int = 64) -> int: + """Compute and store embeddings for a list of nodes.""" + if not self.provider: + return 0 + + # Filter to nodes that need embedding + to_embed: list[tuple[GraphNode, str, str]] = [] + provider_name = self.provider.name + + for node in nodes: + if node.kind == "File": + continue + text = _node_to_text(node) + text_hash = hashlib.sha256(text.encode()).hexdigest() + + existing = self._conn.execute( + "SELECT text_hash, provider FROM embeddings WHERE qualified_name = ?", + (node.qualified_name,), + ).fetchone() + + # Re-embed if text changed OR provider changed + if (existing and existing["text_hash"] == text_hash + and existing["provider"] == provider_name): + continue + to_embed.append((node, text, text_hash)) + + if not to_embed: + return 0 + + # Encode in batches + texts = [t for _, t, _ in to_embed] + vectors = self.provider.embed(texts) + + for (node, _text, text_hash), vec in zip(to_embed, vectors): + blob = _encode_vector(vec) + self._conn.execute( + """INSERT OR REPLACE INTO embeddings (qualified_name, vector, text_hash, provider) + VALUES (?, ?, ?, ?)""", + (node.qualified_name, blob, text_hash, provider_name), + ) + + self._conn.commit() + return len(to_embed) + + def search(self, query: str, limit: int = 20) -> list[tuple[str, float]]: + """Search for nodes by semantic similarity.""" + if not self.provider: + return [] + + provider_name = self.provider.name + query_vec = self.provider.embed_query(query) + + # Process in chunks, only matching current provider + scored: list[tuple[str, float]] = [] + cursor = self._conn.execute( + "SELECT qualified_name, vector FROM embeddings WHERE provider = ?", + (provider_name,), + ) + chunk_size = 500 + while True: + rows = cursor.fetchmany(chunk_size) + if not rows: + break + for row in rows: + vec = _decode_vector(row["vector"]) + sim = _cosine_similarity(query_vec, vec) + scored.append((row["qualified_name"], sim)) + + scored.sort(key=lambda x: x[1], reverse=True) + return scored[:limit] + + def remove_node(self, qualified_name: str) -> None: + self._conn.execute( + "DELETE FROM embeddings WHERE qualified_name = ?", (qualified_name,) + ) + self._conn.commit() + + def count(self) -> int: + return self._conn.execute("SELECT COUNT(*) FROM embeddings").fetchone()[0] + + +def embed_all_nodes(graph_store: GraphStore, embedding_store: EmbeddingStore) -> int: + """Embed all non-file nodes in the graph.""" + if not embedding_store.available: + return 0 + + all_files = graph_store.get_all_files() + all_nodes: list[GraphNode] = [] + for f in all_files: + all_nodes.extend(graph_store.get_nodes_by_file(f)) + + return embedding_store.embed_nodes(all_nodes) + + +def semantic_search( + query: str, + graph_store: GraphStore, + embedding_store: EmbeddingStore, + limit: int = 20, +) -> list[dict[str, Any]]: + """Search nodes using vector similarity, falling back to keyword search.""" + if embedding_store.available and embedding_store.count() > 0: + results = embedding_store.search(query, limit=limit) + output = [] + for qn, score in results: + node = graph_store.get_node(qn) + if node: + d = node_to_dict(node) + d["similarity_score"] = round(score, 4) + output.append(d) + return output + + # Fallback to keyword search + nodes = graph_store.search_nodes(query, limit=limit) + return [node_to_dict(n) for n in nodes] diff --git a/code_review_graph/enrich.py b/code_review_graph/enrich.py new file mode 100644 index 0000000..f95c334 --- /dev/null +++ b/code_review_graph/enrich.py @@ -0,0 +1,303 @@ +"""PreToolUse search enrichment for Claude Code hooks. + +Intercepts Grep/Glob/Bash/Read tool calls and enriches them with +structural context from the code knowledge graph: callers, callees, +execution flows, community membership, and test coverage. +""" + +from __future__ import annotations + +import json +import logging +import os +import re +import sys +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + +# Flags that consume the next token in grep/rg commands +_RG_FLAGS_WITH_VALUES = frozenset({ + "-e", "-f", "-m", "-A", "-B", "-C", "-g", "--glob", + "-t", "--type", "--include", "--exclude", "--max-count", + "--max-depth", "--max-filesize", "--color", "--colors", + "--context-separator", "--field-match-separator", + "--path-separator", "--replace", "--sort", "--sortr", +}) + + +def extract_pattern(tool_name: str, tool_input: dict[str, Any]) -> str | None: + """Extract a search pattern from a tool call's input. + + Returns None if no meaningful pattern can be extracted. + """ + if tool_name == "Grep": + return tool_input.get("pattern") + + if tool_name == "Glob": + raw = tool_input.get("pattern", "") + # Extract meaningful name from glob: "**/auth*.ts" -> "auth" + # Skip pure extension globs like "**/*.ts" + match = re.search(r"[*/]([a-zA-Z][a-zA-Z0-9_]{2,})", raw) + return match.group(1) if match else None + + if tool_name == "Bash": + cmd = tool_input.get("command", "") + if not re.search(r"\brg\b|\bgrep\b", cmd): + return None + tokens = cmd.split() + found_cmd = False + skip_next = False + for token in tokens: + if skip_next: + skip_next = False + continue + if not found_cmd: + if re.search(r"\brg$|\bgrep$", token): + found_cmd = True + continue + if token.startswith("-"): + if token in _RG_FLAGS_WITH_VALUES: + skip_next = True + continue + cleaned = token.strip("'\"") + return cleaned if len(cleaned) >= 3 else None + return None + + return None + + +def _make_relative(file_path: str, repo_root: str) -> str: + """Make a file path relative to repo_root for display.""" + try: + return str(Path(file_path).relative_to(repo_root)) + except ValueError: + return file_path + + +def _get_community_name(conn: Any, community_id: int) -> str: + """Fetch a community name by ID.""" + row = conn.execute( + "SELECT name FROM communities WHERE id = ?", (community_id,) + ).fetchone() + return row["name"] if row else "" + + +def _get_flow_names_for_node(conn: Any, node_id: int) -> list[str]: + """Fetch execution flow names that a node participates in (max 3).""" + rows = conn.execute( + "SELECT f.name FROM flow_memberships fm " + "JOIN flows f ON fm.flow_id = f.id " + "WHERE fm.node_id = ? LIMIT 3", + (node_id,), + ).fetchall() + return [r["name"] for r in rows] + + +def _format_node_context( + node: Any, + store: Any, + conn: Any, + repo_root: str, +) -> list[str]: + """Format a single node's structural context as plain text lines.""" + from .graph import GraphNode + assert isinstance(node, GraphNode) + + qn = node.qualified_name + loc = _make_relative(node.file_path, repo_root) + if node.line_start: + loc = f"{loc}:{node.line_start}" + + header = f"{node.name} ({loc})" + + # Community + if node.extra.get("community_id"): + cname = _get_community_name(conn, node.extra["community_id"]) + if cname: + header += f" [{cname}]" + else: + # Check via direct query + row = conn.execute( + "SELECT community_id FROM nodes WHERE id = ?", (node.id,) + ).fetchone() + if row and row["community_id"]: + cname = _get_community_name(conn, row["community_id"]) + if cname: + header += f" [{cname}]" + + lines = [header] + + # Callers (max 5, deduplicated) + callers: list[str] = [] + seen: set[str] = set() + for e in store.get_edges_by_target(qn): + if e.kind == "CALLS" and len(callers) < 5: + c = store.get_node(e.source_qualified) + if c and c.name not in seen: + seen.add(c.name) + callers.append(c.name) + if callers: + lines.append(f" Called by: {', '.join(callers)}") + + # Callees (max 5, deduplicated) + callees: list[str] = [] + seen.clear() + for e in store.get_edges_by_source(qn): + if e.kind == "CALLS" and len(callees) < 5: + c = store.get_node(e.target_qualified) + if c and c.name not in seen: + seen.add(c.name) + callees.append(c.name) + if callees: + lines.append(f" Calls: {', '.join(callees)}") + + # Execution flows + flow_names = _get_flow_names_for_node(conn, node.id) + if flow_names: + lines.append(f" Flows: {', '.join(flow_names)}") + + # Tests + tests: list[str] = [] + for e in store.get_edges_by_target(qn): + if e.kind == "TESTED_BY" and len(tests) < 3: + t = store.get_node(e.source_qualified) + if t: + tests.append(t.name) + if tests: + lines.append(f" Tests: {', '.join(tests)}") + + return lines + + +def enrich_search(pattern: str, repo_root: str) -> str: + """Search the graph for pattern and return enriched context.""" + from .graph import GraphStore + from .search import _fts_search + + db_path = Path(repo_root) / ".code-review-graph" / "graph.db" + if not db_path.exists(): + return "" + + store = GraphStore(db_path) + try: + conn = store._conn + + fts_results = _fts_search(conn, pattern, limit=8) + if not fts_results: + return "" + + all_lines: list[str] = [] + count = 0 + for node_id, _score in fts_results: + if count >= 5: + break + node = store.get_node_by_id(node_id) + if not node or node.is_test: + continue + node_lines = _format_node_context(node, store, conn, repo_root) + all_lines.extend(node_lines) + all_lines.append("") + count += 1 + + if not all_lines: + return "" + + header = f'[code-review-graph] {count} symbol(s) matching "{pattern}":\n' + return header + "\n".join(all_lines) + finally: + store.close() + + +def enrich_file_read(file_path: str, repo_root: str) -> str: + """Enrich a file read with structural context for functions in that file.""" + from .graph import GraphStore + + db_path = Path(repo_root) / ".code-review-graph" / "graph.db" + if not db_path.exists(): + return "" + + store = GraphStore(db_path) + try: + conn = store._conn + nodes = store.get_nodes_by_file(file_path) + if not nodes: + # Try with resolved path + try: + resolved = str(Path(file_path).resolve()) + nodes = store.get_nodes_by_file(resolved) + except (OSError, ValueError): + pass + if not nodes: + return "" + + # Filter to functions/classes/types (skip File nodes), limit to 10 + interesting = [ + n for n in nodes + if n.kind in ("Function", "Class", "Type", "Test") + ][:10] + + if not interesting: + return "" + + all_lines: list[str] = [] + for node in interesting: + node_lines = _format_node_context(node, store, conn, repo_root) + all_lines.extend(node_lines) + all_lines.append("") + + rel_path = _make_relative(file_path, repo_root) + header = ( + f"[code-review-graph] {len(interesting)} symbol(s) in {rel_path}:\n" + ) + return header + "\n".join(all_lines) + finally: + store.close() + + +def run_hook() -> None: + """Entry point for the enrich CLI subcommand. + + Reads Claude Code hook JSON from stdin, extracts the search pattern, + queries the graph, and outputs hookSpecificOutput JSON to stdout. + """ + try: + hook_input = json.load(sys.stdin) + except (json.JSONDecodeError, ValueError): + return + + tool_name = hook_input.get("tool_name", "") + tool_input = hook_input.get("tool_input", {}) + cwd = hook_input.get("cwd", os.getcwd()) + + # Find repo root by walking up from cwd + from .incremental import find_project_root + + repo_root = str(find_project_root(Path(cwd))) + db_path = Path(repo_root) / ".code-review-graph" / "graph.db" + if not db_path.exists(): + return + + # Dispatch + context = "" + if tool_name == "Read": + fp = tool_input.get("file_path", "") + if fp: + context = enrich_file_read(fp, repo_root) + else: + pattern = extract_pattern(tool_name, tool_input) + if not pattern or len(pattern) < 3: + return + context = enrich_search(pattern, repo_root) + + if not context: + return + + response = { + "hookSpecificOutput": { + "hookEventName": "PreToolUse", + "additionalContext": context, + } + } + json.dump(response, sys.stdout) diff --git a/code_review_graph/eval/__init__.py b/code_review_graph/eval/__init__.py new file mode 100644 index 0000000..c69cdbf --- /dev/null +++ b/code_review_graph/eval/__init__.py @@ -0,0 +1,33 @@ +"""Evaluation framework for code-review-graph. + +Provides scoring metrics (token efficiency, MRR, precision/recall), +benchmark runners, and report generators for benchmarking graph-based code reviews. +""" + +from __future__ import annotations + +from .reporter import generate_full_report, generate_markdown_report, generate_readme_tables +from .scorer import compute_mrr, compute_precision_recall, compute_token_efficiency + + +def __getattr__(name: str): + """Lazy-import runner functions (require pyyaml).""" + _runner_names = {"load_all_configs", "load_config", "run_eval", "write_csv"} + if name in _runner_names: + from . import runner + return getattr(runner, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + "compute_mrr", + "compute_precision_recall", + "compute_token_efficiency", + "generate_full_report", + "generate_markdown_report", + "generate_readme_tables", + "load_all_configs", + "load_config", + "run_eval", + "write_csv", +] diff --git a/code_review_graph/eval/benchmarks/__init__.py b/code_review_graph/eval/benchmarks/__init__.py new file mode 100644 index 0000000..aff13f3 --- /dev/null +++ b/code_review_graph/eval/benchmarks/__init__.py @@ -0,0 +1 @@ +"""Benchmark modules for the evaluation framework.""" diff --git a/code_review_graph/eval/benchmarks/agent_baseline.py b/code_review_graph/eval/benchmarks/agent_baseline.py new file mode 100644 index 0000000..25ce181 --- /dev/null +++ b/code_review_graph/eval/benchmarks/agent_baseline.py @@ -0,0 +1,193 @@ +"""Agent baseline benchmark: grep-and-read-top-k versus a graph query. + +The whole-corpus baseline in the standalone token benchmark is an upper +bound no real agent pays: a competent agent greps for identifiers from the +question and reads only the best-matching files. This benchmark measures +that realistic baseline: + +1. Derive search terms from the question (identifier-shaped tokens via + ``search.extract_query_identifiers`` plus plain keywords). +2. Pure-python grep over the corpus (no external ``rg``/``grep`` binary), + ranking files by total case-insensitive match count. +3. Read the top-k files (k=3) and token-count them with the chars/4 utility + (``token_benchmark.estimate_tokens``) as ``baseline_tokens``. +4. Compare against the graph-query cost for the same question — hybrid + search hits plus one hop of neighbor edges, the same accounting used by + ``code_review_graph/token_benchmark.py``. + +Questions come from ``agent_questions:`` in the repo config, falling back to +the ``search_queries`` query strings when absent. + +Failure semantics match the other benchmarks: a thrown search is recorded +with ``status="error"`` and excluded from aggregates; rows where either side +of the ratio is zero get ``status="no_graph_results"`` / +``status="no_baseline_match"`` and are likewise excluded. +""" + +from __future__ import annotations + +import logging +import statistics +from collections.abc import Iterator +from pathlib import Path + +from code_review_graph.token_benchmark import estimate_tokens + +logger = logging.getLogger(__name__) + +DEFAULT_TOP_K = 3 + +_SOURCE_EXTS = ( + ".py", ".js", ".ts", ".tsx", ".go", ".rs", ".java", + ".c", ".cpp", ".h", ".rb", ".php", ".swift", ".kt", +) + +_SKIP_DIRS = { + ".git", ".hg", ".svn", "node_modules", "__pycache__", + ".code-review-graph", ".venv", "venv", "dist", "build", +} + +_STOPWORDS = { + "how", "does", "do", "the", "a", "an", "is", "are", "was", "what", + "where", "when", "which", "who", "why", "and", "or", "in", "on", "of", + "to", "for", "with", "via", "into", "from", "this", "that", "it", "its", +} + + +def derive_search_terms(question: str) -> list[str]: + """Derive lowercase grep terms: identifiers first, then plain keywords. + + Identifier-shaped tokens (``Client.request``, ``get_users``, ``APIRoute``) + are extracted via ``search.extract_query_identifiers``; remaining words of + 3+ characters that are not stopwords are appended. Order is deterministic. + """ + from code_review_graph.search import extract_query_identifiers + + terms: list[str] = [] + seen: set[str] = set() + for ident in extract_query_identifiers(question): + if ident not in seen: + seen.add(ident) + terms.append(ident) + for word in question.split(): + w = word.strip(".,;:!?\"'()[]{}`").lower() + if len(w) >= 3 and w not in _STOPWORDS and w not in seen: + seen.add(w) + terms.append(w) + return terms + + +def iter_source_files(repo_path: Path) -> Iterator[Path]: + """Yield source files under *repo_path*, skipping vendored/VCS dirs.""" + for path in sorted(repo_path.rglob("*")): + if path.suffix not in _SOURCE_EXTS or not path.is_file(): + continue + if any(part in _SKIP_DIRS for part in path.parts): + continue + yield path + + +def grep_rank( + repo_path: Path, terms: list[str], k: int = DEFAULT_TOP_K, +) -> list[tuple[str, int]]: + """Rank source files by total case-insensitive term matches; take top-k. + + Pure python — no external grep/rg dependency. Deterministic: ties break + on the relative path. Files with zero matches are dropped. + """ + lowered = [t.lower() for t in terms if t] + if not lowered: + return [] + scores: list[tuple[str, int]] = [] + for path in iter_source_files(repo_path): + try: + text = path.read_text(encoding="utf-8", errors="replace").lower() + except OSError: + continue + count = sum(text.count(term) for term in lowered) + if count > 0: + scores.append((str(path.relative_to(repo_path)), count)) + scores.sort(key=lambda item: (-item[1], item[0])) + return scores[:k] + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run the agent baseline benchmark for one repo.""" + questions = list(config.get("agent_questions") or []) + if not questions: + questions = [sq["query"] for sq in config.get("search_queries", [])] + + k = int(config.get("agent_baseline_top_k", DEFAULT_TOP_K)) + results: list[dict] = [] + + for question in questions: + terms = derive_search_terms(question) + top = grep_rank(repo_path, terms, k=k) + baseline_tokens = 0 + for rel, _count in top: + try: + baseline_tokens += estimate_tokens( + (repo_path / rel).read_text(encoding="utf-8", errors="replace") + ) + except OSError: + continue + + row: dict = { + "repo": config["name"], + "question": question, + "terms": " ".join(terms), + "files_matched": len(top), + "top_files": ";".join(rel for rel, _ in top), + "baseline_tokens": baseline_tokens, + "graph_tokens": "", + "baseline_to_graph_ratio": "", + "status": "ok", + "error": "", + } + + try: + from code_review_graph.search import hybrid_search + hits = hybrid_search(store, question, limit=5) + except Exception as exc: + logger.warning("hybrid_search failed on %r: %s", question, exc) + row["status"] = "error" + row["error"] = str(exc)[:200] + results.append(row) + continue + + # Same accounting as the standalone token benchmark: search hits + # plus up to 5 outgoing edges of neighbor context per hit. + graph_tokens = 0 + for hit in hits: + graph_tokens += estimate_tokens(str(hit)) + qn = hit.get("qualified_name", "") + for edge in store.get_edges_by_source(qn)[:5]: + graph_tokens += estimate_tokens(str(edge)) + + row["graph_tokens"] = graph_tokens + if baseline_tokens > 0 and graph_tokens > 0: + row["baseline_to_graph_ratio"] = round(baseline_tokens / graph_tokens, 1) + elif graph_tokens == 0: + row["status"] = "no_graph_results" + else: + row["status"] = "no_baseline_match" + results.append(row) + + return results + + +def aggregate(results: list[dict]) -> dict: + """Aggregate over rows where both sides of the comparison exist.""" + ok = [r for r in results if r.get("status") == "ok"] + ratios = [float(r["baseline_to_graph_ratio"]) for r in ok] + return { + "total_rows": len(results), + "ok_rows": len(ok), + "error_rows": sum(1 for r in results if r.get("status") == "error"), + "median_baseline_to_graph_ratio": ( + round(statistics.median(ratios), 1) if ratios else None + ), + "mean_baseline_to_graph_ratio": ( + round(statistics.mean(ratios), 1) if ratios else None + ), + } diff --git a/code_review_graph/eval/benchmarks/build_performance.py b/code_review_graph/eval/benchmarks/build_performance.py new file mode 100644 index 0000000..7cfe3ba --- /dev/null +++ b/code_review_graph/eval/benchmarks/build_performance.py @@ -0,0 +1,60 @@ +"""Build performance benchmark: measures timing of graph operations.""" + +from __future__ import annotations + +import logging +import time +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run build performance benchmark.""" + stats = store.get_stats() + + # Time flow detection + try: + from code_review_graph.flows import store_flows, trace_flows + t0 = time.perf_counter() + flows = trace_flows(store) + store_flows(store, flows) + flow_time = time.perf_counter() - t0 + except Exception as exc: + logger.warning("Flow detection failed: %s", exc) + flow_time = 0.0 + + # Time community detection + try: + from code_review_graph.communities import detect_communities, store_communities + t0 = time.perf_counter() + comms = detect_communities(store) + store_communities(store, comms) + community_time = time.perf_counter() - t0 + except Exception as exc: + logger.warning("Community detection failed: %s", exc) + community_time = 0.0 + + # Time search (average of queries) + search_times: list[float] = [] + for sq in config.get("search_queries", [])[:10]: + t0 = time.perf_counter() + store.search_nodes(sq["query"], limit=20) + search_times.append(time.perf_counter() - t0) + + avg_search_ms = round( + sum(search_times) / max(len(search_times), 1) * 1000, 1 + ) + + return [{ + "repo": config["name"], + "file_count": stats.files_count, + "node_count": stats.total_nodes, + "edge_count": stats.total_edges, + "flow_detection_seconds": round(flow_time, 3), + "community_detection_seconds": round(community_time, 3), + "search_avg_ms": avg_search_ms, + "nodes_per_second": round( + stats.total_nodes / max(flow_time, 0.001) + ), + }] diff --git a/code_review_graph/eval/benchmarks/flow_completeness.py b/code_review_graph/eval/benchmarks/flow_completeness.py new file mode 100644 index 0000000..c99d80e --- /dev/null +++ b/code_review_graph/eval/benchmarks/flow_completeness.py @@ -0,0 +1,36 @@ +"""Flow completeness benchmark: evaluates entry point detection and flow tracing.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run flow completeness benchmark.""" + from code_review_graph.flows import store_flows, trace_flows + + flows = trace_flows(store) + count = store_flows(store, flows) + + # Get detected entry point names + detected_entries = set() + for flow in flows: + detected_entries.add(flow.get("entry_point") or flow.get("name", "")) + + known = set(config.get("entry_points", [])) + found = sum(1 for ep in known if any(ep in d for d in detected_entries)) + + depths = [f.get("depth", 0) for f in flows] + + return [{ + "repo": config["name"], + "known_entry_points": len(known), + "detected_entry_points": found, + "recall": round(found / max(len(known), 1), 3), + "detected_flows": count, + "avg_flow_depth": round(sum(depths) / max(len(depths), 1), 1), + "max_flow_depth": max(depths, default=0), + }] diff --git a/code_review_graph/eval/benchmarks/impact_accuracy.py b/code_review_graph/eval/benchmarks/impact_accuracy.py new file mode 100644 index 0000000..8dfecdb --- /dev/null +++ b/code_review_graph/eval/benchmarks/impact_accuracy.py @@ -0,0 +1,220 @@ +"""Impact accuracy benchmark: measures precision/recall of change impact analysis. + +Two ground-truth modes are emitted side by side (``ground_truth_mode`` column): + +- **graph-derived (circular — upper bound)** — the historical mode. Ground + truth is the changed files plus files with CALLS/IMPORTS_FROM edges into + them, i.e. derived from the same graph the predictor traverses. Recall in + this mode is an upper bound by construction, not independent evidence. +- **co-change (same commit, seed excluded)** — the honest mode. The predictor + is seeded with a single changed file and graded against the *other* files + the author actually touched in the same commit. The ground truth comes from + git history, not from the graph. + +Failure semantics: if ``analyze_changes`` throws, the row is recorded with +``status="error"`` and empty metric fields — it stays in the CSV but is +excluded from aggregates. (Previously a failure silently set +``predicted = set(changed)``, guaranteeing a fake recall of 1.0.) +""" + +from __future__ import annotations + +import logging +import statistics +import subprocess +from pathlib import Path + +logger = logging.getLogger(__name__) + +MODE_GRAPH_DERIVED = "graph-derived (circular — upper bound)" +MODE_CO_CHANGE = "co-change (same commit, seed excluded)" + + +def _get_changed_files(repo_path: Path, sha: str) -> list[str]: + """Get list of changed files for a commit.""" + result = subprocess.run( + ["git", "diff", "--name-only", f"{sha}~1", sha], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + if result.returncode != 0: + result = subprocess.run( + ["git", "diff", "--name-only", "HEAD~1", "HEAD"], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()] + + +def _files_from_analysis(analysis: dict) -> set[str]: + """Extract predicted file paths from an ``analyze_changes`` result.""" + predicted: set[str] = set() + for f in analysis.get("changed_functions", []): + if isinstance(f, dict) and "file_path" in f: + predicted.add(f["file_path"]) + elif isinstance(f, dict) and "file" in f: + predicted.add(f["file"]) + for flow in analysis.get("affected_flows", []): + if isinstance(flow, dict): + for node in flow.get("nodes", []): + if isinstance(node, dict) and "file_path" in node: + predicted.add(node["file_path"]) + return predicted + + +def _graph_neighbor_files(store, files: list[str]) -> set[str]: + """Files with CALLS/IMPORTS_FROM edges into any node of *files* (one hop).""" + out: set[str] = set() + for f in files: + for node in store.get_nodes_by_file(f): + for edge in store.get_edges_by_target(node.qualified_name): + if edge.kind in ("CALLS", "IMPORTS_FROM"): + src_qual = edge.source_qualified + src_file = src_qual.split("::")[0] if "::" in src_qual else "" + if src_file: + out.add(src_file) + return out + + +def _base_row(repo: str, sha: str, mode: str, seed: str) -> dict: + return { + "repo": repo, + "commit": sha, + "ground_truth_mode": mode, + "seed_file": seed, + "predicted_files": "", + "actual_files": "", + "true_positives": "", + "precision": "", + "recall": "", + "f1": "", + "status": "ok", + "error": "", + } + + +def _scored_row( + repo: str, sha: str, mode: str, seed: str, + predicted: set[str], actual: set[str], +) -> dict: + tp = len(predicted & actual) + precision = tp / max(len(predicted), 1) + recall = tp / max(len(actual), 1) + f1 = 2 * precision * recall / max(precision + recall, 0.001) + row = _base_row(repo, sha, mode, seed) + row.update({ + "predicted_files": len(predicted), + "actual_files": len(actual), + "true_positives": tp, + "precision": round(precision, 3), + "recall": round(recall, 3), + "f1": round(f1, 3), + }) + return row + + +def _error_row(repo: str, sha: str, mode: str, seed: str, exc: Exception) -> dict: + row = _base_row(repo, sha, mode, seed) + row["status"] = "error" + row["error"] = str(exc)[:200] + return row + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run impact accuracy benchmark (both ground-truth modes).""" + from code_review_graph.changes import analyze_changes + + results = [] + repo = config["name"] + for tc in config.get("test_commits", []): + sha = tc["sha"] + changed = _get_changed_files(repo_path, sha) + if not changed: + continue + + # --- Mode 1: graph-derived ground truth (circular — upper bound) --- + try: + analysis = analyze_changes( + store, changed, repo_root=str(repo_path), base=sha + "~1", + ) + except Exception as exc: + # Old behaviour set predicted = set(changed) here, which + # guarantees recall 1.0 on a *failed* run. Mark failed instead. + logger.warning("analyze_changes failed on %s: %s", sha, exc) + results.append(_error_row(repo, sha, MODE_GRAPH_DERIVED, "", exc)) + analysis = None + + if analysis is not None: + predicted = set(changed) | _files_from_analysis(analysis) + actual = set(changed) | _graph_neighbor_files(store, changed) + results.append( + _scored_row(repo, sha, MODE_GRAPH_DERIVED, "", predicted, actual) + ) + + # --- Mode 2: co-change ground truth (honest) --- + # Seed the predictor with a single changed file and grade against + # the other files the author touched in the same commit. Note the + # seed analysis deliberately gets no repo_root/diff: it must only + # see the seed file, never the full commit diff. + seed = sorted(changed)[0] + co_actual = set(changed) - {seed} + if not co_actual: + row = _base_row(repo, sha, MODE_CO_CHANGE, seed) + row["status"] = "skipped" + row["error"] = "single-file commit: no co-changed files to grade against" + results.append(row) + continue + + try: + seed_analysis = analyze_changes(store, [seed]) + except Exception as exc: + logger.warning("analyze_changes (seed=%s) failed on %s: %s", seed, sha, exc) + results.append(_error_row(repo, sha, MODE_CO_CHANGE, seed, exc)) + continue + + co_predicted = _files_from_analysis(seed_analysis) + co_predicted |= _graph_neighbor_files(store, [seed]) + co_predicted.discard(seed) + results.append( + _scored_row(repo, sha, MODE_CO_CHANGE, seed, co_predicted, co_actual) + ) + + return results + + +def aggregate(results: list[dict]) -> dict: + """Per-mode means over successful rows only. + + Error/skipped rows stay in the CSV but never contribute to a number. + """ + out: dict = { + "total_rows": len(results), + "error_rows": sum(1 for r in results if r.get("status") == "error"), + "skipped_rows": sum(1 for r in results if r.get("status") == "skipped"), + } + for key, mode in ( + ("graph_derived", MODE_GRAPH_DERIVED), + ("co_change", MODE_CO_CHANGE), + ): + rows = [ + r for r in results + if r.get("ground_truth_mode") == mode and r.get("status") == "ok" + ] + out[key] = { + "ok_rows": len(rows), + "mean_precision": ( + round(statistics.mean(float(r["precision"]) for r in rows), 3) + if rows else None + ), + "mean_recall": ( + round(statistics.mean(float(r["recall"]) for r in rows), 3) + if rows else None + ), + "mean_f1": ( + round(statistics.mean(float(r["f1"]) for r in rows), 3) + if rows else None + ), + } + return out diff --git a/code_review_graph/eval/benchmarks/multi_hop_retrieval.py b/code_review_graph/eval/benchmarks/multi_hop_retrieval.py new file mode 100644 index 0000000..316b82e --- /dev/null +++ b/code_review_graph/eval/benchmarks/multi_hop_retrieval.py @@ -0,0 +1,125 @@ +"""Multi-hop retrieval benchmark. + +Tests a two-step tool chain that mimics how an LLM agent actually uses the +graph for complex tasks: + + 1. ``hybrid_search(nl_query)`` to find a starting anchor from a natural- + language question. + 2. ``query_graph(pattern, target=anchor)`` to traverse one hop along the + requested edge kind (callers_of / callees_of / tests_for / ...). + +For each task the benchmark records: + +- ``anchor_found`` — did semantic search return a node whose qualified_name + ends with the expected suffix in the top-K? +- ``anchor_rank`` — index in the search result list (lower is better). +- ``neighbor_count`` — number of neighbors returned by the traversal. +- ``neighbor_recall`` — fraction of ``expected_neighbor_names`` that appear + among the neighbor names. +- ``score`` — ``int(anchor_found) * neighbor_recall``. Range 0–1. + +Tasks are defined per-config under ``multi_hop_tasks:`` in +``code_review_graph/eval/configs/*.yaml``. See +``docs/REPRODUCING.md`` for the schema and the curated canonical task set. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +def _name_set(rows: list[dict[str, Any]]) -> set[str]: + out: set[str] = set() + for r in rows: + name = (r.get("name") or "").lower() + if name: + out.add(name) + return out + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run the multi-hop retrieval benchmark for one repo.""" + # Imports are local so an import-time failure in one optional benchmark + # does not poison the whole runner. + from code_review_graph.search import hybrid_search + from code_review_graph.tools.query import query_graph + + repo_root = str(repo_path) + results: list[dict] = [] + + for task in config.get("multi_hop_tasks", []): + task_id = task["id"] + nl_query = task["nl_query"] + suffix = task["anchor_qualified_suffix"].lower() + traversal = task.get("traversal_pattern", "callers_of") + expected = [e.lower() for e in task.get("expected_neighbor_names", [])] + k = int(task.get("k", 10)) + + # Step 1 — semantic search + try: + hits = hybrid_search(store, nl_query, limit=k) + except Exception as exc: # noqa: BLE001 — benchmark must not abort the runner + logger.warning("hybrid_search failed on %s: %s", task_id, exc) + hits = [] + + anchor = None + anchor_rank = -1 + for i, h in enumerate(hits): + qn = (h.get("qualified_name") or "").lower() + if qn.endswith(suffix): + anchor = h + anchor_rank = i + break + + if anchor is None: + results.append({ + "repo": config["name"], + "task_id": task_id, + "nl_query": nl_query, + "anchor_found": False, + "anchor_rank": -1, + "neighbor_count": 0, + "expected_count": len(expected), + "matched_count": 0, + "neighbor_recall": 0.0, + "score": 0.0, + }) + continue + + # Step 2 — single-hop graph traversal from the anchor + try: + trav = query_graph( + pattern=traversal, + target=anchor["qualified_name"], + repo_root=repo_root, + detail_level="standard", + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "query_graph(%s) failed on %s: %s", traversal, task_id, exc, + ) + trav = {} + + rows = trav.get("data") or trav.get("results") or [] + names = _name_set(rows) + matched = sum(1 for e in expected if e in names) + recall = matched / len(expected) if expected else 0.0 + + results.append({ + "repo": config["name"], + "task_id": task_id, + "nl_query": nl_query, + "anchor_found": True, + "anchor_rank": anchor_rank, + "neighbor_count": len(rows), + "expected_count": len(expected), + "matched_count": matched, + "neighbor_recall": round(recall, 3), + "score": round(recall, 3), + }) + + return results diff --git a/code_review_graph/eval/benchmarks/search_quality.py b/code_review_graph/eval/benchmarks/search_quality.py new file mode 100644 index 0000000..3cf80bb --- /dev/null +++ b/code_review_graph/eval/benchmarks/search_quality.py @@ -0,0 +1,59 @@ +"""Search quality benchmark: measures search result ranking via MRR.""" + +from __future__ import annotations + +import logging +import sqlite3 +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run search quality benchmark.""" + results = [] + for sq in config.get("search_queries", []): + query = sq["query"] + expected = sq["expected"] + + try: + from code_review_graph.search import hybrid_search + search_results = hybrid_search(store, query, limit=20) + except (ImportError, sqlite3.OperationalError) as exc: + logger.debug("hybrid_search unavailable, using fallback: %s", exc) + # Fallback to basic search + search_results = [ + {"qualified_name": n.qualified_name} + for n in store.search_nodes(query, limit=20) + ] + + rank = 0 + for i, r in enumerate(search_results): + if isinstance(r, dict): + qn = r.get("qualified_name", "") + elif hasattr(r, "qualified_name"): + qn = r.qualified_name + else: + qn = "" + qn_lower = qn.lower() + exp_lower = expected.lower() + # Match if expected is substring of qn, qn is substring of expected, + # or the name part after :: matches + exp_name = expected.rsplit("::", 1)[-1] if "::" in expected else expected + qn_name = qn.rsplit("::", 1)[-1] if "::" in qn else qn + if ( + exp_lower in qn_lower + or qn_lower in exp_lower + or exp_name.lower() == qn_name.lower() + ): + rank = i + 1 + break + + results.append({ + "repo": config["name"], + "query": query, + "expected": expected, + "rank": rank, + "reciprocal_rank": round(1.0 / rank if rank > 0 else 0.0, 3), + }) + return results diff --git a/code_review_graph/eval/benchmarks/token_efficiency.py b/code_review_graph/eval/benchmarks/token_efficiency.py new file mode 100644 index 0000000..6b7c4bf --- /dev/null +++ b/code_review_graph/eval/benchmarks/token_efficiency.py @@ -0,0 +1,143 @@ +"""Token efficiency benchmark: compares naive, standard, and graph-based token counts. + +Failure semantics: if ``get_review_context`` throws, the row is recorded with +``status="error"`` and empty metric fields. It stays in the CSV for forensics +but is excluded from every aggregate — a failed tool call is not a +measurement. (Previously a failure silently produced ``graph_tokens=0`` and +``ratio = naive / 1``, inflating the results.) +""" + +from __future__ import annotations + +import json +import logging +import statistics +import subprocess +from pathlib import Path + +logger = logging.getLogger(__name__) + + +def _count_tokens(text: str) -> int: + """Approximate token count (1 token ~ 4 chars).""" + return len(text) // 4 + + +def _get_changed_files(repo_path: Path, sha: str) -> list[str]: + """Get list of changed files for a commit.""" + result = subprocess.run( + ["git", "diff", "--name-only", f"{sha}~1", sha], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + if result.returncode != 0: + # Fallback: diff against parent + result = subprocess.run( + ["git", "diff", "--name-only", "HEAD~1", "HEAD"], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + return [f.strip() for f in result.stdout.strip().splitlines() if f.strip()] + + +def _count_file_tokens(repo_path: Path, files: list[str]) -> int: + """Count tokens from full file contents (naive approach).""" + total = 0 + for f in files: + fp = repo_path / f + if fp.is_file(): + try: + total += _count_tokens(fp.read_text(encoding="utf-8", errors="replace")) + except OSError: + pass + return total + + +def _count_diff_tokens(repo_path: Path, sha: str) -> int: + """Count tokens from git diff output (standard approach).""" + result = subprocess.run( + ["git", "diff", f"{sha}~1", sha], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + if result.returncode != 0: + result = subprocess.run( + ["git", "diff", "HEAD~1", "HEAD"], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + return _count_tokens(result.stdout) + + +def run(repo_path: Path, store, config: dict) -> list[dict]: + """Run token efficiency benchmark.""" + results = [] + for tc in config.get("test_commits", []): + changed = _get_changed_files(repo_path, tc["sha"]) + if not changed: + continue + + naive_tokens = _count_file_tokens(repo_path, changed) + standard_tokens = _count_diff_tokens(repo_path, tc["sha"]) + + row: dict = { + "repo": config["name"], + "commit": tc["sha"], + "description": tc.get("description", ""), + "changed_files": len(changed), + "naive_tokens": naive_tokens, + "standard_tokens": standard_tokens, + "graph_tokens": "", + "naive_to_graph_ratio": "", + "standard_to_graph_ratio": "", + "status": "ok", + "error": "", + } + + # Graph-based: use get_review_context + try: + from code_review_graph.tools import get_review_context + ctx = get_review_context( + changed_files=changed, repo_root=str(repo_path) + ) + graph_tokens = _count_tokens(json.dumps(ctx)) + except Exception as exc: + # A failed tool call is not a measurement. Recording + # graph_tokens=0 used to turn this into ratio = naive/1 — a + # huge fake win. Mark the row failed; aggregate() excludes it. + logger.warning("get_review_context failed on %s: %s", tc["sha"], exc) + row["status"] = "error" + row["error"] = str(exc)[:200] + results.append(row) + continue + + row["graph_tokens"] = graph_tokens + row["naive_to_graph_ratio"] = round(naive_tokens / max(graph_tokens, 1), 1) + row["standard_to_graph_ratio"] = round(standard_tokens / max(graph_tokens, 1), 1) + results.append(row) + return results + + +def aggregate(results: list[dict]) -> dict: + """Aggregate token-efficiency rows, excluding failed measurements. + + Rows with ``status != "ok"`` stay in the CSV for forensics but must not + contribute to any headline number. + """ + ok = [r for r in results if r.get("status") == "ok"] + ratios = [float(r["naive_to_graph_ratio"]) for r in ok] + return { + "total_rows": len(results), + "ok_rows": len(ok), + "error_rows": sum(1 for r in results if r.get("status") == "error"), + "median_naive_to_graph_ratio": ( + round(statistics.median(ratios), 1) if ratios else None + ), + "mean_naive_to_graph_ratio": ( + round(statistics.mean(ratios), 1) if ratios else None + ), + } diff --git a/code_review_graph/eval/configs/code-review-graph.yaml b/code_review_graph/eval/configs/code-review-graph.yaml new file mode 100644 index 0000000..e0fa13e --- /dev/null +++ b/code_review_graph/eval/configs/code-review-graph.yaml @@ -0,0 +1,50 @@ +name: code-review-graph +url: https://github.com/tirth8205/code-review-graph +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. (This config replaces +# the historical "nextjs" entry, which used the same URL but mis-labelled the +# target as a Next.js monorepo.) +commit: 84bde35459c52e1e0c4b25c6c4799743021e0fc7 +language: python +size_category: medium + +test_commits: + - sha: 528801f841e519567ef54d6e52e9b9831d162e1b + description: "feat: add multi-platform MCP server installation support" + changed_files: 3 + - sha: 84bde35459c52e1e0c4b25c6c4799743021e0fc7 + description: "feat: add Google Antigravity platform support for MCP install" + changed_files: 2 + +entry_points: + - "code_review_graph/cli.py::cli" + - "code_review_graph/main.py::main" + +search_queries: + - query: "GraphStore nodes" + expected: "code_review_graph/graph.py::GraphStore" + - query: "parse AST" + expected: "code_review_graph/parser.py::CodeParser" + - query: "full build" + expected: "code_review_graph/incremental.py::full_build" + +multi_hop_tasks: + - id: crg-parse-file-callers + nl_query: "Who invokes the parser entry point on a single source file" + anchor_qualified_suffix: "code_review_graph/parser.py::codeparser.parse_file" + traversal_pattern: callers_of + expected_neighbor_names: ["setup_method"] + k: 10 + - id: crg-upsert-node-callers + nl_query: "Where the graph store inserts or updates a node" + anchor_qualified_suffix: "code_review_graph/graph.py::graphstore.upsert_node" + traversal_pattern: callers_of + expected_neighbor_names: ["store_file_nodes_edges"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does GraphStore upsert_node store a node" + - "Where does full_build parse the repository" + - "How does hybrid_search rank search results" diff --git a/code_review_graph/eval/configs/express.yaml b/code_review_graph/eval/configs/express.yaml new file mode 100644 index 0000000..a3a3d23 --- /dev/null +++ b/code_review_graph/eval/configs/express.yaml @@ -0,0 +1,45 @@ +name: express +url: https://github.com/expressjs/express +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. +commit: b4ab7d65d7724d9309b6faaaf82ad492da2a6d35 +language: javascript +size_category: small + +test_commits: + - sha: 925a1dff1e42f1b393c977b8b77757fcf633e09f + description: "fix: bump qs minimum to ^6.14.2 for CVE-2026-2391" + changed_files: 1 + - sha: b4ab7d65d7724d9309b6faaaf82ad492da2a6d35 + description: "test: include edge case tests for res.type()" + changed_files: 1 + +entry_points: + - "lib/application.js::app.handle" + - "lib/express.js::createApplication" + +search_queries: + - query: "app handle" + expected: "lib/application.js::app" + - query: "response send" + expected: "lib/response.js::res" + - query: "request" + expected: "lib/request.js::req" + +# Express has only one task — JS modules use prototypes + module.exports +# heavily, so most "method" callers are not represented as proper Function +# edges in the graph. createApplication is the cleanest anchor. +multi_hop_tasks: + - id: express-create-application-callees + nl_query: "What express does when constructing an application" + anchor_qualified_suffix: "lib/express.js::createapplication" + traversal_pattern: callees_of + expected_neighbor_names: ["mixin", "create", "init"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does app.handle process the middleware stack" + - "Where does res.send write the response body" + - "How does createApplication initialize an app" diff --git a/code_review_graph/eval/configs/fastapi.yaml b/code_review_graph/eval/configs/fastapi.yaml new file mode 100644 index 0000000..7c748f0 --- /dev/null +++ b/code_review_graph/eval/configs/fastapi.yaml @@ -0,0 +1,48 @@ +name: fastapi +url: https://github.com/tiangolo/fastapi +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. +commit: 0227991a01e61bf5cdd93cc00e9e243f52b47a4a +language: python +size_category: medium + +test_commits: + - sha: fa3588c38c7473aca7536b12d686102de4b0f407 + description: "Fix typo for client_secret in OAuth2 form docstrings" + changed_files: 1 + - sha: 0227991a01e61bf5cdd93cc00e9e243f52b47a4a + description: "Exclude spam comments from statistics in scripts/people.py" + changed_files: 1 + +entry_points: + - "fastapi/applications.py::FastAPI" + - "fastapi/routing.py::APIRouter" + +search_queries: + - query: "FastAPI application" + expected: "fastapi/applications.py::FastAPI" + - query: "APIRoute routing" + expected: "fastapi/routing.py::APIRoute" + - query: "Depends injection" + expected: "fastapi/params.py::Depends" + +multi_hop_tasks: + - id: fastapi-route-handler-callers + nl_query: "How fastapi binds a route handler to an APIRoute" + anchor_qualified_suffix: "fastapi/routing.py::apiroute.get_route_handler" + traversal_pattern: callers_of + expected_neighbor_names: ["__init__"] + k: 10 + - id: fastapi-get-dependant-callers + nl_query: "Where fastapi resolves dependency declarations into a tree" + anchor_qualified_suffix: "fastapi/dependencies/utils.py::get_dependant" + traversal_pattern: callers_of + expected_neighbor_names: ["get_parameterless_sub_dependant", "solve_dependencies"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does include_router register routes on the application" + - "Where does APIRoute build its route handler" + - "How does solve_dependencies resolve Depends parameters" diff --git a/code_review_graph/eval/configs/flask.yaml b/code_review_graph/eval/configs/flask.yaml new file mode 100644 index 0000000..71e1bec --- /dev/null +++ b/code_review_graph/eval/configs/flask.yaml @@ -0,0 +1,50 @@ +name: flask +url: https://github.com/pallets/flask +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. +commit: a29f88ce6f2f9843bd6fcbbfce1390a2071965d6 +language: python +size_category: small + +test_commits: + - sha: fbb6f0bc4c60a0bada0e03c3480d0ccf30a3c1df + description: "all teardown callbacks are called despite errors" + changed_files: 10 + - sha: a29f88ce6f2f9843bd6fcbbfce1390a2071965d6 + description: "document that headers must be set before streaming" + changed_files: 4 + +entry_points: + - "src/flask/app.py::Flask.wsgi_app" + - "src/flask/sansio/app.py::App.add_url_rule" + +search_queries: + - query: "Flask wsgi" + expected: "src/flask/app.py::Flask" + - query: "AppContext globals" + expected: "src/flask/ctx.py::AppContext" + - query: "create logger" + expected: "src/flask/logging.py::create_logger" + +# Multi-hop retrieval tasks (semantic_search → query_graph one-hop) +# See docs/REPRODUCING.md for the schema. +multi_hop_tasks: + - id: flask-dispatch-callers + nl_query: "Where Flask dispatches HTTP requests" + anchor_qualified_suffix: "src/flask/app.py::flask.dispatch_request" + traversal_pattern: callers_of + expected_neighbor_names: ["full_dispatch_request"] + k: 10 + - id: flask-exception-callers + nl_query: "Where Flask handles uncaught exceptions" + anchor_qualified_suffix: "src/flask/app.py::flask.handle_exception" + traversal_pattern: callers_of + expected_neighbor_names: ["wsgi_app"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does dispatch_request route an incoming HTTP request" + - "Where is the AppContext pushed and popped" + - "How does create_logger configure application logging" diff --git a/code_review_graph/eval/configs/gin.yaml b/code_review_graph/eval/configs/gin.yaml new file mode 100644 index 0000000..f0c280f --- /dev/null +++ b/code_review_graph/eval/configs/gin.yaml @@ -0,0 +1,51 @@ +name: gin +url: https://github.com/gin-gonic/gin +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. +commit: 5c00df8afadd06cc5be530dde00fe6d9fa4a2e4a +language: go +size_category: small + +test_commits: + - sha: 052d1a79aafe3f04078a2716f8e77d4340308383 + description: "feat(render): add PDF renderer and tests" + changed_files: 5 + - sha: 472d086af2acd924cb4b9d7be0525f7d790f69bc + description: "fix(tree): panic in findCaseInsensitivePathRec with RedirectFixedPath" + changed_files: 2 + - sha: 5c00df8afadd06cc5be530dde00fe6d9fa4a2e4a + description: "fix(render): write content length in Data.Render" + changed_files: 2 + +entry_points: + - "gin.go::Engine" + - "routergroup.go::RouterGroup" + +search_queries: + - query: "Engine ServeHTTP" + expected: "gin.go::Engine" + - query: "Context request" + expected: "context.go::Context" + - query: "node tree" + expected: "tree.go::node" + +multi_hop_tasks: + - id: gin-serve-http-callees + nl_query: "What does the gin engine do when serving an HTTP request" + anchor_qualified_suffix: "gin.go::engine.servehttp" + traversal_pattern: callees_of + expected_neighbor_names: ["reset"] + k: 10 + - id: gin-context-next-callers + nl_query: "Who advances the gin middleware chain via Context.Next" + anchor_qualified_suffix: "context.go::context.next" + traversal_pattern: callers_of + expected_neighbor_names: ["handleHTTPRequest", "serveError"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does Engine.ServeHTTP route an incoming request" + - "Where does Context.Next advance the middleware chain" + - "How does the node tree match wildcard routes" diff --git a/code_review_graph/eval/configs/httpx.yaml b/code_review_graph/eval/configs/httpx.yaml new file mode 100644 index 0000000..9184c4e --- /dev/null +++ b/code_review_graph/eval/configs/httpx.yaml @@ -0,0 +1,48 @@ +name: httpx +url: https://github.com/encode/httpx +# Pinned to the latest test_commit SHA so the snapshot is deterministic and +# every test_commit below is reachable as an ancestor. +commit: b55d4635701d9dc22928ee647880c76b078ba3f2 +language: python +size_category: small + +test_commits: + - sha: ae1b9f66238f75ced3ced5e4485408435de10768 + description: "Expose FunctionAuth in __all__" + changed_files: 3 + - sha: b55d4635701d9dc22928ee647880c76b078ba3f2 + description: "Upgrade Python type checker mypy" + changed_files: 4 + +entry_points: + - "httpx/_client.py::Client" + - "httpx/_client.py::AsyncClient" + +search_queries: + - query: "Client request" + expected: "httpx/_client.py::Client" + - query: "Response headers" + expected: "httpx/_models.py::Response" + - query: "BaseClient" + expected: "httpx/_client.py::BaseClient" + +multi_hop_tasks: + - id: httpx-client-request-callers + nl_query: "Which HTTP verbs route through the httpx Client.request" + anchor_qualified_suffix: "httpx/_client.py::client.request" + traversal_pattern: callers_of + expected_neighbor_names: ["get", "options", "head", "post", "put", "patch"] + k: 10 + - id: httpx-async-request-tests + nl_query: "Tests covering the httpx async client request method" + anchor_qualified_suffix: "httpx/_client.py::asyncclient.request" + traversal_pattern: callers_of + expected_neighbor_names: ["test_raise_for_status"] + k: 10 + +# Questions for the agent_baseline benchmark (pure-python grep top-k vs graph +# query). See docs/REPRODUCING.md for the methodology. +agent_questions: + - "How does Client.request send an HTTP request" + - "Where are Response headers parsed and decoded" + - "How does BaseClient build request URLs" diff --git a/code_review_graph/eval/reporter.py b/code_review_graph/eval/reporter.py new file mode 100644 index 0000000..3a21985 --- /dev/null +++ b/code_review_graph/eval/reporter.py @@ -0,0 +1,301 @@ +"""Markdown report generator for evaluation benchmark results. + +Takes a list of benchmark result dicts and produces a formatted markdown table +suitable for inclusion in documentation or CI output. +""" + +from __future__ import annotations + +import csv +from pathlib import Path +from typing import Any + + +def generate_markdown_report(results: list[dict[str, Any]]) -> str: + """Generate a markdown report from benchmark results. + + Each result dict should contain at minimum a ``benchmark`` key identifying + the benchmark name, plus any metric keys (e.g. ``ratio``, + ``reduction_percent``, ``mrr``, ``precision``, ``recall``, ``f1``). + + Args: + results: List of result dicts from benchmark runs. + + Returns: + A markdown string containing a summary table and per-benchmark details. + """ + if not results: + return "# Evaluation Report\n\nNo benchmark results to report.\n" + + lines: list[str] = [] + lines.append("# Evaluation Report") + lines.append("") + + # Collect all metric keys across results (excluding 'benchmark') + all_keys: list[str] = [] + seen: set[str] = set() + for r in results: + for k in r: + if k != "benchmark" and k not in seen: + all_keys.append(k) + seen.add(k) + + # Summary table + lines.append("## Summary") + lines.append("") + + header = "| Benchmark | " + " | ".join(all_keys) + " |" + separator = "| --- | " + " | ".join("---" for _ in all_keys) + " |" + lines.append(header) + lines.append(separator) + + for r in results: + name = r.get("benchmark", "unknown") + values = [str(r.get(k, "-")) for k in all_keys] + lines.append(f"| {name} | " + " | ".join(values) + " |") + + lines.append("") + + # Per-benchmark detail sections + lines.append("## Details") + lines.append("") + for r in results: + name = r.get("benchmark", "unknown") + lines.append(f"### {name}") + lines.append("") + for k in all_keys: + v = r.get(k, "-") + lines.append(f"- **{k}**: {v}") + lines.append("") + + return "\n".join(lines) + + +def _read_csvs(results_dir: Path, prefix: str) -> list[dict[str, str]]: + """Read all CSV files matching a prefix from the results directory.""" + rows: list[dict[str, str]] = [] + for p in sorted(results_dir.glob(f"*_{prefix}_*.csv")): + with open(p, newline="") as f: + reader = csv.DictReader(f) + rows.extend(reader) + return rows + + +def _md_table(headers: list[str], rows: list[list[str]]) -> str: + """Build a markdown table from headers and rows.""" + lines = [] + lines.append("| " + " | ".join(headers) + " |") + lines.append("| " + " | ".join("---" for _ in headers) + " |") + for row in rows: + lines.append("| " + " | ".join(row) + " |") + return "\n".join(lines) + + +def generate_full_report(results_dir: str | Path) -> str: + """Generate a full markdown evaluation report from CSV result files. + + Reads all CSV files in *results_dir*, groups them by benchmark type, + and produces a markdown report with methodology notes and per-benchmark + result tables. + + Args: + results_dir: Directory containing CSV result files. + + Returns: + Markdown string with the full report. + """ + results_dir = Path(results_dir) + lines: list[str] = [] + lines.append("# Evaluation Report") + lines.append("") + lines.append("## Methodology") + lines.append("") + lines.append("Benchmarks are run against real open-source repositories.") + lines.append("Token counts use a consistent `len(text) // 4` approximation.") + lines.append( + "Impact accuracy reports two ground-truth modes: " + "graph-derived (circular — upper bound) and co-change " + "(files co-changed in the same commit, seed excluded)." + ) + lines.append( + "Rows with `status=error` are kept for forensics but excluded " + "from all aggregates." + ) + lines.append("") + + benchmark_types = [ + "token_efficiency", + "impact_accuracy", + "agent_baseline", + "flow_completeness", + "search_quality", + "build_performance", + "multi_hop_retrieval", + ] + + for btype in benchmark_types: + rows = _read_csvs(results_dir, btype) + if not rows: + continue + + title = btype.replace("_", " ").title() + lines.append(f"## {title}") + lines.append("") + + headers = list(rows[0].keys()) + table_rows = [[r.get(h, "-") for h in headers] for r in rows] + lines.append(_md_table(headers, table_rows)) + lines.append("") + + if len(lines) <= 6: + lines.append("No benchmark results found.") + lines.append("") + + return "\n".join(lines) + + +def generate_readme_tables(results_dir: str | Path) -> str: + """Generate concise README-ready tables from CSV result files. + + Produces three tables: + - Table A: Token Efficiency + - Table B: Accuracy & Quality + - Table C: Performance + + Args: + results_dir: Directory containing CSV result files. + + Returns: + Markdown string with the three tables. + """ + results_dir = Path(results_dir) + lines: list[str] = [] + + # Table A: Token Efficiency + te_rows = _read_csvs(results_dir, "token_efficiency") + if te_rows: + lines.append("### Token Efficiency") + lines.append("") + headers = [ + "Repo", "Files", "Naive Tokens", "Standard Tokens", + "Graph Tokens", "Naive/Graph", "Std/Graph", + ] + table_rows = [] + for r in te_rows: + table_rows.append([ + r.get("repo", "-"), + r.get("changed_files", "-"), + r.get("naive_tokens", "-"), + r.get("standard_tokens", "-"), + r.get("graph_tokens", "-"), + r.get("naive_to_graph_ratio", "-"), + r.get("standard_to_graph_ratio", "-"), + ]) + lines.append(_md_table(headers, table_rows)) + lines.append("") + + # Table B: Accuracy & Quality + ia_rows = _read_csvs(results_dir, "impact_accuracy") + fc_rows = _read_csvs(results_dir, "flow_completeness") + sq_rows = _read_csvs(results_dir, "search_quality") + + if ia_rows or fc_rows or sq_rows: + lines.append("### Accuracy & Quality") + lines.append("") + headers = ["Repo", "Impact F1 (graph-derived)", "Flow Recall", "Search MRR"] + # Build a per-repo summary + repo_data: dict[str, dict[str, object]] = {} + mrr_accum: dict[str, list[float]] = {} + f1_accum: dict[str, list[float]] = {} + for r in ia_rows: + # Failed rows are kept in the CSV for forensics but must never + # contribute to a headline number; co-change rows are a + # different metric and get their own reporting. + if r.get("status", "ok") not in ("", "ok"): + continue + mode = r.get("ground_truth_mode", "") + if mode and not mode.startswith("graph-derived"): + continue + repo = r.get("repo", "?") + repo_data.setdefault(repo, {}) + try: + f1_accum.setdefault(repo, []).append(float(r.get("f1", ""))) + except (ValueError, TypeError): + pass + for r in fc_rows: + repo_data.setdefault(r.get("repo", "?"), {})["recall"] = r.get("recall", "-") + for r in sq_rows: + repo = r.get("repo", "?") + repo_data.setdefault(repo, {}) + try: + mrr_accum.setdefault(repo, []).append(float(r.get("reciprocal_rank", 0))) + except (ValueError, TypeError): + pass + + table_rows = [] + for repo, d in sorted(repo_data.items()): + mrr_vals = mrr_accum.get(repo, []) + mrr = ( + str(round(sum(mrr_vals) / len(mrr_vals), 3)) + if mrr_vals + else "-" + ) + f1_vals = f1_accum.get(repo, []) + f1 = ( + str(round(sum(f1_vals) / len(f1_vals), 3)) + if f1_vals + else "-" + ) + table_rows.append([ + repo, + f1, + str(d.get("recall", "-")), + mrr, + ]) + lines.append(_md_table(headers, table_rows)) + lines.append("") + + # Table B2: Agent Baseline (grep top-k vs graph query) + ab_rows = _read_csvs(results_dir, "agent_baseline") + if ab_rows: + lines.append("### Agent Baseline (grep top-k vs graph query)") + lines.append("") + headers = [ + "Repo", "Question", "Baseline Tokens", "Graph Tokens", + "Baseline/Graph", "Status", + ] + table_rows = [] + for r in ab_rows: + table_rows.append([ + r.get("repo", "-"), + r.get("question", "-"), + r.get("baseline_tokens", "-"), + r.get("graph_tokens", "-"), + r.get("baseline_to_graph_ratio", "-"), + r.get("status", "ok") or "ok", + ]) + lines.append(_md_table(headers, table_rows)) + lines.append("") + + # Table C: Performance + bp_rows = _read_csvs(results_dir, "build_performance") + if bp_rows: + lines.append("### Performance") + lines.append("") + headers = ["Repo", "Files", "Nodes", "Flow Det. (s)", "Search (ms)"] + table_rows = [] + for r in bp_rows: + table_rows.append([ + r.get("repo", "-"), + r.get("file_count", "-"), + r.get("node_count", "-"), + r.get("flow_detection_seconds", "-"), + r.get("search_avg_ms", "-"), + ]) + lines.append(_md_table(headers, table_rows)) + lines.append("") + + if not lines: + return "No benchmark results found.\n" + + return "\n".join(lines) diff --git a/code_review_graph/eval/runner.py b/code_review_graph/eval/runner.py new file mode 100644 index 0000000..b0f550d --- /dev/null +++ b/code_review_graph/eval/runner.py @@ -0,0 +1,211 @@ +"""Evaluation runner: orchestrates benchmark execution across repositories.""" + +from __future__ import annotations + +import csv +import logging +import subprocess +from datetime import date +from pathlib import Path + +try: + import yaml # type: ignore[import-untyped] +except ImportError: + yaml = None # type: ignore[assignment] + +from code_review_graph.eval.benchmarks import ( + agent_baseline, + build_performance, + flow_completeness, + impact_accuracy, + multi_hop_retrieval, + search_quality, + token_efficiency, +) + +logger = logging.getLogger(__name__) + +BENCHMARK_REGISTRY = { + "token_efficiency": token_efficiency.run, + "impact_accuracy": impact_accuracy.run, + "flow_completeness": flow_completeness.run, + "search_quality": search_quality.run, + "build_performance": build_performance.run, + "multi_hop_retrieval": multi_hop_retrieval.run, + "agent_baseline": agent_baseline.run, +} + +CONFIGS_DIR = Path(__file__).parent / "configs" +DEFAULT_OUTPUT = Path("evaluate/results") +DEFAULT_REPOS = Path("evaluate/test_repos") + + +def _require_yaml(): + if yaml is None: + raise ImportError("pyyaml is required: pip install code-review-graph[eval]") + + +def load_config(name: str) -> dict: + """Load a single benchmark config by name.""" + _require_yaml() + path = CONFIGS_DIR / f"{name}.yaml" + with open(path) as f: + return yaml.safe_load(f) + + +def load_all_configs() -> list[dict]: + """Load all benchmark configs from the configs directory.""" + _require_yaml() + configs = [] + for p in sorted(CONFIGS_DIR.glob("*.yaml")): + with open(p) as f: + configs.append(yaml.safe_load(f)) + return configs + + +def clone_or_update(config: dict, repos_dir: Path | None = None) -> Path: + """Clone or update a repository at the config's pinned ``commit`` SHA. + + Full clones (no ``--depth``) are required: the pinned ``test_commits`` are + often older than any reasonable shallow-clone window, and a missed SHA + used to silently fall back to ``git diff HEAD~1 HEAD`` — producing + benchmark numbers tied to whatever upstream HEAD looked like that day. + + Every subprocess call's exit status is checked; failures raise + ``RuntimeError`` so reproducibility issues surface immediately instead of + yielding garbage results. + """ + repos_dir = repos_dir or DEFAULT_REPOS + repos_dir.mkdir(parents=True, exist_ok=True) + repo_path = repos_dir / config["name"] + + if repo_path.exists(): + proc = subprocess.run( + ["git", "fetch", "--all", "--tags"], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError( + f"git fetch failed in {repo_path}: {proc.stderr.strip()}" + ) + else: + proc = subprocess.run( + ["git", "clone", config["url"], str(repo_path)], + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError( + f"git clone failed for {config['url']}: {proc.stderr.strip()}" + ) + + commit = config.get("commit", "HEAD") + if commit != "HEAD": + proc = subprocess.run( + ["git", "checkout", commit], + cwd=str(repo_path), + capture_output=True, + text=True, + ) + if proc.returncode != 0: + raise RuntimeError( + f"git checkout {commit} failed in {repo_path}: " + f"{proc.stderr.strip()}" + ) + + return repo_path + + +def write_csv(results: list[dict], path: Path) -> None: + """Write benchmark results to a CSV file.""" + if not results: + return + path.parent.mkdir(parents=True, exist_ok=True) + fieldnames = list(results[0].keys()) + with open(path, "w", newline="") as f: + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(results) + + +def run_eval( + repos: list[str] | None = None, + benchmarks: list[str] | None = None, + output_dir: str | Path | None = None, +) -> dict[str, list[dict]]: + """Run evaluation benchmarks across repositories. + + Args: + repos: List of repo config names to evaluate (None = all). + benchmarks: List of benchmark names to run (None = all). + output_dir: Directory for CSV output files. + + Returns: + Dict mapping ``{repo}_{benchmark}`` to list of result dicts. + """ + output_dir = Path(output_dir) if output_dir else DEFAULT_OUTPUT + output_dir.mkdir(parents=True, exist_ok=True) + + if repos: + configs = [load_config(r) for r in repos] + else: + configs = load_all_configs() + + benchmark_names = benchmarks or list(BENCHMARK_REGISTRY.keys()) + all_results: dict[str, list[dict]] = {} + today = date.today().isoformat() + + for config in configs: + name = config["name"] + logger.info("Evaluating %s...", name) + + # Resolve the repo path to an absolute Path before handing it to + # full_build / get_db_path so the stored qualified_names match what + # the CLI/MCP layer produces (those paths go through _get_store -> + # _validate_repo_root which .resolve()s). Without this, a later + # ``code-review-graph update --repo `` writes the same + # function under a new absolute-prefixed qualified_name, leaving the + # graph with duplicate nodes for the same source location. + repo_path = clone_or_update(config).resolve() + + # Build graph + from code_review_graph.graph import GraphStore + from code_review_graph.incremental import full_build, get_db_path + from code_review_graph.postprocessing import run_post_processing + + db_path = get_db_path(repo_path) + store = GraphStore(db_path) + + full_build(repo_path, store) + # full_build is the parsing-only primitive; the higher-level CLI/MCP + # wrappers run postprocessing on top. The eval framework bypasses + # those, so call it directly here. Without this, FTS5 stays empty + # and downstream benchmarks (token_efficiency, search_quality) + # silently produce useless results. See: search.rebuild_fts_index. + pp_result = run_post_processing(store) + for warning in pp_result.get("warnings", []): + logger.warning(" postprocessing: %s", warning) + + for bench_name in benchmark_names: + if bench_name not in BENCHMARK_REGISTRY: + logger.warning("Unknown benchmark: %s", bench_name) + continue + + logger.info(" Running %s...", bench_name) + try: + bench_fn = BENCHMARK_REGISTRY[bench_name] + results = bench_fn(repo_path, store, config) + + key = f"{name}_{bench_name}" + all_results[key] = results + write_csv(results, output_dir / f"{key}_{today}.csv") + logger.info(" %s: %d result(s)", bench_name, len(results)) + except Exception as e: + logger.error(" %s failed: %s", bench_name, e) + all_results[f"{name}_{bench_name}"] = [] + + store.close() + + return all_results diff --git a/code_review_graph/eval/scorer.py b/code_review_graph/eval/scorer.py new file mode 100644 index 0000000..4902982 --- /dev/null +++ b/code_review_graph/eval/scorer.py @@ -0,0 +1,85 @@ +"""Scoring metrics for evaluating graph-based code review quality. + +Provides: +- Token efficiency: measures how many tokens the graph saves vs raw context. +- Mean Reciprocal Rank (MRR): evaluates ranking quality for search results. +- Precision / Recall / F1: evaluates set-based retrieval accuracy. +""" + +from __future__ import annotations + + +def compute_token_efficiency(raw_tokens: int, graph_tokens: int) -> dict: + """Compute token efficiency metrics. + + Args: + raw_tokens: Number of tokens when sending raw source code. + graph_tokens: Number of tokens when using graph-based context. + + Returns: + Dict with keys: + - raw_tokens: the raw token count + - graph_tokens: the graph token count + - ratio: graph_tokens / raw_tokens (lower is better) + - reduction_percent: percentage of tokens saved (higher is better) + """ + if raw_tokens <= 0: + return { + "raw_tokens": raw_tokens, + "graph_tokens": graph_tokens, + "ratio": 0.0, + "reduction_percent": 0.0, + } + ratio = graph_tokens / raw_tokens + reduction = (1.0 - ratio) * 100.0 + return { + "raw_tokens": raw_tokens, + "graph_tokens": graph_tokens, + "ratio": round(ratio, 4), + "reduction_percent": round(reduction, 2), + } + + +def compute_mrr(correct: str, results: list[str]) -> float: + """Compute Mean Reciprocal Rank for a single query. + + Args: + correct: The correct/expected result identifier. + results: Ordered list of result identifiers (best first). + + Returns: + 1/rank if *correct* is found in *results*, else 0.0. + """ + for i, r in enumerate(results, start=1): + if r == correct: + return 1.0 / i + return 0.0 + + +def compute_precision_recall(predicted: set, actual: set) -> dict: + """Compute precision, recall, and F1 score. + + Args: + predicted: Set of predicted/returned items. + actual: Set of ground-truth items. + + Returns: + Dict with keys: precision, recall, f1. + """ + if not predicted and not actual: + return {"precision": 1.0, "recall": 1.0, "f1": 1.0} + + true_positive = len(predicted & actual) + precision = true_positive / len(predicted) if predicted else 0.0 + recall = true_positive / len(actual) if actual else 0.0 + + if precision + recall > 0: + f1 = 2 * precision * recall / (precision + recall) + else: + f1 = 0.0 + + return { + "precision": round(precision, 4), + "recall": round(recall, 4), + "f1": round(f1, 4), + } diff --git a/code_review_graph/eval/token_benchmark.py b/code_review_graph/eval/token_benchmark.py new file mode 100644 index 0000000..a83e9e5 --- /dev/null +++ b/code_review_graph/eval/token_benchmark.py @@ -0,0 +1,182 @@ +"""Measures total tokens consumed by agent workflows against benchmark repos.""" + +from __future__ import annotations + +import json +import logging +from typing import Any, Callable + +logger = logging.getLogger(__name__) + + +def estimate_tokens(obj: Any) -> int: + """Estimate token count from JSON-serializable object. + + Uses character count / 4 as a rough approximation for English + code. + """ + return len(json.dumps(obj, default=str)) // 4 + + +def benchmark_review_workflow(repo_root: str, base: str = "HEAD~1") -> dict: + """Simulate a review workflow and measure total tokens consumed.""" + from ..tools.context import get_minimal_context + from ..tools.review import detect_changes_func + + total_tokens = 0 + calls = [] + + # Step 1: get_minimal_context + result = get_minimal_context(task="review changes", repo_root=repo_root, base=base) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "get_minimal_context", "tokens": tokens}) + + # Step 2: detect_changes (minimal) + result = detect_changes_func(base=base, repo_root=repo_root, detail_level="minimal") + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "detect_changes_minimal", "tokens": tokens}) + + return { + "workflow": "review", + "total_tokens": total_tokens, + "tool_calls": len(calls), + "calls": calls, + } + + +def benchmark_architecture_workflow(repo_root: str) -> dict: + """Simulate an architecture exploration workflow.""" + from ..tools.community_tools import list_communities_func + from ..tools.context import get_minimal_context + from ..tools.flows_tools import list_flows + + total_tokens = 0 + calls = [] + + result = get_minimal_context(task="map architecture", repo_root=repo_root) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "get_minimal_context", "tokens": tokens}) + + result = list_communities_func(repo_root=repo_root, detail_level="minimal") + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "list_communities_minimal", "tokens": tokens}) + + result = list_flows(repo_root=repo_root, detail_level="minimal") + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "list_flows_minimal", "tokens": tokens}) + + return { + "workflow": "architecture", + "total_tokens": total_tokens, + "tool_calls": len(calls), + "calls": calls, + } + + +def benchmark_debug_workflow(repo_root: str) -> dict: + """Simulate a debug workflow.""" + from ..tools.context import get_minimal_context + from ..tools.query import semantic_search_nodes + + total_tokens = 0 + calls = [] + + result = get_minimal_context(task="debug login bug", repo_root=repo_root) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "get_minimal_context", "tokens": tokens}) + + result = semantic_search_nodes( + query="login", repo_root=repo_root, detail_level="minimal", + ) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "semantic_search_minimal", "tokens": tokens}) + + return { + "workflow": "debug", + "total_tokens": total_tokens, + "tool_calls": len(calls), + "calls": calls, + } + + +def benchmark_onboard_workflow(repo_root: str) -> dict: + """Simulate an onboarding workflow.""" + from ..tools.context import get_minimal_context + from ..tools.query import list_graph_stats + + total_tokens = 0 + calls = [] + + result = get_minimal_context(task="onboard developer", repo_root=repo_root) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "get_minimal_context", "tokens": tokens}) + + result = list_graph_stats(repo_root=repo_root) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "list_graph_stats", "tokens": tokens}) + + return { + "workflow": "onboard", + "total_tokens": total_tokens, + "tool_calls": len(calls), + "calls": calls, + } + + +def benchmark_pre_merge_workflow(repo_root: str, base: str = "HEAD~1") -> dict: + """Simulate a pre-merge check workflow.""" + from ..tools.context import get_minimal_context + from ..tools.review import detect_changes_func + + total_tokens = 0 + calls = [] + + result = get_minimal_context(task="pre-merge check", repo_root=repo_root, base=base) + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "get_minimal_context", "tokens": tokens}) + + result = detect_changes_func(base=base, repo_root=repo_root, detail_level="minimal") + tokens = estimate_tokens(result) + total_tokens += tokens + calls.append({"tool": "detect_changes_minimal", "tokens": tokens}) + + return { + "workflow": "pre_merge", + "total_tokens": total_tokens, + "tool_calls": len(calls), + "calls": calls, + } + + +ALL_WORKFLOWS: dict[str, Callable[..., dict]] = { + "review": benchmark_review_workflow, + "architecture": benchmark_architecture_workflow, + "debug": benchmark_debug_workflow, + "onboard": benchmark_onboard_workflow, + "pre_merge": benchmark_pre_merge_workflow, +} + + +def run_all_benchmarks(repo_root: str, base: str = "HEAD~1") -> list[dict]: + """Run all workflow benchmarks and return results.""" + results = [] + for name, fn in ALL_WORKFLOWS.items(): + try: + if "base" in fn.__code__.co_varnames: + result = fn(repo_root=repo_root, base=base) + else: + result = fn(repo_root=repo_root) + results.append(result) + except Exception as e: + logger.warning("Benchmark %s failed: %s", name, e) + results.append({"workflow": name, "error": str(e)}) + return results diff --git a/code_review_graph/exports.py b/code_review_graph/exports.py new file mode 100644 index 0000000..9269099 --- /dev/null +++ b/code_review_graph/exports.py @@ -0,0 +1,409 @@ +"""Additional export formats: GraphML, Neo4j Cypher, Obsidian vault, SVG.""" + +from __future__ import annotations + +import html +import logging +import re +from pathlib import Path + +from .graph import GraphStore, _sanitize_name +from .visualization import export_graph_data + +logger = logging.getLogger(__name__) + + +# ------------------------------------------------------------------- +# GraphML export (for Gephi, yEd, Cytoscape) +# ------------------------------------------------------------------- + +def export_graphml(store: GraphStore, output_path: Path) -> Path: + """Export the graph as GraphML XML for Gephi/yEd/Cytoscape. + + Returns the path to the written file. + """ + data = export_graph_data(store) + nodes = data["nodes"] + edges = data["edges"] + + lines = [ + '', + '', + ' ', + ' ', + ' ', + ' ', + ' ', + ' ', + ] + + for n in nodes: + nid = html.escape(n["qualified_name"], quote=True) + lines.append(f' ') + lines.append(f' ' + f'{html.escape(n.get("kind", ""))}') + lines.append(f' ' + f'{html.escape(n.get("file_path", ""))}') + lang = n.get("language", "") or "" + lines.append(f' ' + f'{html.escape(lang)}') + cid = n.get("community_id") + if cid is not None: + lines.append(f' ' + f'{cid}') + lines.append(' ') + + for i, e in enumerate(edges): + src = html.escape(e["source"], quote=True) + tgt = html.escape(e["target"], quote=True) + kind = html.escape(e.get("kind", ""), quote=True) + lines.append( + f' ' + ) + lines.append(f' {kind}') + lines.append(' ') + + lines.append(' ') + lines.append('') + + output_path.write_text("\n".join(lines), encoding="utf-8") + logger.info("GraphML exported to %s (%d nodes, %d edges)", + output_path, len(nodes), len(edges)) + return output_path + + +# ------------------------------------------------------------------- +# Neo4j Cypher export +# ------------------------------------------------------------------- + +def export_neo4j_cypher(store: GraphStore, output_path: Path) -> Path: + """Export the graph as Neo4j Cypher CREATE statements. + + Returns the path to the written file. + """ + data = export_graph_data(store) + nodes = data["nodes"] + edges = data["edges"] + + lines = [ + "// Generated by code-review-graph", + "// Import: paste into Neo4j Browser or run via cypher-shell", + "", + ] + + # Create nodes + for n in nodes: + kind = n.get("kind", "Node") + props = { + "qualified_name": n["qualified_name"], + "name": n.get("name", ""), + "file_path": n.get("file_path", ""), + "language": n.get("language", "") or "", + } + cid = n.get("community_id") + if cid is not None: + props["community_id"] = cid + props_str = _cypher_props(props) + lines.append(f"CREATE (:{kind} {props_str});") + + lines.append("") + + # Create edges via MATCH + for e in edges: + kind = e.get("kind", "RELATES_TO") + src_qn = _cypher_escape(e["source"]) + tgt_qn = _cypher_escape(e["target"]) + lines.append( + f"MATCH (a {{qualified_name: '{src_qn}'}}), " + f"(b {{qualified_name: '{tgt_qn}'}}) " + f"CREATE (a)-[:{kind}]->(b);" + ) + + output_path.write_text("\n".join(lines), encoding="utf-8") + logger.info("Neo4j Cypher exported to %s (%d nodes, %d edges)", + output_path, len(nodes), len(edges)) + return output_path + + +def _cypher_escape(s: str) -> str: + """Escape a string for Cypher single-quoted literals.""" + return s.replace("\\", "\\\\").replace("'", "\\'") + + +def _cypher_props(d: dict) -> str: + """Format a dict as Cypher property map.""" + parts = [] + for k, v in d.items(): + if isinstance(v, str): + parts.append(f"{k}: '{_cypher_escape(v)}'") + elif isinstance(v, (int, float)): + parts.append(f"{k}: {v}") + elif isinstance(v, bool): + parts.append(f"{k}: {'true' if v else 'false'}") + return "{" + ", ".join(parts) + "}" + + +# ------------------------------------------------------------------- +# Obsidian vault export +# ------------------------------------------------------------------- + +def export_obsidian_vault( + store: GraphStore, output_dir: Path +) -> Path: + """Export the graph as an Obsidian vault with wikilinks. + + Creates: + - One .md per node with YAML frontmatter and [[wikilinks]] + - _COMMUNITY_*.md overview notes per community + - _INDEX.md with links to all nodes + + Returns the output directory path. + """ + data = export_graph_data(store) + nodes = data["nodes"] + edges = data["edges"] + communities = data.get("communities", []) + + output_dir.mkdir(parents=True, exist_ok=True) + + # Build adjacency for wikilinks + neighbors: dict[str, list[dict]] = {} + for e in edges: + src = e["source"] + tgt = e["target"] + kind = e.get("kind", "RELATES_TO") + neighbors.setdefault(src, []).append( + {"target": tgt, "kind": kind} + ) + neighbors.setdefault(tgt, []).append( + {"target": src, "kind": kind} + ) + + # Node name -> slug mapping + slugs: dict[str, str] = {} + for n in nodes: + slug = _obsidian_slug(n.get("name", n["qualified_name"])) + # Handle collisions + base_slug = slug + counter = 1 + while slug in slugs.values(): + slug = f"{base_slug}-{counter}" + counter += 1 + slugs[n["qualified_name"]] = slug + + # Write node pages + for n in nodes: + qn = n["qualified_name"] + slug = slugs[qn] + name = n.get("name", qn) + + frontmatter = { + "kind": n.get("kind", ""), + "file": n.get("file_path", ""), + "language": n.get("language", "") or "", + "community": n.get("community_id"), + "tags": [n.get("kind", "").lower()], + } + + lines = ["---"] + for k, v in frontmatter.items(): + if isinstance(v, list): + lines.append(f"{k}:") + for item in v: + lines.append(f" - {item}") + elif v is not None: + lines.append(f"{k}: {v}") + lines.append("---") + lines.append(f"# {_sanitize_name(name)}") + lines.append("") + lines.append(f"**Kind:** {n.get('kind', '')}") + lines.append(f"**File:** `{n.get('file_path', '')}`") + lines.append("") + + # Wikilinks to neighbors + nbrs = neighbors.get(qn, []) + if nbrs: + lines.append("## Connections") + lines.append("") + seen = set() + for nb in nbrs: + tgt_slug = slugs.get(nb["target"]) + if tgt_slug and tgt_slug not in seen: + seen.add(tgt_slug) + tgt_name = tgt_slug.replace("-", " ").title() + lines.append( + f"- {nb['kind']}: " + f"[[{tgt_slug}|{tgt_name}]]" + ) + + page_path = output_dir / f"{slug}.md" + page_path.write_text("\n".join(lines), encoding="utf-8") + + # Write community overview pages + community_map: dict[int, list[str]] = {} + for n in nodes: + cid = n.get("community_id") + if cid is not None: + community_map.setdefault(cid, []).append( + n["qualified_name"] + ) + + for c in communities: + cid = c.get("id") + cname = c.get("name", f"community-{cid}") + members = community_map.get(cid, []) + + lines = [f"# Community: {_sanitize_name(cname)}", ""] + lines.append(f"**Size:** {c.get('size', len(members))}") + lines.append(f"**Cohesion:** {c.get('cohesion', 0):.2f}") + lang = c.get("dominant_language", "") + if lang: + lines.append(f"**Language:** {lang}") + lines.append("") + lines.append("## Members") + lines.append("") + for qn in members[:50]: + slug = slugs.get(qn) + if slug: + lines.append(f"- [[{slug}]]") + + page_path = output_dir / f"_COMMUNITY_{cid}.md" + page_path.write_text("\n".join(lines), encoding="utf-8") + + # Write index + index_lines = ["# Code Graph Index", ""] + index_lines.append(f"**Nodes:** {len(nodes)}") + index_lines.append(f"**Edges:** {len(edges)}") + index_lines.append( + f"**Communities:** {len(communities)}" + ) + index_lines.append("") + index_lines.append("## All Nodes") + index_lines.append("") + for n in sorted(nodes, key=lambda x: x.get("name", "")): + slug = slugs.get(n["qualified_name"]) + if slug: + index_lines.append( + f"- [[{slug}]] ({n.get('kind', '')})" + ) + + (output_dir / "_INDEX.md").write_text( + "\n".join(index_lines), encoding="utf-8" + ) + + logger.info( + "Obsidian vault exported to %s (%d pages)", + output_dir, len(nodes) + ) + return output_dir + + +def _obsidian_slug(name: str) -> str: + """Convert a name to an Obsidian-friendly filename slug.""" + slug = re.sub(r"[^\w\s-]", "", name.lower()) + slug = re.sub(r"[\s_]+", "-", slug).strip("-") + return slug[:100] or "unnamed" + + +# ------------------------------------------------------------------- +# SVG export (matplotlib-based) +# ------------------------------------------------------------------- + +def export_svg(store: GraphStore, output_path: Path) -> Path: + """Export a static SVG graph visualization. + + Requires matplotlib (optional dependency). + Returns the path to the written file. + """ + try: + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + except ImportError: + raise ImportError( + "matplotlib is required for SVG export. " + "Install with: pip install matplotlib" + ) + + import networkx as nx + + data = export_graph_data(store) + nodes_data = data["nodes"] + edges_data = data["edges"] + + nxg: nx.DiGraph = nx.DiGraph() # type: ignore[type-arg] + for n in nodes_data: + nxg.add_node( + n["qualified_name"], + label=n.get("name", ""), + kind=n.get("kind", ""), + ) + for e in edges_data: + if e["source"] in nxg and e["target"] in nxg: + nxg.add_edge(e["source"], e["target"]) + + if nxg.number_of_nodes() == 0: + raise ValueError("Graph is empty, nothing to export") + + # Color by kind + kind_colors = { + "File": "#6c757d", + "Class": "#0d6efd", + "Function": "#198754", + "Type": "#ffc107", + "Test": "#dc3545", + } + colors = [ + kind_colors.get( + nxg.nodes[n].get("kind", ""), "#adb5bd" + ) + for n in nxg.nodes() + ] + + fig, ax = plt.subplots(1, 1, figsize=(16, 12)) + pos = nx.spring_layout( + nxg, k=2 / (nxg.number_of_nodes() ** 0.5), + iterations=50, seed=42 + ) + + # Limit labels to avoid clutter + labels = {} + if nxg.number_of_nodes() <= 100: + labels = { + n: nxg.nodes[n].get("label", n.split("::")[-1]) + for n in nxg.nodes() + } + + nx.draw_networkx_nodes( + nxg, pos, ax=ax, node_color=colors, + node_size=30, alpha=0.8 + ) + nx.draw_networkx_edges( + nxg, pos, ax=ax, alpha=0.2, + arrows=True, arrowsize=5 + ) + if labels: + nx.draw_networkx_labels( + nxg, pos, labels=labels, ax=ax, + font_size=6 + ) + + ax.set_title("Code Review Graph", fontsize=14) + ax.axis("off") + + fig.savefig( + str(output_path), format="svg", + bbox_inches="tight", dpi=150 + ) + plt.close(fig) + + logger.info("SVG exported to %s (%d nodes)", + output_path, nxg.number_of_nodes()) + return output_path diff --git a/code_review_graph/flows.py b/code_review_graph/flows.py new file mode 100644 index 0000000..fd12ade --- /dev/null +++ b/code_review_graph/flows.py @@ -0,0 +1,698 @@ +"""Execution flow detection, tracing, and criticality scoring. + +Detects entry points in the codebase (functions with no incoming CALLS edges, +framework-decorated handlers, and conventional name patterns), traces execution +paths via forward BFS through CALLS edges, scores each flow for criticality, +and persists results to the ``flows`` / ``flow_memberships`` tables. +""" + +from __future__ import annotations + +import json +import logging +import re +from collections import deque +from typing import Optional + +from .constants import SECURITY_KEYWORDS as _SECURITY_KEYWORDS +from .graph import FlowAdjacency, GraphNode, GraphStore, _sanitize_name + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Constants +# --------------------------------------------------------------------------- + +# Decorator patterns that indicate a function is a framework entry point. +_FRAMEWORK_DECORATOR_PATTERNS: list[re.Pattern[str]] = [ + # Python web frameworks + re.compile(r"app\.(get|post|put|delete|patch|route|websocket|on_event)", re.IGNORECASE), + re.compile(r"router\.(get|post|put|delete|patch|route)", re.IGNORECASE), + re.compile(r"blueprint\.(route|before_request|after_request)", re.IGNORECASE), + re.compile(r"(before|after)_(request|response)", re.IGNORECASE), + # CLI frameworks + re.compile(r"click\.(command|group)", re.IGNORECASE), + re.compile(r"\w+\.(command|group)\b", re.IGNORECASE), # Click subgroups: @mygroup.command() + # Pydantic validators/serializers + re.compile(r"(field|model)_(serializer|validator)", re.IGNORECASE), + # Task queues + re.compile(r"(celery\.)?(task|shared_task|periodic_task)", re.IGNORECASE), + # Django + re.compile(r"receiver", re.IGNORECASE), + re.compile(r"api_view", re.IGNORECASE), + re.compile(r"\baction\b", re.IGNORECASE), + # Testing + re.compile(r"pytest\.(fixture|mark)"), + re.compile(r"(override_settings|modify_settings)", re.IGNORECASE), + # SQLAlchemy / event systems + re.compile(r"(event\.)?listens_for", re.IGNORECASE), + # Java Spring + re.compile(r"(Get|Post|Put|Delete|Patch|RequestMapping)Mapping", re.IGNORECASE), + re.compile(r"(Scheduled|EventListener|Bean|Configuration)", re.IGNORECASE), + # JS/TS frameworks + re.compile(r"(Component|Injectable|Controller|Module|Guard|Pipe)", re.IGNORECASE), + re.compile(r"(Subscribe|Mutation|Query|Resolver)", re.IGNORECASE), + # Express / Koa / Hono route handlers + re.compile(r"(app|router)\.(get|post|put|delete|patch|use|all)\b"), + # Android lifecycle + re.compile(r"@(Override|OnLifecycleEvent|Composable)", re.IGNORECASE), + # Kotlin coroutines / Android ViewModel + re.compile(r"(HiltViewModel|AndroidEntryPoint|Inject)", re.IGNORECASE), + # AI/agent frameworks (pydantic-ai, langchain, etc.) + re.compile(r"\w+\.(tool|tool_plain|system_prompt|result_validator)\b", re.IGNORECASE), + re.compile(r"^tool\b"), # bare @tool (LangChain, etc.) + # Middleware and exception handlers (Starlette, FastAPI, Sanic) + re.compile(r"\w+\.(middleware|exception_handler|on_exception)\b", re.IGNORECASE), + # Generic route decorator (Flask blueprints: @bp.route, @auth_bp.route, etc.) + re.compile(r"\w+\.route\b", re.IGNORECASE), +] + +# Name patterns that indicate conventional entry points. +_ENTRY_NAME_PATTERNS: list[re.Pattern[str]] = [ + re.compile(r"^main$"), + re.compile(r"^__main__$"), + re.compile(r"^test_"), + re.compile(r"^Test[A-Z]"), + re.compile(r"^on_"), + re.compile(r"^handle_"), + # Lambda / serverless handler functions (wired via config, not code calls) + re.compile(r"^handler$"), + re.compile(r"^handle$"), + re.compile(r"^lambda_handler$"), + # Alembic migration entry points + re.compile(r"^upgrade$"), + re.compile(r"^downgrade$"), + # FastAPI lifecycle / dependency injection + re.compile(r"^lifespan$"), + re.compile(r"^get_db$"), + # Android Activity/Fragment lifecycle + re.compile(r"^on(Create|Start|Resume|Pause|Stop|Destroy|Bind|Receive)"), + # Servlet / JAX-RS + re.compile(r"^do(Get|Post|Put|Delete)$"), + # Python BaseHTTPRequestHandler + re.compile(r"^do_(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)$"), + re.compile(r"^log_message$"), + # Express middleware signature + re.compile(r"^(middleware|errorHandler)$"), + # Angular lifecycle hooks + re.compile( + r"^ng(OnInit|OnChanges|OnDestroy|DoCheck" + r"|AfterContentInit|AfterContentChecked|AfterViewInit|AfterViewChecked)$" + ), + # Angular Pipe / ControlValueAccessor / Guards / Resolvers + re.compile(r"^(transform|writeValue|registerOnChange|registerOnTouched|setDisabledState)$"), + re.compile(r"^(canActivate|canDeactivate|canActivateChild|canLoad|canMatch|resolve)$"), + # React class component lifecycle + re.compile( + r"^(componentDidMount|componentDidUpdate|componentWillUnmount" + r"|shouldComponentUpdate|render)$" + ), +] + + +# --------------------------------------------------------------------------- +# Entry-point detection +# --------------------------------------------------------------------------- + + +def _has_framework_decorator(node: GraphNode) -> bool: + """Return True if *node* has a decorator matching a framework pattern.""" + decorators = node.extra.get("decorators") + if not decorators: + return False + if isinstance(decorators, str): + decorators = [decorators] + for dec in decorators: + for pat in _FRAMEWORK_DECORATOR_PATTERNS: + if pat.search(dec): + return True + return False + + +def _matches_entry_name(node: GraphNode) -> bool: + """Return True if *node*'s name matches a conventional entry-point pattern.""" + for pat in _ENTRY_NAME_PATTERNS: + if pat.search(node.name): + return True + return False + + +_TEST_FILE_RE = re.compile( + r"([\\/]__tests__[\\/]|\.spec\.[jt]sx?$|\.test\.[jt]sx?$|[\\/]test_[^/\\]*\.py$)", +) + + +def _is_test_file(file_path: str) -> bool: + """Return True if *file_path* looks like a test file.""" + return bool(_TEST_FILE_RE.search(file_path)) + + +def detect_entry_points( + store: GraphStore, + include_tests: bool = False, +) -> list[GraphNode]: + """Find functions that are entry points in the graph. + + An entry point is a Function/Test node that either: + 1. Has no incoming CALLS edges (true root), or + 2. Has a framework decorator (e.g. ``@app.get``), or + 3. Matches a conventional name pattern (``main``, ``test_*``, etc.). + + When *include_tests* is False (the default), Test nodes are excluded so + that flow analysis focuses on production entry points. + """ + # Build a set of all qualified names that are CALLS targets. Exclude + # edges sourced at File nodes so that script-/notebook-/top-level-only + # callees (e.g. ``run_job()`` invoked from module scope, a top-level + # ```` render) remain detectable as entry points. + called_qnames = store.get_all_call_targets(include_file_sources=False) + + # Scan all nodes for entry-point candidates. + candidate_nodes = store.get_nodes_by_kind(["Function", "Test"]) + + entry_points: list[GraphNode] = [] + seen_qn: set[str] = set() + + for node in candidate_nodes: + if not include_tests and (node.is_test or _is_test_file(node.file_path)): + continue + + is_entry = False + + # True root: no one calls this function. + if node.qualified_name not in called_qnames: + is_entry = True + + # Framework decorator match. + if _has_framework_decorator(node): + is_entry = True + + # Conventional name match. + if _matches_entry_name(node): + is_entry = True + + if is_entry and node.qualified_name not in seen_qn: + entry_points.append(node) + seen_qn.add(node.qualified_name) + + return entry_points + + +# --------------------------------------------------------------------------- +# Flow tracing (BFS) +# --------------------------------------------------------------------------- + + +def _trace_single_flow( + adj: FlowAdjacency, + ep: GraphNode, + max_depth: int = 15, +) -> Optional[dict]: + """Trace a single execution flow from *ep* via forward BFS. + + Returns a flow dict (see :func:`trace_flows` for the schema) or ``None`` + if the flow is trivial (single-node, no outgoing CALLS that resolve). + """ + path_ids: list[int] = [ep.id] + path_qnames: list[str] = [ep.qualified_name] + visited: set[str] = {ep.qualified_name} + queue: deque[tuple[str, int]] = deque([(ep.qualified_name, 0)]) + + actual_depth = 0 + nodes_by_qn = adj.nodes_by_qn + calls_out = adj.calls_out + + while queue: + current_qn, depth = queue.popleft() + if depth > actual_depth: + actual_depth = depth + if depth >= max_depth: + continue + + for target_qn in calls_out.get(current_qn, ()): + if target_qn in visited: + continue + target_node = nodes_by_qn.get(target_qn) + if target_node is None: + continue + visited.add(target_qn) + path_ids.append(target_node.id) + path_qnames.append(target_qn) + queue.append((target_qn, depth + 1)) + + # Skip trivial single-node flows. + if len(path_ids) < 2: + return None + + files = list({ + n.file_path + for qn in path_qnames + if (n := nodes_by_qn.get(qn)) is not None + }) + + flow: dict = { + "name": _sanitize_name(ep.name), + "entry_point": ep.qualified_name, + "entry_point_id": ep.id, + "path": path_ids, + "depth": actual_depth, + "node_count": len(path_ids), + "file_count": len(files), + "files": files, + "criticality": 0.0, + } + flow["criticality"] = compute_criticality(flow, adj) + return flow + + +def trace_flows( + store: GraphStore, + max_depth: int = 15, + include_tests: bool = False, +) -> list[dict]: + """Trace execution flows from every entry point via forward BFS. + + Returns a list of flow dicts, each containing: + - name: human-readable flow name (entry point name) + - entry_point: qualified name of the entry point + - entry_point_id: node database id of the entry point + - path: ordered list of node IDs in the flow + - depth: maximum BFS depth reached + - node_count: number of distinct nodes in the path + - file_count: number of distinct files touched + - files: list of distinct file paths + - criticality: computed criticality score (0.0-1.0) + """ + entry_points = detect_entry_points(store, include_tests=include_tests) + if not entry_points: + return [] + + adj = store.load_flow_adjacency() + flows: list[dict] = [] + + for ep in entry_points: + flow = _trace_single_flow(adj, ep, max_depth) + if flow is not None: + flows.append(flow) + + # Sort by criticality descending. + flows.sort(key=lambda f: f["criticality"], reverse=True) + return flows + + +# --------------------------------------------------------------------------- +# Criticality scoring +# --------------------------------------------------------------------------- + + +def compute_criticality(flow: dict, adj: FlowAdjacency) -> float: + """Score a flow from 0.0 to 1.0 based on multiple weighted factors. + + Weights: + - File spread: 0.30 + - External calls: 0.20 + - Security sensitivity: 0.25 + - Test coverage gap: 0.15 + - Depth: 0.10 + """ + node_ids: list[int] = flow.get("path", []) + if not node_ids: + return 0.0 + + nodes_by_id = adj.nodes_by_id + nodes_by_qn = adj.nodes_by_qn + calls_out = adj.calls_out + has_tested_by = adj.has_tested_by + + nodes: list[GraphNode] = [ + n for nid in node_ids if (n := nodes_by_id.get(nid)) is not None + ] + if not nodes: + return 0.0 + + # --- File spread (0.0 - 1.0) --- + file_count = len({n.file_path for n in nodes}) + # Normalize: 1 file => 0.0, 5+ files => 1.0 + file_spread = min((file_count - 1) / 4.0, 1.0) if file_count > 1 else 0.0 + + # --- External calls (0.0 - 1.0) --- + # Calls that target nodes NOT in the graph are considered external. + external_count = 0 + for n in nodes: + for target_qn in calls_out.get(n.qualified_name, ()): + if target_qn not in nodes_by_qn: + external_count += 1 + # Normalize: 0 => 0.0, 5+ => 1.0 + external_score = min(external_count / 5.0, 1.0) + + # --- Security sensitivity (0.0 - 1.0) --- + security_hits = 0 + for n in nodes: + name_lower = n.name.lower() + qn_lower = n.qualified_name.lower() + for kw in _SECURITY_KEYWORDS: + if kw in name_lower or kw in qn_lower: + security_hits += 1 + break # Count each node at most once. + security_score = min(security_hits / max(len(nodes), 1), 1.0) + + # --- Test coverage gap (0.0 - 1.0) --- + tested_count = sum(1 for n in nodes if n.qualified_name in has_tested_by) + coverage = tested_count / max(len(nodes), 1) + test_gap = 1.0 - coverage + + # --- Depth (0.0 - 1.0) --- + depth = flow.get("depth", 0) + # Normalize: 0 => 0.0, 10+ => 1.0 + depth_score = min(depth / 10.0, 1.0) + + # --- Weighted sum --- + criticality = ( + file_spread * 0.30 + + external_score * 0.20 + + security_score * 0.25 + + test_gap * 0.15 + + depth_score * 0.10 + ) + return round(min(max(criticality, 0.0), 1.0), 4) + + +# --------------------------------------------------------------------------- +# Persistence +# --------------------------------------------------------------------------- + + +def store_flows(store: GraphStore, flows: list[dict]) -> int: + """Clear existing flows and persist new ones. + + Returns the number of flows stored. + """ + # NOTE: store_flows uses _conn directly because it performs + # multi-statement batch writes (DELETE + INSERT loop) that are + # tightly coupled to the DB transaction lifecycle. + conn = store._conn + + if conn.in_transaction: + logger.warning("Rolling back uncommitted transaction before BEGIN IMMEDIATE") + conn.rollback() + # Wrap the full DELETE + INSERT sequence in an explicit transaction + # so partial writes cannot occur if an exception interrupts the loop. + conn.execute("BEGIN IMMEDIATE") + try: + conn.execute("DELETE FROM flow_memberships") + conn.execute("DELETE FROM flows") + + count = 0 + for flow in flows: + path_json = json.dumps(flow.get("path", [])) + conn.execute( + """INSERT INTO flows + (name, entry_point_id, depth, node_count, file_count, + criticality, path_json) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + ( + flow["name"], + flow["entry_point_id"], + flow["depth"], + flow["node_count"], + flow["file_count"], + flow["criticality"], + path_json, + ), + ) + flow_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] + + # Insert memberships. + node_ids = flow.get("path", []) + for position, node_id in enumerate(node_ids): + conn.execute( + "INSERT OR IGNORE INTO flow_memberships (flow_id, node_id, position) " + "VALUES (?, ?, ?)", + (flow_id, node_id, position), + ) + count += 1 + + conn.commit() + except BaseException: + conn.rollback() + raise + return count + + +def incremental_trace_flows( + store: GraphStore, + changed_files: list[str], + max_depth: int = 15, +) -> int: + """Re-trace only flows that touch *changed_files*. Much faster than full trace. + + 1. Find flow IDs whose memberships reference nodes in *changed_files*. + 2. Collect the entry-point node IDs of those flows before deleting them. + 3. Delete only the affected flows and their memberships. + 4. Re-detect entry points, keeping those in *changed_files* **or** whose + node ID was an entry point of a deleted flow. + 5. BFS-trace each relevant entry point via :func:`_trace_single_flow`. + 6. INSERT the new flows (without clearing unrelated flows). + + Returns the number of re-traced flows that were stored. + """ + if not changed_files: + return 0 + + conn = store._conn + changed_file_set = set(changed_files) + + # ------------------------------------------------------------------ + # 1. Find affected flow IDs + # ------------------------------------------------------------------ + placeholders = ",".join("?" * len(changed_files)) + affected_rows = conn.execute( + f"SELECT DISTINCT fm.flow_id FROM flow_memberships fm " # nosec B608 + f"JOIN nodes n ON n.id = fm.node_id " + f"WHERE n.file_path IN ({placeholders})", + changed_files, + ).fetchall() + affected_ids = [r[0] for r in affected_rows] + + # ------------------------------------------------------------------ + # 2. Collect old entry-point node IDs before deletion + # ------------------------------------------------------------------ + entry_point_ids: set[int] = set() + if affected_ids: + ep_placeholders = ",".join("?" * len(affected_ids)) + ep_rows = conn.execute( + f"SELECT entry_point_id FROM flows " # nosec B608 + f"WHERE id IN ({ep_placeholders})", + affected_ids, + ).fetchall() + entry_point_ids = {r[0] for r in ep_rows} + + # ------------------------------------------------------------------ + # 3. Delete affected flows and their memberships + # ------------------------------------------------------------------ + # Wrap in an explicit transaction so a crash mid-loop cannot leave + # orphaned flow_memberships rows pointing at deleted flows. See #258. + if affected_ids: + if conn.in_transaction: + conn.commit() + conn.execute("BEGIN IMMEDIATE") + try: + for fid in affected_ids: + conn.execute( + "DELETE FROM flow_memberships WHERE flow_id = ?", (fid,), + ) + conn.execute("DELETE FROM flows WHERE id = ?", (fid,)) + conn.commit() + except BaseException: + conn.rollback() + raise + + # ------------------------------------------------------------------ + # 4. Re-detect entry points and filter to relevant ones + # ------------------------------------------------------------------ + entry_points = detect_entry_points(store) + relevant_eps = [ + ep for ep in entry_points + if ep.file_path in changed_file_set or ep.id in entry_point_ids + ] + + # ------------------------------------------------------------------ + # 5. BFS-trace each relevant entry point + # ------------------------------------------------------------------ + new_flows: list[dict] = [] + if relevant_eps: + adj = store.load_flow_adjacency() + for ep in relevant_eps: + flow = _trace_single_flow(adj, ep, max_depth) + if flow is not None: + new_flows.append(flow) + + # ------------------------------------------------------------------ + # 6. INSERT new flows without clearing unrelated ones + # ------------------------------------------------------------------ + count = 0 + for flow in new_flows: + path_json = json.dumps(flow.get("path", [])) + conn.execute( + """INSERT INTO flows + (name, entry_point_id, depth, node_count, file_count, + criticality, path_json) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + ( + flow["name"], + flow["entry_point_id"], + flow["depth"], + flow["node_count"], + flow["file_count"], + flow["criticality"], + path_json, + ), + ) + flow_id = conn.execute("SELECT last_insert_rowid()").fetchone()[0] + + node_ids = flow.get("path", []) + for position, node_id in enumerate(node_ids): + conn.execute( + "INSERT OR IGNORE INTO flow_memberships (flow_id, node_id, position) " + "VALUES (?, ?, ?)", + (flow_id, node_id, position), + ) + count += 1 + + conn.commit() + return count + + +# --------------------------------------------------------------------------- +# Query helpers +# --------------------------------------------------------------------------- + + +def get_flows( + store: GraphStore, + sort_by: str = "criticality", + limit: int = 50, +) -> list[dict]: + """Retrieve stored flows from the database. + + Args: + store: The graph store. + sort_by: Column to sort by (``criticality``, ``depth``, ``node_count``). + limit: Maximum number of flows to return. + """ + allowed_sort = {"criticality", "depth", "node_count", "file_count", "name"} + if sort_by not in allowed_sort: + sort_by = "criticality" + + order = "DESC" if sort_by in ("criticality", "depth", "node_count", "file_count") else "ASC" + + # NOTE: get_flows reads from the flows table which is managed by + # the flows module; _conn access is documented coupling. + rows = store._conn.execute( + f"SELECT * FROM flows ORDER BY {sort_by} {order} LIMIT ?", # nosec B608 + (limit,), + ).fetchall() + + results: list[dict] = [] + for row in rows: + results.append({ + "id": row["id"], + "name": _sanitize_name(row["name"]), + "entry_point_id": row["entry_point_id"], + "depth": row["depth"], + "node_count": row["node_count"], + "file_count": row["file_count"], + "criticality": row["criticality"], + "path": json.loads(row["path_json"]), + "created_at": row["created_at"], + "updated_at": row["updated_at"], + }) + return results + + +def get_flow_by_id(store: GraphStore, flow_id: int) -> Optional[dict]: + """Retrieve a single flow with full path details. + + Returns a dict with the flow metadata plus a ``steps`` list containing + each node's name, kind, file, and line info. + """ + # NOTE: get_flow_by_id reads from the flows table; see store_flows note. + row = store._conn.execute( + "SELECT * FROM flows WHERE id = ?", (flow_id,) + ).fetchone() + if row is None: + return None + + path_ids: list[int] = json.loads(row["path_json"]) + + # Build detailed step info. + steps: list[dict] = [] + for nid in path_ids: + node = store.get_node_by_id(nid) + if node: + steps.append({ + "node_id": node.id, + "name": _sanitize_name(node.name), + "kind": node.kind, + "file": node.file_path, + "line_start": node.line_start, + "line_end": node.line_end, + "qualified_name": _sanitize_name(node.qualified_name), + }) + + return { + "id": row["id"], + "name": _sanitize_name(row["name"]), + "entry_point_id": row["entry_point_id"], + "depth": row["depth"], + "node_count": row["node_count"], + "file_count": row["file_count"], + "criticality": row["criticality"], + "path": path_ids, + "steps": steps, + "created_at": row["created_at"], + "updated_at": row["updated_at"], + } + + +def get_affected_flows( + store: GraphStore, + changed_files: list[str], +) -> dict: + """Find flows that include nodes from the given changed files. + + Returns:: + + { + "affected_flows": [], + "total": , + } + """ + if not changed_files: + return {"affected_flows": [], "total": 0} + + # Find node IDs belonging to changed files. + node_ids = store.get_node_ids_by_files(changed_files) + + if not node_ids: + return {"affected_flows": [], "total": 0} + + # Find flow IDs that contain any of these nodes. + flow_ids = store.get_flow_ids_by_node_ids(node_ids) + + if not flow_ids: + return {"affected_flows": [], "total": 0} + + affected: list[dict] = [] + for fid in flow_ids: + flow = get_flow_by_id(store, fid) + if flow: + affected.append(flow) + + # Sort by criticality descending. + affected.sort(key=lambda f: f.get("criticality", 0), reverse=True) + + return { + "affected_flows": affected, + "total": len(affected), + } diff --git a/code_review_graph/graph.py b/code_review_graph/graph.py new file mode 100644 index 0000000..4ed3c7f --- /dev/null +++ b/code_review_graph/graph.py @@ -0,0 +1,1373 @@ +"""SQLite-backed knowledge graph storage and query engine. + +Stores code structure as nodes (File, Class, Function, Type, Test) and +edges (CALLS, IMPORTS_FROM, INHERITS, IMPLEMENTS, CONTAINS, TESTED_BY, DEPENDS_ON, REFERENCES). +Supports impact-radius queries and subgraph extraction. +""" + +from __future__ import annotations + +import json +import logging +import os +import sqlite3 +import threading +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Any, Optional + +import networkx as nx + +from .constants import BFS_ENGINE, MAX_IMPACT_DEPTH, MAX_IMPACT_NODES +from .migrations import get_schema_version, run_migrations +from .parser import EdgeInfo, NodeInfo + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Schema +# --------------------------------------------------------------------------- + +_SCHEMA_SQL = """ +CREATE TABLE IF NOT EXISTS nodes ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind TEXT NOT NULL, -- File, Class, Function, Type, Test + name TEXT NOT NULL, + qualified_name TEXT NOT NULL UNIQUE, + file_path TEXT NOT NULL, + line_start INTEGER, + line_end INTEGER, + language TEXT, + parent_name TEXT, + params TEXT, + return_type TEXT, + modifiers TEXT, + is_test INTEGER DEFAULT 0, + file_hash TEXT, + extra TEXT DEFAULT '{}', + updated_at REAL NOT NULL +); + +CREATE TABLE IF NOT EXISTS edges ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + kind TEXT NOT NULL, -- CALLS, IMPORTS_FROM, INHERITS, REFERENCES, etc. + source_qualified TEXT NOT NULL, + target_qualified TEXT NOT NULL, + file_path TEXT NOT NULL, + line INTEGER DEFAULT 0, + extra TEXT DEFAULT '{}', + confidence REAL DEFAULT 1.0, + confidence_tier TEXT DEFAULT 'EXTRACTED', + updated_at REAL NOT NULL +); + +CREATE TABLE IF NOT EXISTS metadata ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE INDEX IF NOT EXISTS idx_nodes_file ON nodes(file_path); +CREATE INDEX IF NOT EXISTS idx_nodes_kind ON nodes(kind); +CREATE INDEX IF NOT EXISTS idx_nodes_qualified ON nodes(qualified_name); +CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_qualified); +CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_qualified); +CREATE INDEX IF NOT EXISTS idx_edges_kind ON edges(kind); +CREATE INDEX IF NOT EXISTS idx_edges_target_kind ON edges(target_qualified, kind); +CREATE INDEX IF NOT EXISTS idx_edges_source_kind ON edges(source_qualified, kind); +CREATE INDEX IF NOT EXISTS idx_edges_file ON edges(file_path); +""" + + +@dataclass +class GraphNode: + id: int + kind: str + name: str + qualified_name: str + file_path: str + line_start: int + line_end: int + language: str + parent_name: Optional[str] + params: Optional[str] + return_type: Optional[str] + is_test: bool + file_hash: Optional[str] + extra: dict + + +@dataclass +class GraphEdge: + id: int + kind: str + source_qualified: str + target_qualified: str + file_path: str + line: int + extra: dict + confidence: float = 1.0 + confidence_tier: str = "EXTRACTED" + + +@dataclass +class FlowAdjacency: + """In-memory adjacency structure for flow tracing. + + Loaded once via :meth:`GraphStore.load_flow_adjacency` and passed to + ``trace_flows`` / ``compute_criticality`` to avoid per-edge SQLite + point queries on large graphs. + """ + calls_out: dict[str, list[str]] + has_tested_by: set[str] + nodes_by_qn: dict[str, "GraphNode"] + nodes_by_id: dict[int, "GraphNode"] + + +@dataclass +class GraphStats: + total_nodes: int + total_edges: int + nodes_by_kind: dict[str, int] + edges_by_kind: dict[str, int] + languages: list[str] + files_count: int + last_updated: Optional[str] + + +# --------------------------------------------------------------------------- +# GraphStore +# --------------------------------------------------------------------------- + + +class GraphStore: + """SQLite-backed code knowledge graph.""" + + def __init__(self, db_path: str | Path) -> None: + self.db_path = Path(db_path) + self.db_path.parent.mkdir(parents=True, exist_ok=True) + self._conn = sqlite3.connect( + str(self.db_path), timeout=30, check_same_thread=False, + isolation_level=None, # Disable implicit transactions (#135) + ) + self._conn.row_factory = sqlite3.Row + self._conn.execute("PRAGMA journal_mode=WAL") + self._conn.execute("PRAGMA busy_timeout=5000") + self._init_schema() + # Ensure schema_version is set, then run pending migrations + if get_schema_version(self._conn) < 1: + # Fresh DB — metadata table just created by _init_schema + self._conn.execute( + "INSERT OR IGNORE INTO metadata (key, value) " + "VALUES ('schema_version', '1')" + ) + self._conn.commit() + run_migrations(self._conn) + self._nxg_cache: nx.DiGraph | None = None + self._cache_lock = threading.Lock() + + def __enter__(self) -> "GraphStore": + return self + + def __exit__(self, exc_type, exc_val, exc_tb) -> None: + self.close() + + def _init_schema(self) -> None: + self._conn.executescript(_SCHEMA_SQL) + self._conn.commit() + + def _invalidate_cache(self) -> None: + """Invalidate the cached NetworkX graph after write operations.""" + with self._cache_lock: + self._nxg_cache = None + + def close(self) -> None: + self._conn.close() + + # --- Write operations --- + + def upsert_node(self, node: NodeInfo, file_hash: str = "") -> int: + """Insert or update a node. Returns the node ID.""" + now = time.time() + qualified = self._make_qualified(node) + extra = json.dumps(node.extra) if node.extra else "{}" + + self._conn.execute( + """INSERT INTO nodes + (kind, name, qualified_name, file_path, line_start, line_end, + language, parent_name, params, return_type, modifiers, is_test, + file_hash, extra, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(qualified_name) DO UPDATE SET + kind=excluded.kind, name=excluded.name, + file_path=excluded.file_path, line_start=excluded.line_start, + line_end=excluded.line_end, language=excluded.language, + parent_name=excluded.parent_name, params=excluded.params, + return_type=excluded.return_type, modifiers=excluded.modifiers, + is_test=excluded.is_test, file_hash=excluded.file_hash, + extra=excluded.extra, updated_at=excluded.updated_at + """, + ( + node.kind, node.name, qualified, node.file_path, + node.line_start, node.line_end, node.language, + node.parent_name, node.params, node.return_type, + node.modifiers, int(node.is_test), file_hash, + extra, now, + ), + ) + row = self._conn.execute( + "SELECT id FROM nodes WHERE qualified_name = ?", (qualified,) + ).fetchone() + return row["id"] + + def upsert_edge(self, edge: EdgeInfo) -> int: + """Insert or update an edge.""" + now = time.time() + extra_dict = edge.extra if edge.extra else {} + confidence = float(extra_dict.get("confidence", 1.0)) + confidence_tier = str(extra_dict.get("confidence_tier", "EXTRACTED")) + extra = json.dumps(extra_dict) + + # Check for existing edge (include line so multiple call sites are preserved) + existing = self._conn.execute( + """SELECT id FROM edges + WHERE kind=? AND source_qualified=? AND target_qualified=? + AND file_path=? AND line=?""", + (edge.kind, edge.source, edge.target, edge.file_path, edge.line), + ).fetchone() + + if existing: + self._conn.execute( + "UPDATE edges SET line=?, extra=?, confidence=?, confidence_tier=?," + " updated_at=? WHERE id=?", + (edge.line, extra, confidence, confidence_tier, now, existing["id"]), + ) + return existing["id"] + + self._conn.execute( + """INSERT INTO edges + (kind, source_qualified, target_qualified, file_path, line, extra, + confidence, confidence_tier, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""", + (edge.kind, edge.source, edge.target, edge.file_path, edge.line, extra, + confidence, confidence_tier, now), + ) + return self._conn.execute("SELECT last_insert_rowid()").fetchone()[0] + + def remove_file_data(self, file_path: str) -> None: + """Remove all nodes and edges associated with a file.""" + self._conn.execute("DELETE FROM nodes WHERE file_path = ?", (file_path,)) + self._conn.execute("DELETE FROM edges WHERE file_path = ?", (file_path,)) + self._invalidate_cache() + + def _begin_immediate(self) -> None: + """Start an IMMEDIATE transaction, rolling back any prior uncommitted + transaction first (regression guard for #135 / #489). + """ + if self._conn.in_transaction: + logger.warning("Rolling back uncommitted transaction before BEGIN IMMEDIATE") + self._conn.rollback() + self._conn.execute("BEGIN IMMEDIATE") + + def store_file_nodes_edges( + self, file_path: str, nodes: list[NodeInfo], edges: list[EdgeInfo], fhash: str = "" + ) -> None: + """Atomically replace all data for a file.""" + self._begin_immediate() + try: + self.remove_file_data(file_path) + for node in nodes: + self.upsert_node(node, file_hash=fhash) + for edge in edges: + self.upsert_edge(edge) + self._conn.commit() + except BaseException: + self._conn.rollback() + raise + self._invalidate_cache() + + def store_file_batch( + self, batch: list[tuple[str, list[NodeInfo], list[EdgeInfo], str]] + ) -> None: + """Atomically replace data for a batch of files in one transaction.""" + self._begin_immediate() + try: + for file_path, nodes, edges, fhash in batch: + self.remove_file_data(file_path) + for node in nodes: + self.upsert_node(node, file_hash=fhash) + for edge in edges: + self.upsert_edge(edge) + self._conn.commit() + except BaseException: + self._conn.rollback() + raise + self._invalidate_cache() + + def set_metadata(self, key: str, value: str) -> None: + self._conn.execute( + "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)", (key, value) + ) + self._conn.commit() + + def get_metadata(self, key: str) -> Optional[str]: + row = self._conn.execute("SELECT value FROM metadata WHERE key=?", (key,)).fetchone() + return row["value"] if row else None + + def commit(self) -> None: + self._conn.commit() + + def rollback(self) -> None: + """Rollback the current transaction.""" + self._conn.rollback() + + # --- Read operations --- + + def get_node(self, qualified_name: str) -> Optional[GraphNode]: + row = self._conn.execute( + "SELECT * FROM nodes WHERE qualified_name = ?", (qualified_name,) + ).fetchone() + return self._row_to_node(row) if row else None + + def get_nodes_by_file(self, file_path: str) -> list[GraphNode]: + rows = self._conn.execute( + "SELECT * FROM nodes WHERE file_path = ?", (file_path,) + ).fetchall() + return [self._row_to_node(r) for r in rows] + + def get_all_nodes(self, exclude_files: bool = True) -> list[GraphNode]: + """Return all nodes, optionally excluding File nodes.""" + if exclude_files: + rows = self._conn.execute( + "SELECT * FROM nodes WHERE kind != 'File'" + ).fetchall() + else: + rows = self._conn.execute("SELECT * FROM nodes").fetchall() + return [self._row_to_node(r) for r in rows] + + def get_edges_by_source(self, qualified_name: str) -> list[GraphEdge]: + rows = self._conn.execute( + "SELECT * FROM edges WHERE source_qualified = ?", (qualified_name,) + ).fetchall() + return [self._row_to_edge(r) for r in rows] + + def get_edges_by_target(self, qualified_name: str) -> list[GraphEdge]: + rows = self._conn.execute( + "SELECT * FROM edges WHERE target_qualified = ?", (qualified_name,) + ).fetchall() + return [self._row_to_edge(r) for r in rows] + + def search_edges_by_target_name(self, name: str, kind: str = "CALLS") -> list[GraphEdge]: + """Search for edges where target_qualified matches an unqualified name. + + CALLS edges often store unqualified target names (e.g. ``generateTestCode``) + rather than fully qualified ones (``file.ts::generateTestCode``). This + method finds those edges by exact match on the plain function name so that + reverse call tracing (callers_of) works even when qualified-name lookup + returns nothing. + """ + rows = self._conn.execute( + "SELECT * FROM edges WHERE target_qualified = ? AND kind = ?", + (name, kind), + ).fetchall() + return [self._row_to_edge(r) for r in rows] + + def get_transitive_tests( + self, qualified_name: str, max_depth: int = 1, max_frontier: int | None = None, + ) -> list[dict]: + """Find tests covering a node, including indirect (transitive) coverage. + + 1. Direct: TESTED_BY edges targeting this node (+ bare-name fallback). + 2. Indirect: follow outgoing CALLS edges up to *max_depth* hops, + then collect TESTED_BY edges on each callee. + + Returns a list of dicts with node fields plus ``indirect: bool``. + + ``max_frontier`` caps the CALLS fan-out per BFS hop to prevent O(N*M) + query explosion on hub functions in large graphs. Defaults to + ``CRG_MAX_TRANSITIVE_FRONTIER`` env var (50 if unset). + """ + if max_frontier is None: + max_frontier = int(os.environ.get("CRG_MAX_TRANSITIVE_FRONTIER", "50")) + conn = self._conn + seen: set[str] = set() + results: list[dict] = [] + + # If the input is a class, expand to its methods first. + input_qns = [qualified_name] + row = conn.execute( + "SELECT kind FROM nodes WHERE qualified_name = ?", + (qualified_name,), + ).fetchone() + if row and row["kind"] == "Class": + for mrow in conn.execute( + "SELECT target_qualified FROM edges " + "WHERE source_qualified = ? AND kind = 'CONTAINS'", + (qualified_name,), + ).fetchall(): + input_qns.append(mrow["target_qualified"]) + + def _node_dict(qn: str, indirect: bool) -> dict | None: + row = conn.execute( + "SELECT * FROM nodes WHERE qualified_name = ?", (qn,) + ).fetchone() + if not row: + return None + return { + "name": row["name"], + "qualified_name": row["qualified_name"], + "file_path": row["file_path"], + "kind": row["kind"], + "indirect": indirect, + } + + # Direct TESTED_BY + for qn in input_qns: + for row in conn.execute( + "SELECT source_qualified FROM edges " + "WHERE target_qualified = ? AND kind = 'TESTED_BY'", + (qn,), + ).fetchall(): + src = row["source_qualified"] + if src not in seen: + seen.add(src) + d = _node_dict(src, indirect=False) + if d: + results.append(d) + + # Bare-name fallback for direct + bare = qualified_name.rsplit("::", 1)[-1] if "::" in qualified_name else qualified_name + for row in conn.execute( + "SELECT source_qualified FROM edges " + "WHERE target_qualified = ? AND kind = 'TESTED_BY'", + (bare,), + ).fetchall(): + src = row["source_qualified"] + if src not in seen: + seen.add(src) + d = _node_dict(src, indirect=False) + if d: + results.append(d) + + # Transitive: follow CALLS edges, then collect TESTED_BY on callees + frontier = set(input_qns) + for _ in range(max_depth): + next_frontier: set[str] = set() + for qn in frontier: + for row in conn.execute( + "SELECT target_qualified FROM edges " + "WHERE source_qualified = ? AND kind = 'CALLS'", + (qn,), + ).fetchall(): + next_frontier.add(row["target_qualified"]) + if len(next_frontier) > max_frontier: + next_frontier = set(list(next_frontier)[:max_frontier]) + for callee in next_frontier: + for row in conn.execute( + "SELECT source_qualified FROM edges " + "WHERE target_qualified = ? AND kind = 'TESTED_BY'", + (callee,), + ).fetchall(): + src = row["source_qualified"] + if src not in seen: + seen.add(src) + d = _node_dict(src, indirect=True) + if d: + results.append(d) + frontier = next_frontier + + return results + + def resolve_bare_call_targets(self) -> int: + """Batch-resolve bare-name CALLS targets using the global node table. + + After parsing, some CALLS edges have bare targets (no ``::`` separator) + because the parser couldn't resolve cross-file. This method matches + them against nodes and updates unambiguous matches in-place. + + Disambiguation strategy: + 1. Single node with that name -> resolve directly + 2. Multiple candidates -> prefer one whose file is imported by the + source file (via IMPORTS_FROM edges) + + Returns the number of resolved edges. + """ + conn = self._conn + + bare_edges = conn.execute( + "SELECT id, source_qualified, target_qualified, file_path " + "FROM edges WHERE kind = 'CALLS' AND target_qualified NOT LIKE '%::%'" + ).fetchall() + if not bare_edges: + return 0 + + # bare_name -> list of qualified_names + node_lookup: dict[str, list[str]] = {} + for row in conn.execute( + "SELECT name, qualified_name FROM nodes " + "WHERE kind IN ('Function', 'Test', 'Class')" + ).fetchall(): + node_lookup.setdefault(row["name"], []).append(row["qualified_name"]) + + # source_file -> set of imported files (for disambiguation) + import_targets: dict[str, set[str]] = {} + for row in conn.execute( + "SELECT DISTINCT file_path, target_qualified FROM edges " + "WHERE kind = 'IMPORTS_FROM'" + ).fetchall(): + target = row["target_qualified"] + target_file = target.split("::", 1)[0] if "::" in target else target + import_targets.setdefault(row["file_path"], set()).add(target_file) + + resolved = 0 + for edge in bare_edges: + bare_name = edge["target_qualified"] + candidates = node_lookup.get(bare_name, []) + if not candidates: + continue + + if len(candidates) == 1: + qualified = candidates[0] + else: + # Disambiguate via imports + src_qn = edge["source_qualified"] + src_file = ( + src_qn.split("::", 1)[0] if "::" in src_qn + else edge["file_path"] + ) + imported_files = import_targets.get(src_file, set()) + imported = [ + c for c in candidates + if c.split("::", 1)[0] in imported_files + ] + if len(imported) == 1: + qualified = imported[0] + else: + continue + + conn.execute( + "UPDATE edges SET target_qualified = ? WHERE id = ?", + (qualified, edge["id"]), + ) + resolved += 1 + + if resolved: + conn.commit() + logger.info("Resolved %d bare-name CALLS targets", resolved) + return resolved + + def get_all_files(self) -> list[str]: + rows = self._conn.execute( + "SELECT DISTINCT file_path FROM nodes WHERE kind = 'File'" + ).fetchall() + return [r["file_path"] for r in rows] + + def search_nodes(self, query: str, limit: int = 20) -> list[GraphNode]: + """Keyword search across node names. + + Tries FTS5 first (fast, tokenized matching), then falls back to + LIKE-based substring search when FTS5 returns no results. + """ + words = query.split() + if not words: + return [] + + # Phase 1: FTS5 search (uses the indexed nodes_fts table) + try: + if len(words) == 1: + fts_query = '"' + query.replace('"', '""') + '"' + else: + fts_query = " AND ".join( + '"' + w.replace('"', '""') + '"' for w in words + ) + rows = self._conn.execute( + "SELECT n.* FROM nodes_fts f " + "JOIN nodes n ON f.rowid = n.id " + "WHERE nodes_fts MATCH ? LIMIT ?", + (fts_query, limit), + ).fetchall() + if rows: + return [self._row_to_node(r) for r in rows] + except Exception: # nosec B110 - FTS5 table may not exist on older schemas + pass + + # Phase 2: LIKE fallback (substring matching) + conditions: list[str] = [] + params: list[str | int] = [] + for word in words: + w = word.lower() + conditions.append( + "(LOWER(name) LIKE ? OR LOWER(qualified_name) LIKE ?)" + ) + params.extend([f"%{w}%", f"%{w}%"]) + + where = " AND ".join(conditions) + sql = f"SELECT * FROM nodes WHERE {where} LIMIT ?" # nosec B608 + params.append(limit) + rows = self._conn.execute(sql, params).fetchall() + return [self._row_to_node(r) for r in rows] + + # --- Impact / Graph traversal --- + + def get_impact_radius( + self, + changed_files: list[str], + max_depth: int = MAX_IMPACT_DEPTH, + max_nodes: int = MAX_IMPACT_NODES, + ) -> dict[str, Any]: + """BFS from changed files to find all impacted nodes within depth N. + + Delegates to ``get_impact_radius_sql()`` by default (faster for + large graphs). Set ``CRG_BFS_ENGINE=networkx`` to use the legacy + Python-side BFS via NetworkX. + + Returns dict with: + - changed_nodes: nodes in changed files + - impacted_nodes: nodes reachable via edges + - impacted_files: unique set of affected files + - edges: connecting edges + """ + if BFS_ENGINE == "networkx": + return self._get_impact_radius_networkx( + changed_files, max_depth=max_depth, max_nodes=max_nodes, + ) + return self.get_impact_radius_sql( + changed_files, max_depth=max_depth, max_nodes=max_nodes, + ) + + # -- SQLite recursive CTE version (default) --------------------------- + + def get_impact_radius_sql( + self, + changed_files: list[str], + max_depth: int = MAX_IMPACT_DEPTH, + max_nodes: int = MAX_IMPACT_NODES, + ) -> dict[str, Any]: + """Impact radius via SQLite recursive CTE. + + Faster than NetworkX for large graphs because it avoids + materialising the full graph in Python. + """ + if not changed_files: + return { + "changed_nodes": [], + "impacted_nodes": [], + "impacted_files": [], + "edges": [], + "truncated": False, + "total_impacted": 0, + } + + # Seed qualified names + seeds: set[str] = set() + for f in changed_files: + nodes = self.get_nodes_by_file(f) + for n in nodes: + seeds.add(n.qualified_name) + + if not seeds: + return { + "changed_nodes": [], + "impacted_nodes": [], + "impacted_files": [], + "edges": [], + "truncated": False, + "total_impacted": 0, + } + + # Build recursive CTE — use a temp table for the seed set to + # keep the query plan efficient and stay under variable limits. + self._conn.execute( + "CREATE TEMP TABLE IF NOT EXISTS _impact_seeds " + "(qn TEXT PRIMARY KEY)" + ) + self._conn.execute("DELETE FROM _impact_seeds") + batch_size = 450 + seed_list = list(seeds) + for i in range(0, len(seed_list), batch_size): + batch = seed_list[i:i + batch_size] + placeholders = ",".join("(?)" for _ in batch) + self._conn.execute( # nosec B608 + f"INSERT OR IGNORE INTO _impact_seeds (qn) VALUES {placeholders}", + batch, + ) + + cte_sql = """ + WITH RECURSIVE impacted(node_qn, depth) AS ( + SELECT qn, 0 FROM _impact_seeds + UNION + SELECT e.target_qualified, i.depth + 1 + FROM impacted i + JOIN edges e ON e.source_qualified = i.node_qn + WHERE i.depth < ? + UNION + SELECT e.source_qualified, i.depth + 1 + FROM impacted i + JOIN edges e ON e.target_qualified = i.node_qn + WHERE i.depth < ? + ) + SELECT DISTINCT node_qn, MIN(depth) AS min_depth + FROM impacted + GROUP BY node_qn + LIMIT ? + """ + rows = self._conn.execute( + cte_sql, (max_depth, max_depth, max_nodes + len(seeds)), + ).fetchall() + + # Split into seeds vs impacted + impacted_qns: set[str] = set() + for r in rows: + qn = r[0] + if qn not in seeds: + impacted_qns.add(qn) + + # Batch-fetch nodes + changed_nodes = self._batch_get_nodes(seeds) + impacted_nodes = self._batch_get_nodes(impacted_qns) + + total_impacted = len(impacted_nodes) + truncated = total_impacted > max_nodes + if truncated: + impacted_nodes = impacted_nodes[:max_nodes] + + impacted_files = list({n.file_path for n in impacted_nodes}) + + relevant_edges: list[GraphEdge] = [] + all_qns = seeds | {n.qualified_name for n in impacted_nodes} + if all_qns: + relevant_edges = self.get_edges_among(all_qns) + + return { + "changed_nodes": changed_nodes, + "impacted_nodes": impacted_nodes, + "impacted_files": impacted_files, + "edges": relevant_edges, + "truncated": truncated, + "total_impacted": total_impacted, + } + + # -- NetworkX BFS version (legacy) ------------------------------------ + + def _get_impact_radius_networkx( + self, + changed_files: list[str], + max_depth: int = MAX_IMPACT_DEPTH, + max_nodes: int = MAX_IMPACT_NODES, + ) -> dict[str, Any]: + """BFS via NetworkX (legacy). Used when CRG_BFS_ENGINE=networkx.""" + nxg = self._build_networkx_graph() + + seeds: set[str] = set() + for f in changed_files: + nodes = self.get_nodes_by_file(f) + for n in nodes: + seeds.add(n.qualified_name) + + visited: set[str] = set() + frontier = seeds.copy() + depth = 0 + impacted: set[str] = set() + + while frontier and depth < max_depth: + visited.update(frontier) + next_frontier: set[str] = set() + for qn in frontier: + if qn in nxg: + for neighbor in nxg.neighbors(qn): + if neighbor not in visited: + next_frontier.add(neighbor) + impacted.add(neighbor) + if qn in nxg: + for pred in nxg.predecessors(qn): + if pred not in visited: + next_frontier.add(pred) + impacted.add(pred) + next_frontier -= visited + if len(visited) + len(next_frontier) > max_nodes: + break + frontier = next_frontier + depth += 1 + + changed_nodes = self._batch_get_nodes(seeds) + impacted_qns = impacted - seeds + impacted_nodes = self._batch_get_nodes(impacted_qns) + + total_impacted = len(impacted_nodes) + truncated = total_impacted > max_nodes + if truncated: + impacted_nodes = impacted_nodes[:max_nodes] + + impacted_files = list({n.file_path for n in impacted_nodes}) + + relevant_edges: list[GraphEdge] = [] + all_qns = seeds | {n.qualified_name for n in impacted_nodes} + if all_qns: + relevant_edges = self.get_edges_among(all_qns) + + return { + "changed_nodes": changed_nodes, + "impacted_nodes": impacted_nodes, + "impacted_files": impacted_files, + "edges": relevant_edges, + "truncated": truncated, + "total_impacted": total_impacted, + } + + def get_subgraph(self, qualified_names: list[str]) -> dict[str, Any]: + """Extract a subgraph containing the specified nodes and their connecting edges.""" + nodes = [] + for qn in qualified_names: + node = self.get_node(qn) + if node: + nodes.append(node) + + edges = [] + qn_set = set(qualified_names) + for qn in qualified_names: + for e in self.get_edges_by_source(qn): + if e.target_qualified in qn_set: + edges.append(e) + + return {"nodes": nodes, "edges": edges} + + def get_stats(self) -> GraphStats: + """Return aggregate statistics about the graph.""" + total_nodes = self._conn.execute("SELECT COUNT(*) FROM nodes").fetchone()[0] + total_edges = self._conn.execute("SELECT COUNT(*) FROM edges").fetchone()[0] + + nodes_by_kind: dict[str, int] = {} + for row in self._conn.execute("SELECT kind, COUNT(*) as cnt FROM nodes GROUP BY kind"): + nodes_by_kind[row["kind"]] = row["cnt"] + + edges_by_kind: dict[str, int] = {} + for row in self._conn.execute("SELECT kind, COUNT(*) as cnt FROM edges GROUP BY kind"): + edges_by_kind[row["kind"]] = row["cnt"] + + languages = [ + r["language"] for r in self._conn.execute( + "SELECT DISTINCT language FROM nodes WHERE language IS NOT NULL AND language != ''" + ) + ] + + files_count = self._conn.execute( + "SELECT COUNT(*) FROM nodes WHERE kind = 'File'" + ).fetchone()[0] + + last_updated = self.get_metadata("last_updated") + + return GraphStats( + total_nodes=total_nodes, + total_edges=total_edges, + nodes_by_kind=nodes_by_kind, + edges_by_kind=edges_by_kind, + languages=languages, + files_count=files_count, + last_updated=last_updated, + ) + + def get_nodes_by_size( + self, + min_lines: int = 50, + max_lines: int | None = None, + kind: str | None = None, + file_path_pattern: str | None = None, + limit: int = 50, + ) -> list[GraphNode]: + """Find nodes within a line-count range, ordered largest first. + + Args: + min_lines: Minimum line count threshold (inclusive). + max_lines: Maximum line count threshold (inclusive). None = no upper bound. + kind: Filter by node kind (Function, Class, File, etc.). + file_path_pattern: SQL LIKE pattern to filter by file path. + limit: Maximum results to return. + + Returns: + List of GraphNode objects, ordered by line count descending. + """ + conditions = [ + "line_start IS NOT NULL", + "line_end IS NOT NULL", + "(line_end - line_start + 1) >= ?", + ] + params: list = [min_lines] + + if max_lines is not None: + conditions.append("(line_end - line_start + 1) <= ?") + params.append(max_lines) + if kind: + conditions.append("kind = ?") + params.append(kind) + if file_path_pattern: + conditions.append("file_path LIKE ?") + params.append(f"%{file_path_pattern}%") + + params.append(limit) + where = " AND ".join(conditions) + rows = self._conn.execute( + f"SELECT * FROM nodes WHERE {where} " # nosec B608 + "ORDER BY (line_end - line_start + 1) DESC LIMIT ?", + params, + ).fetchall() + return [self._row_to_node(r) for r in rows] + + # --- Public query helpers (used by flows, changes, communities, etc.) --- + + def get_node_by_id(self, node_id: int) -> Optional[GraphNode]: + """Fetch a single node by its integer primary key.""" + row = self._conn.execute( + "SELECT * FROM nodes WHERE id = ?", (node_id,) + ).fetchone() + return self._row_to_node(row) if row else None + + def get_nodes_by_kind( + self, + kinds: list[str], + file_pattern: str | None = None, + ) -> list[GraphNode]: + """Return nodes matching any of *kinds*, optionally filtered by file. + + Args: + kinds: List of node kind strings (e.g. ``["Function", "Test"]``). + file_pattern: If provided, only nodes whose ``file_path`` + contains *file_pattern* (SQL LIKE ``%pattern%``) are + returned. + """ + if not kinds: + return [] + placeholders = ",".join("?" for _ in kinds) + conditions = [f"kind IN ({placeholders})"] + params: list[str] = list(kinds) + if file_pattern: + conditions.append("file_path LIKE ?") + params.append(f"%{file_pattern}%") + where = " AND ".join(conditions) + rows = self._conn.execute( # nosec B608 + f"SELECT * FROM nodes WHERE {where}", params, + ).fetchall() + return [self._row_to_node(r) for r in rows] + + def count_flow_memberships(self, node_id: int) -> int: + """Return the number of flows a node participates in.""" + row = self._conn.execute( + "SELECT COUNT(*) as cnt FROM flow_memberships " + "WHERE node_id = ?", + (node_id,), + ).fetchone() + return row["cnt"] if row else 0 + + def get_flow_criticalities_for_node(self, node_id: int) -> list[float]: + """Return criticality values for all flows a node participates in.""" + rows = self._conn.execute( + "SELECT f.criticality FROM flows f " + "JOIN flow_memberships fm ON fm.flow_id = f.id " + "WHERE fm.node_id = ?", + (node_id,), + ).fetchall() + return [r["criticality"] for r in rows] + + def get_node_community_id(self, node_id: int) -> int | None: + """Return the ``community_id`` for a node, or ``None``.""" + row = self._conn.execute( + "SELECT community_id FROM nodes WHERE id = ?", + (node_id,), + ).fetchone() + if row and row["community_id"] is not None: + return row["community_id"] + return None + + def get_community_ids_by_qualified_names( + self, qns: list[str], + ) -> dict[str, int | None]: + """Batch-fetch ``community_id`` for a list of qualified names. + + Returns a mapping from qualified name to community_id (may be + ``None`` if the node has no assigned community). + """ + result: dict[str, int | None] = {} + batch_size = 450 + for i in range(0, len(qns), batch_size): + batch = qns[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + "SELECT qualified_name, community_id FROM nodes " + f"WHERE qualified_name IN ({placeholders})", + batch, + ).fetchall() + for r in rows: + result[r["qualified_name"]] = r["community_id"] + return result + + def get_files_matching(self, pattern: str) -> list[str]: + """Return distinct ``file_path`` values matching a LIKE suffix.""" + rows = self._conn.execute( + "SELECT DISTINCT file_path FROM nodes " + "WHERE file_path LIKE ?", + (f"%{pattern}",), + ).fetchall() + return [r["file_path"] for r in rows] + + def get_nodes_without_signature(self) -> list[sqlite3.Row]: + """Return raw rows for nodes that have no signature yet.""" + return self._conn.execute( + "SELECT id, name, kind, params, return_type " + "FROM nodes WHERE signature IS NULL" + ).fetchall() + + def update_node_signature( + self, node_id: int, signature: str, + ) -> None: + """Set the ``signature`` column for a single node.""" + self._conn.execute( + "UPDATE nodes SET signature = ? WHERE id = ?", + (signature, node_id), + ) + + def get_all_community_ids(self) -> dict[str, int | None]: + """Return a mapping of *all* qualified names to their community_id. + + Used primarily by the visualization exporter. + """ + try: + rows = self._conn.execute( + "SELECT qualified_name, community_id FROM nodes" + ).fetchall() + return { + r["qualified_name"]: r["community_id"] + for r in rows + } + except sqlite3.OperationalError as exc: + # community_id column may not exist yet on pre-v6 schemas + logger.debug("Community IDs unavailable (schema not yet migrated): %s", exc) + return {} + + def get_node_ids_by_files( + self, file_paths: list[str], + ) -> set[int]: + """Return node IDs belonging to the given file paths.""" + if not file_paths: + return set() + result: set[int] = set() + batch_size = 450 + for i in range(0, len(file_paths), batch_size): + batch = file_paths[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + "SELECT id FROM nodes " + f"WHERE file_path IN ({placeholders})", + batch, + ).fetchall() + result.update(r["id"] for r in rows) + return result + + def get_flow_ids_by_node_ids( + self, node_ids: set[int], + ) -> list[int]: + """Return distinct flow IDs that contain any of *node_ids*.""" + if not node_ids: + return [] + nids = list(node_ids) + result: list[int] = [] + batch_size = 450 + for i in range(0, len(nids), batch_size): + batch = nids[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + "SELECT DISTINCT flow_id FROM flow_memberships " + f"WHERE node_id IN ({placeholders})", + batch, + ).fetchall() + result.extend(r["flow_id"] for r in rows) + # Deduplicate across batches + return list(dict.fromkeys(result)) + + def get_flow_qualified_names(self, flow_id: int) -> set[str]: + """Return the set of qualified names for nodes in a flow.""" + rows = self._conn.execute( + "SELECT n.qualified_name FROM flow_memberships fm " + "JOIN nodes n ON fm.node_id = n.id WHERE fm.flow_id = ?", + (flow_id,), + ).fetchall() + return {r["qualified_name"] for r in rows} + + def get_node_kind_by_id(self, node_id: int) -> str | None: + """Return just the ``kind`` column for a node, or ``None``.""" + row = self._conn.execute( + "SELECT kind FROM nodes WHERE id = ?", (node_id,), + ).fetchone() + return row["kind"] if row else None + + def get_all_call_targets(self, include_file_sources: bool = True) -> set[str]: + """Return the set of all CALLS-edge target qualified names. + + When ``include_file_sources`` is False, CALLS edges whose source is a + File node (module-scope calls from top-level script glue, CLI + entrypoints, or notebook cells) are excluded. Callers that treat "has + an incoming call" as "is not a root" (e.g. entry-point detection) + should pass ``include_file_sources=False`` — otherwise a script-only + callee looks called and is hidden from flow analysis. + + The File-node filter joins against ``nodes.kind`` rather than pattern- + matching ``source_qualified`` so that file paths containing ``::`` or + any future change to the File-node naming convention cannot silently + miscategorize edges. + """ + if include_file_sources: + rows = self._conn.execute( + "SELECT DISTINCT target_qualified FROM edges " + "WHERE kind = 'CALLS'" + ).fetchall() + else: + rows = self._conn.execute( + "SELECT DISTINCT e.target_qualified FROM edges e " + "LEFT JOIN nodes n ON n.qualified_name = e.source_qualified " + "WHERE e.kind = 'CALLS' " + "AND (n.kind IS NULL OR n.kind != 'File')" + ).fetchall() + return {r["target_qualified"] for r in rows} + + def get_communities_list( + self, + ) -> list[sqlite3.Row]: + """Return raw rows from the ``communities`` table.""" + try: + return self._conn.execute( + "SELECT id, name FROM communities" + ).fetchall() + except sqlite3.OperationalError as exc: + # communities table doesn't exist yet on pre-v4 schemas + logger.debug("Communities list unavailable (table missing): %s", exc) + return [] + + def get_community_member_qns( + self, community_id: int, + ) -> list[str]: + """Return qualified names of nodes in a community.""" + rows = self._conn.execute( + "SELECT qualified_name FROM nodes " + "WHERE community_id = ?", + (community_id,), + ).fetchall() + return [r["qualified_name"] for r in rows] + + def get_nodes_by_community_id( + self, community_id: int, + ) -> list[GraphNode]: + """Return all nodes belonging to a community.""" + rows = self._conn.execute( + "SELECT * FROM nodes WHERE community_id = ?", + (community_id,), + ).fetchall() + return [self._row_to_node(r) for r in rows] + + def get_outgoing_targets( + self, source_qns: list[str], + ) -> list[str]: + """Return ``target_qualified`` for edges sourced from *source_qns*.""" + results: list[str] = [] + batch_size = 450 + for i in range(0, len(source_qns), batch_size): + batch = source_qns[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + "SELECT target_qualified FROM edges " + f"WHERE source_qualified IN ({placeholders})", + batch, + ).fetchall() + results.extend(r["target_qualified"] for r in rows) + return results + + def get_incoming_sources( + self, target_qns: list[str], + ) -> list[str]: + """Return ``source_qualified`` for edges targeting *target_qns*.""" + results: list[str] = [] + batch_size = 450 + for i in range(0, len(target_qns), batch_size): + batch = target_qns[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + "SELECT source_qualified FROM edges " + f"WHERE target_qualified IN ({placeholders})", + batch, + ).fetchall() + results.extend(r["source_qualified"] for r in rows) + return results + + # --- Public edge access (for visualization etc.) --- + + def get_all_edges(self) -> list[GraphEdge]: + """Return all edges in the graph.""" + rows = self._conn.execute("SELECT * FROM edges").fetchall() + return [self._row_to_edge(r) for r in rows] + + def get_edges_among(self, qualified_names: set[str]) -> list[GraphEdge]: + """Return edges where both source and target are in the given set. + + Batches the source-side IN clause to stay under SQLite's default + SQLITE_MAX_VARIABLE_NUMBER limit, then filters targets in Python. + """ + if not qualified_names: + return [] + qns = list(qualified_names) + results: list[GraphEdge] = [] + batch_size = 450 # Stay well under SQLite's default 999 limit + for i in range(0, len(qns), batch_size): + batch = qns[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + f"SELECT * FROM edges WHERE source_qualified IN ({placeholders})", + batch, + ).fetchall() + for r in rows: + edge = self._row_to_edge(r) + if edge.target_qualified in qualified_names: + results.append(edge) + return results + + def _batch_get_nodes(self, qualified_names: set[str]) -> list[GraphNode]: + """Batch-fetch nodes by qualified name, staying under SQLite variable limits.""" + if not qualified_names: + return [] + qns = list(qualified_names) + results: list[GraphNode] = [] + batch_size = 450 + for i in range(0, len(qns), batch_size): + batch = qns[i:i + batch_size] + placeholders = ",".join("?" for _ in batch) + rows = self._conn.execute( # nosec B608 + f"SELECT * FROM nodes WHERE qualified_name IN ({placeholders})", + batch, + ).fetchall() + results.extend(self._row_to_node(r) for r in rows) + return results + + def load_flow_adjacency(self) -> "FlowAdjacency": + """Load all nodes and CALLS/TESTED_BY edges into memory for fast traversal. + + Reads the entire ``nodes`` and ``edges`` tables in two streaming + queries and returns an in-memory adjacency structure suitable for + flow tracing and criticality scoring. At ~500k nodes / 3M edges + this fits in a few hundred MB and eliminates tens of millions of + single-row SQLite point queries that otherwise dominate + ``trace_flows`` / ``compute_criticality`` runtime. + """ + nodes_by_qn: dict[str, GraphNode] = {} + nodes_by_id: dict[int, GraphNode] = {} + for row in self._conn.execute("SELECT * FROM nodes"): + node = self._row_to_node(row) + nodes_by_qn[node.qualified_name] = node + nodes_by_id[node.id] = node + + calls_out: dict[str, list[str]] = {} + has_tested_by: set[str] = set() + for row in self._conn.execute( + "SELECT kind, source_qualified, target_qualified FROM edges " + "WHERE kind IN ('CALLS', 'TESTED_BY')" + ): + kind, src, tgt = row["kind"], row["source_qualified"], row["target_qualified"] + if kind == "CALLS": + calls_out.setdefault(src, []).append(tgt) + else: # TESTED_BY + has_tested_by.add(tgt) + + return FlowAdjacency( + calls_out=calls_out, + has_tested_by=has_tested_by, + nodes_by_qn=nodes_by_qn, + nodes_by_id=nodes_by_id, + ) + + # --- Internal helpers --- + + def _build_networkx_graph(self) -> nx.DiGraph: + """Build (or return cached) in-memory NetworkX directed graph from all edges.""" + with self._cache_lock: + if self._nxg_cache is not None: + return self._nxg_cache + g: nx.DiGraph = nx.DiGraph() + rows = self._conn.execute("SELECT * FROM edges").fetchall() + for r in rows: + g.add_edge(r["source_qualified"], r["target_qualified"], kind=r["kind"]) + self._nxg_cache = g + return g + + def _make_qualified(self, node: NodeInfo) -> str: + if node.kind == "File": + return node.file_path + if node.parent_name: + return f"{node.file_path}::{node.parent_name}.{node.name}" + return f"{node.file_path}::{node.name}" + + def _row_to_node(self, row: sqlite3.Row) -> GraphNode: + return GraphNode( + id=row["id"], + kind=row["kind"], + name=row["name"], + qualified_name=row["qualified_name"], + file_path=row["file_path"], + line_start=row["line_start"], + line_end=row["line_end"], + language=row["language"] or "", + parent_name=row["parent_name"], + params=row["params"], + return_type=row["return_type"], + is_test=bool(row["is_test"]), + file_hash=row["file_hash"], + extra=json.loads(row["extra"]) if row["extra"] else {}, + ) + + def _row_to_edge(self, row: sqlite3.Row) -> GraphEdge: + extra = json.loads(row["extra"]) if row["extra"] else {} + confidence = row["confidence"] if "confidence" in row.keys() else 1.0 + confidence_tier = row["confidence_tier"] if "confidence_tier" in row.keys() else "EXTRACTED" + return GraphEdge( + id=row["id"], + kind=row["kind"], + source_qualified=row["source_qualified"], + target_qualified=row["target_qualified"], + file_path=row["file_path"], + line=row["line"], + extra=extra, + confidence=confidence, + confidence_tier=confidence_tier, + ) + + +def _sanitize_name(s: str, max_len: int = 256) -> str: + """Strip ASCII control characters and truncate to prevent prompt injection. + + Node names extracted from source code could contain adversarial strings + (e.g. ``IGNORE_ALL_PREVIOUS_INSTRUCTIONS``). This function removes control + characters (0x00-0x1F except tab and newline) and enforces a length limit so + that names flowing through MCP tool responses cannot easily influence AI + agent behaviour. + """ + # Strip control chars 0x00-0x1F except \t (0x09) and \n (0x0A) + cleaned = "".join( + ch for ch in s + if ch in ("\t", "\n") or ord(ch) >= 0x20 + ) + return cleaned[:max_len] + + +def node_to_dict(n: GraphNode) -> dict: + return { + "id": n.id, "kind": n.kind, "name": _sanitize_name(n.name), + "qualified_name": _sanitize_name(n.qualified_name), "file_path": n.file_path, + "line_start": n.line_start, "line_end": n.line_end, + "language": n.language, + "parent_name": _sanitize_name(n.parent_name) if n.parent_name else n.parent_name, + "is_test": n.is_test, + } + + +def edge_to_dict(e: GraphEdge) -> dict: + return { + "id": e.id, "kind": e.kind, + "source": _sanitize_name(e.source_qualified), + "target": _sanitize_name(e.target_qualified), + "file_path": e.file_path, "line": e.line, + "confidence": e.confidence, "confidence_tier": e.confidence_tier, + } diff --git a/code_review_graph/graph_diff.py b/code_review_graph/graph_diff.py new file mode 100644 index 0000000..0c9e033 --- /dev/null +++ b/code_review_graph/graph_diff.py @@ -0,0 +1,122 @@ +"""Graph snapshot diffing -- compare graph state over time.""" + +from __future__ import annotations + +import json +import logging +from pathlib import Path +from typing import Any + +from .graph import GraphStore + +logger = logging.getLogger(__name__) + + +def take_snapshot(store: GraphStore) -> dict[str, Any]: + """Take a snapshot of the current graph state. + + Returns a dict with node and edge counts, qualified names, + and community assignments for later diffing. + """ + stats = store.get_stats() + nodes = store.get_all_nodes(exclude_files=False) + community_map = store.get_all_community_ids() + + return { + "node_count": stats.total_nodes, + "edge_count": stats.total_edges, + "nodes": { + n.qualified_name: { + "kind": n.kind, + "file": n.file_path, + "community_id": community_map.get( + n.qualified_name + ), + } + for n in nodes + }, + "edges": { + f"{e.source_qualified}->" + f"{e.target_qualified}:{e.kind}" + for e in store.get_all_edges() + }, + } + + +def save_snapshot(snapshot: dict, path: Path) -> None: + """Save a snapshot to a JSON file.""" + data = dict(snapshot) + if isinstance(data.get("edges"), set): + data["edges"] = sorted(data["edges"]) + path.write_text( + json.dumps(data, indent=2), encoding="utf-8" + ) + + +def load_snapshot(path: Path) -> dict: + """Load a snapshot from a JSON file.""" + data = json.loads(path.read_text(encoding="utf-8")) + if isinstance(data.get("edges"), list): + data["edges"] = set(data["edges"]) + return data + + +def diff_snapshots( + before: dict, after: dict, +) -> dict[str, Any]: + """Compare two graph snapshots. + + Returns: + Dict with new_nodes, removed_nodes, new_edges, + removed_edges, community_changes, and summary + statistics. + """ + before_nodes = set(before.get("nodes", {}).keys()) + after_nodes = set(after.get("nodes", {}).keys()) + before_edges = before.get("edges", set()) + after_edges = after.get("edges", set()) + + new_nodes = after_nodes - before_nodes + removed_nodes = before_nodes - after_nodes + new_edges = after_edges - before_edges + removed_edges = before_edges - after_edges + + # Community changes for nodes that exist in both + community_changes = [] + for qn in before_nodes & after_nodes: + before_cid = before["nodes"][qn].get( + "community_id" + ) + after_cid = after["nodes"][qn].get( + "community_id" + ) + if before_cid != after_cid: + community_changes.append({ + "node": qn, + "before_community": before_cid, + "after_community": after_cid, + }) + + return { + "new_nodes": [ + {"qualified_name": qn, **after["nodes"][qn]} + for qn in sorted(new_nodes) + ][:100], + "removed_nodes": sorted(removed_nodes)[:100], + "new_edges": sorted(new_edges)[:100], + "removed_edges": sorted(removed_edges)[:100], + "community_changes": community_changes[:50], + "summary": { + "nodes_added": len(new_nodes), + "nodes_removed": len(removed_nodes), + "edges_added": len(new_edges), + "edges_removed": len(removed_edges), + "community_moves": len(community_changes), + "before_total": before.get( + "node_count", 0 + ), + "after_total": after.get( + "node_count", 0 + ), + }, + } diff --git a/code_review_graph/hints.py b/code_review_graph/hints.py new file mode 100644 index 0000000..9fd8ce5 --- /dev/null +++ b/code_review_graph/hints.py @@ -0,0 +1,384 @@ +"""Context-aware hints system for MCP tool responses. + +Tracks session state (in-memory only) and generates intelligent +next-step suggestions after each tool call. Hints are appended as +``_hints`` to new tool responses so that Claude Code can propose +follow-up actions without the user having to discover them. +""" + +from __future__ import annotations + +import time +from collections import deque +from typing import Any + +# ---- intent categories and their characteristic tool names ---- + +_INTENT_TOOLS: dict[str, set[str]] = { + "reviewing": { + "detect_changes", "get_review_context", "get_affected_flows", "get_impact_radius", + }, + "debugging": { + "query_graph", "get_flow", "semantic_search_nodes", + }, + "refactoring": { + "refactor", "find_dead_code", "suggest_refactorings", + }, + "exploring": { + "list_communities", "get_architecture_overview", "list_flows", "list_graph_stats", + }, +} + +# ---- workflow adjacency: for each tool, which tools are useful next ---- + +_WORKFLOW: dict[str, list[dict[str, str]]] = { + "list_flows": [ + { + "tool": "get_flow", + "suggestion": "Drill into a specific flow for step-by-step details", + }, + { + "tool": "get_affected_flows", + "suggestion": "Check which flows are affected by recent changes", + }, + { + "tool": "get_architecture_overview", + "suggestion": "See the high-level architecture", + }, + ], + "get_flow": [ + { + "tool": "query_graph", + "suggestion": "Inspect callers/callees of a step in this flow", + }, + { + "tool": "get_affected_flows", + "suggestion": "Check if changes affect this flow", + }, + { + "tool": "list_flows", + "suggestion": "Browse other execution flows", + }, + ], + "get_affected_flows": [ + { + "tool": "detect_changes", + "suggestion": "Get risk-scored change analysis", + }, + { + "tool": "get_flow", + "suggestion": "Inspect a specific affected flow", + }, + { + "tool": "get_review_context", + "suggestion": "Build a full review context for the changes", + }, + ], + "list_communities": [ + { + "tool": "get_community", + "suggestion": "Inspect a specific community's members", + }, + { + "tool": "get_architecture_overview", + "suggestion": "See cross-community coupling and warnings", + }, + { + "tool": "list_flows", + "suggestion": "See execution flows across communities", + }, + ], + "get_community": [ + { + "tool": "query_graph", + "suggestion": "Explore callers/callees of community members", + }, + { + "tool": "list_communities", + "suggestion": "Browse other communities", + }, + { + "tool": "get_architecture_overview", + "suggestion": "See how this community fits the architecture", + }, + ], + "get_architecture_overview": [ + { + "tool": "list_communities", + "suggestion": "Drill into individual communities", + }, + { + "tool": "detect_changes", + "suggestion": "See how recent changes affect the architecture", + }, + { + "tool": "list_flows", + "suggestion": "Explore execution flows", + }, + ], + "detect_changes": [ + { + "tool": "get_review_context", + "suggestion": "Build a full review context with source snippets", + }, + { + "tool": "get_affected_flows", + "suggestion": "See which execution flows are affected", + }, + { + "tool": "get_impact_radius", + "suggestion": "Expand the blast radius analysis", + }, + { + "tool": "refactor", + "suggestion": "Look for refactoring opportunities in changed code", + }, + ], + "refactor": [ + { + "tool": "query_graph", + "suggestion": "Verify call sites before applying a rename", + }, + { + "tool": "detect_changes", + "suggestion": "Check risk of the refactored code", + }, + { + "tool": "semantic_search_nodes", + "suggestion": "Find related symbols to also rename", + }, + ], + "semantic_search_nodes": [ + { + "tool": "query_graph", + "suggestion": "Inspect callers/callees of a search result", + }, + { + "tool": "get_flow", + "suggestion": "See the execution flow through a matched node", + }, + { + "tool": "get_impact_radius", + "suggestion": "Check the blast radius from matched nodes", + }, + ], +} + +# Maximum items per hints category returned to the caller. +_MAX_PER_CATEGORY = 3 + +# Session history caps. +_MAX_TOOLS_HISTORY = 100 +_MAX_NODES_TRACKED = 1000 + + +# --------------------------------------------------------------------------- +# SessionState +# --------------------------------------------------------------------------- + + +class SessionState: + """In-memory session state for a single MCP connection.""" + + def __init__(self) -> None: + self.tools_called: deque[str] = deque(maxlen=_MAX_TOOLS_HISTORY) + self.nodes_queried: set[str] = set() + self.files_touched: set[str] = set() + self.inferred_intent: str | None = None + self.last_tool_time: float = 0.0 + + def record_tool_call(self, tool_name: str) -> None: + """Record a tool invocation (FIFO, capped at 100).""" + self.tools_called.append(tool_name) + self.last_tool_time = time.time() + + def record_nodes(self, node_ids: list[str]) -> None: + """Record queried node identifiers (capped at 1000).""" + for nid in node_ids: + if len(self.nodes_queried) >= _MAX_NODES_TRACKED: + break + self.nodes_queried.add(nid) + + def record_files(self, files: list[str]) -> None: + """Record touched file paths.""" + self.files_touched.update(files) + + +# --------------------------------------------------------------------------- +# Intent inference +# --------------------------------------------------------------------------- + + +def infer_intent(session: SessionState) -> str: + """Classify the user's likely intent from their tool-call history. + + Returns one of: ``"reviewing"``, ``"debugging"``, ``"refactoring"``, + ``"exploring"`` (default). + """ + if not session.tools_called: + return "exploring" + + # Score each intent by how many of the last N calls match its tools. + recent = list(session.tools_called)[-10:] + scores: dict[str, int] = {intent: 0 for intent in _INTENT_TOOLS} + for tool in recent: + for intent, tools in _INTENT_TOOLS.items(): + if tool in tools: + scores[intent] += 1 + + best = max(scores, key=lambda k: scores[k]) + if scores[best] == 0: + return "exploring" + return best + + +# --------------------------------------------------------------------------- +# Hints generation +# --------------------------------------------------------------------------- + + +def generate_hints( + tool_name: str, + result: dict[str, Any], + session: SessionState, +) -> dict[str, Any]: + """Build context-aware hints for a tool response. + + Returns:: + + { + "next_steps": [{"tool": ..., "suggestion": ...}, ...], + "related": [...], + "warnings": [...], + } + + At most ``_MAX_PER_CATEGORY`` items per list. Tools already called + in this session are suppressed from ``next_steps``. + """ + # Update session state. + session.record_tool_call(tool_name) + session.inferred_intent = infer_intent(session) + + next_steps = _build_next_steps(tool_name, session) + warnings = _extract_warnings(result) + # Build related BEFORE tracking, so that the current result's files + # are not yet in files_touched and can appear as suggestions. + related = _build_related(tool_name, result, session) + + # Collect files/nodes from result for session tracking. + _track_result(result, session) + + return { + "next_steps": next_steps[:_MAX_PER_CATEGORY], + "related": related[:_MAX_PER_CATEGORY], + "warnings": warnings[:_MAX_PER_CATEGORY], + } + + +# --------------------------------------------------------------------------- +# Internal helpers +# --------------------------------------------------------------------------- + + +def _track_result(result: dict[str, Any], session: SessionState) -> None: + """Extract node IDs and file paths from a tool result and record them.""" + # Files + for key in ("changed_files", "impacted_files"): + files = result.get(key) + if isinstance(files, list): + session.record_files([f for f in files if isinstance(f, str)]) + + # Nodes — look in common result shapes + node_ids: list[str] = [] + for key in ("results", "changed_nodes", "impacted_nodes"): + items = result.get(key) + if isinstance(items, list): + for item in items: + if isinstance(item, dict): + qn = item.get("qualified_name") + if qn: + node_ids.append(qn) + if node_ids: + session.record_nodes(node_ids) + + +def _build_next_steps( + tool_name: str, session: SessionState +) -> list[dict[str, str]]: + """Return next-step suggestions, filtering already-called tools.""" + called = set(session.tools_called) + candidates = _WORKFLOW.get(tool_name, []) + out: list[dict[str, str]] = [] + for c in candidates: + if c["tool"] not in called: + out.append(c) + return out + + +def _extract_warnings(result: dict[str, Any]) -> list[str]: + """Pull warning signals from a tool result.""" + warnings: list[str] = [] + + # Test gaps + test_gaps = result.get("test_gaps") + if isinstance(test_gaps, list) and test_gaps: + names = [g.get("name", g) if isinstance(g, dict) else str(g) for g in test_gaps[:5]] + warnings.append( + f"Test coverage gaps: {', '.join(names)}" + ) + + # High risk score + risk = result.get("risk_score") + if isinstance(risk, (int, float)) and risk > 0.7: + warnings.append(f"High risk score ({risk:.2f}) — review carefully") + + # Coupling warnings from architecture overview + arch_warnings = result.get("warnings") + if isinstance(arch_warnings, list): + for w in arch_warnings[:3]: + if isinstance(w, str): + warnings.append(w) + elif isinstance(w, dict) and "message" in w: + warnings.append(w["message"]) + + return warnings + + +def _build_related( + tool_name: str, + result: dict[str, Any], + session: SessionState, +) -> list[str]: + """Suggest related node/file identifiers from the result.""" + related: list[str] = [] + seen: set[str] = set() + + # Suggest impacted files the user hasn't touched yet + impacted = result.get("impacted_files") + if isinstance(impacted, list): + for f in impacted: + if isinstance(f, str) and f not in session.files_touched and f not in seen: + related.append(f) + seen.add(f) + if len(related) >= _MAX_PER_CATEGORY: + break + + return related + + +# --------------------------------------------------------------------------- +# Module-level session singleton +# --------------------------------------------------------------------------- + +_session = SessionState() + + +def get_session() -> SessionState: + """Return the global in-memory session state.""" + return _session + + +def reset_session() -> None: + """Reset the global session (useful for testing).""" + global _session + _session = SessionState() diff --git a/code_review_graph/incremental.py b/code_review_graph/incremental.py new file mode 100644 index 0000000..81dc302 --- /dev/null +++ b/code_review_graph/incremental.py @@ -0,0 +1,1234 @@ +"""Incremental graph update logic. + +Detects changed files via git diff, re-parses only changed + impacted files, +and updates the graph accordingly. Also supports CLI invocation for hooks. +""" + +from __future__ import annotations + +import concurrent.futures +import fnmatch +import hashlib +import logging +import os +import re +import subprocess +import sys +import threading +import time +from pathlib import Path, PurePosixPath +from typing import Callable, Optional + +from .graph import GraphStore +from .parser import CodeParser + +_MAX_PARSE_WORKERS = int(os.environ.get("CRG_PARSE_WORKERS", str(min(os.cpu_count() or 4, 8)))) + + +def _select_executor_kind() -> str: + """Return 'process' or 'thread' for parallel parsing. + + Defaults to ``process`` (the original behavior, fastest on Linux/macOS). + Auto-switches to ``thread`` when running on Windows with stdin not + attached to a TTY — that combination indicates an MCP/stdio host, where + ``ProcessPoolExecutor`` workers inherit the parent's pipe handles and + leak as zombies after the pool closes (issues #46, #136). + + Override explicitly with ``CRG_PARSE_EXECUTOR={process,thread}``. + + Tree-sitter parsing in the worker releases the GIL during native + parsing, so the speedup loss for falling back to threads is small + (typically <30% on the full-build path) and the trade is worth it + to avoid the deadlock + zombie process accumulation. + """ + explicit = os.environ.get("CRG_PARSE_EXECUTOR", "").strip().lower() + if explicit in ("process", "thread"): + return explicit + if sys.platform == "win32" and not sys.stdin.isatty(): + return "thread" + return "process" + + +def _make_executor(max_workers: int): + """Construct the parallel-parse executor selected by [_select_executor_kind].""" + if _select_executor_kind() == "thread": + return concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) + return concurrent.futures.ProcessPoolExecutor(max_workers=max_workers) + +logger = logging.getLogger(__name__) + + +def _run_rescript_resolver(store: GraphStore) -> Optional[dict]: + """Run the ReScript cross-module resolver, swallowing any failure so + build never fails because of it. Returns stats or None on error. + """ + try: + from .rescript_resolver import resolve_rescript_cross_module + return resolve_rescript_cross_module(store) + except Exception as exc: # noqa: BLE001 - best-effort post-pass + logger.warning("ReScript cross-module resolver failed: %s", exc) + return None + + +def _run_spring_resolver(store: GraphStore) -> Optional[dict]: + """Run the Spring DI call resolver, swallowing any failure so + build never fails because of it. Returns stats or None on error. + """ + try: + from .spring_resolver import resolve_spring_di_calls + return resolve_spring_di_calls(store) + except Exception as exc: # noqa: BLE001 - best-effort post-pass + logger.warning("Spring DI resolver failed: %s", exc) + return None + + +def _run_temporal_resolver(store: GraphStore) -> Optional[dict]: + """Run the Temporal workflow/activity call resolver, swallowing any failure so + build never fails because of it. Returns stats or None on error. + """ + try: + from .temporal_resolver import resolve_temporal_calls + return resolve_temporal_calls(store) + except Exception as exc: # noqa: BLE001 - best-effort post-pass + logger.warning("Temporal resolver failed: %s", exc) + return None + +# Default ignore patterns (in addition to .gitignore). +# +# `/**` patterns are matched at any depth by _should_ignore, so +# `node_modules/**` also excludes `packages/app/node_modules/react/index.js` +# inside monorepos. See: #91 +DEFAULT_IGNORE_PATTERNS = [ + ".code-review-graph/**", + "node_modules/**", + ".git/**", + ".svn/**", + "__pycache__/**", + "*.pyc", + ".venv/**", + "venv/**", + "dist/**", + "build/**", + ".next/**", + "target/**", + # PHP / Laravel / Composer + "vendor/**", + "bootstrap/cache/**", + "public/build/**", + # Ruby / Bundler + ".bundle/**", + # Java / Kotlin / Gradle + ".gradle/**", + "*.jar", + # Dart / Flutter + ".dart_tool/**", + ".pub-cache/**", + # General + "coverage/**", + ".cache/**", + "*.min.js", + "*.min.css", + "*.map", + "*.lock", + "package-lock.json", + "yarn.lock", + "*.db", + "*.sqlite", + "*.db-journal", + "*.db-wal", +] + + +def find_svn_root(start: Path | None = None) -> Optional[Path]: + """Walk up from start to find the SVN working copy root. + + For SVN 1.7+, there is a single ``.svn`` at the WC root. + For older SVN, every directory has ``.svn`` — we return the topmost one + found so that the WC root is correctly identified. + """ + current = start or Path.cwd() + candidate: Optional[Path] = None + while current != current.parent: + if (current / ".svn").exists(): + candidate = current + current = current.parent + if (current / ".svn").exists(): + candidate = current + return candidate + + +def find_repo_root( + start: Path | None = None, + stop_at: Path | None = None, +) -> Optional[Path]: + """Walk up from ``start`` to find the nearest ``.git`` directory or SVN working copy root. + + Args: + start: Starting directory. Defaults to ``Path.cwd()``. + stop_at: Optional boundary — if provided, the walk examines + ``stop_at`` for a ``.git`` directory and then stops without + crossing above it. Useful for tests that create a synthetic + repo under ``tmp_path`` (so the walk does not accidentally + climb into a developer's home-directory dotfiles repo) and + for any production caller that wants to bound the ancestor + walk — e.g. multi-repo orchestrators, CI containers with + bind-mounted volumes, embedded sandboxes. See #241. + + Returns: + The first ancestor containing ``.git`` or an SVN working copy, + or ``None`` if no ancestor up to and including ``stop_at`` (when + set) or the filesystem root (when ``stop_at is None``) contains one. + """ + current = start or Path.cwd() + while current != current.parent: + if (current / ".git").exists(): + return current + if stop_at is not None and current == stop_at: + return None + current = current.parent + if (current / ".git").exists(): + return current + # No Git root found — try SVN + return find_svn_root(start) + + +def detect_vcs(root: Path) -> str: + """Return ``'git'``, ``'svn'``, or ``'none'`` based on VCS markers at *root*.""" + if (root / ".git").exists(): + return "git" + if (root / ".svn").exists(): + return "svn" + return "none" + + +def find_project_root( + start: Path | None = None, + stop_at: Path | None = None, +) -> Path: + """Find the project root. + + Resolution order (highest precedence first): + + 1. ``CRG_REPO_ROOT`` environment variable — explicit override for + anyone scripting the CLI from outside the repo (CI jobs, daemons, + multi-repo orchestrators). See: #155 + 2. Git repository root via :func:`find_repo_root` from ``start``, + honoring ``stop_at`` if provided. + 3. ``start`` itself (or cwd if no start given). + + ``stop_at`` is forwarded to :func:`find_repo_root` so callers that + want to bound the ancestor walk (typically tests; see #241) can do so + without having to call ``find_repo_root`` directly. + """ + env_override = os.environ.get("CRG_REPO_ROOT", "").strip() + if env_override: + p = Path(env_override).expanduser().resolve() + if p.exists(): + return p + root = find_repo_root(start, stop_at=stop_at) + if root: + return root + return start or Path.cwd() + + +def _write_data_dir_gitignore(data_dir: Path) -> None: + """Write .gitignore file in data directory if it doesn't exist. + + The gitignore contains a single '*' to prevent accidental commits. + """ + inner_gitignore = data_dir / ".gitignore" + if not inner_gitignore.exists(): + try: + # `encoding="utf-8"` is REQUIRED — the em-dash in the header is + # U+2014 which falls outside cp1252. On Windows, calling + # write_text without an encoding silently uses the system default + # codepage, producing a file that subsequently fails to decode as + # UTF-8 (see issue #239). + inner_gitignore.write_text( + "# Auto-generated by code-review-graph — do not commit database files.\n" + "# The graph.db contains absolute paths and code structure metadata.\n" + "*\n", + encoding="utf-8", + ) + except OSError: + # Data dir might be read-only (rare); that's OK, it's a best-effort guard. + pass + + +def get_data_dir(repo_root: Path) -> Path: + """Return the directory where this project's graph data lives. + + Resolution priority: + 1. Registry entry for this repo (set via --data-dir) + 2. CRG_DATA_DIR environment variable (global override) + 3. Default: /.code-review-graph/ + + By default, ``/.code-review-graph``. If the + ``CRG_DATA_DIR`` environment variable is set, it is used verbatim + instead — letting you keep graphs outside the working tree (useful + for ephemeral workspaces, Docker volumes, or shared caches). See: #155 + + The directory is created if it does not already exist; an inner + ``.gitignore`` (with ``*``) is written so any accidentally-nested + files never get committed. Both are idempotent. + """ + # Check registry first + try: + from .registry import Registry + registry_data_dir = Registry().get_data_dir_for_repo(str(repo_root)) + if registry_data_dir: + data_dir = Path(registry_data_dir).resolve() + data_dir.mkdir(parents=True, exist_ok=True) + _write_data_dir_gitignore(data_dir) + return data_dir + except Exception as exc: + # If registry lookup fails, log and fall through to other methods + logger.debug("Registry lookup failed for %s: %s", repo_root, exc) + + # Check environment variable + env_override = os.environ.get("CRG_DATA_DIR", "").strip() + if env_override: + data_dir = Path(env_override).expanduser().resolve() + else: + data_dir = repo_root / ".code-review-graph" + + data_dir.mkdir(parents=True, exist_ok=True) + _write_data_dir_gitignore(data_dir) + + return data_dir + + +def get_db_path(repo_root: Path) -> Path: + """Determine the database path for a repository. + + Respects ``CRG_DATA_DIR`` (see :func:`get_data_dir`). Migrates a + legacy top-level ``.code-review-graph.db`` file into the new + directory when it exists (WAL/SHM side-files are discarded). + """ + crg_dir = get_data_dir(repo_root) + new_db = crg_dir / "graph.db" + + # Migrate legacy database if present (only meaningful when the + # legacy file sits at the repo root — if CRG_DATA_DIR is set we + # skip the migration because there's no relationship between the + # legacy location and the new one). + legacy_db = repo_root / ".code-review-graph.db" + if legacy_db.exists() and not new_db.exists(): + legacy_db.rename(new_db) + # Discard stale WAL/SHM side-files from the old location + for suffix in ("-wal", "-shm", "-journal"): + side = repo_root / f".code-review-graph.db{suffix}" + if side.exists(): + side.unlink() + + return new_db + + +def ensure_repo_gitignore_excludes_crg(repo_root: Path) -> str: + """Ensure repo-level .gitignore excludes ``.code-review-graph/``. + + Returns one of: + - ``created``: .gitignore was created with the entry + - ``updated``: entry was appended to existing .gitignore + - ``already-present``: no changes were needed + """ + gitignore_path = repo_root / ".gitignore" + existing = gitignore_path.read_text(encoding="utf-8") if gitignore_path.exists() else "" + + for raw_line in existing.splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line == ".code-review-graph" or line.startswith(".code-review-graph/"): + return "already-present" + + block = "# Added by code-review-graph\n.code-review-graph/\n" + prefix = "\n" if existing and not existing.endswith("\n") else "" + gitignore_path.write_text(existing + prefix + block, encoding="utf-8") + + if existing: + return "updated" + return "created" + + +def _load_ignore_patterns(repo_root: Path) -> list[str]: + """Load ignore patterns from .code-review-graphignore file.""" + patterns = list(DEFAULT_IGNORE_PATTERNS) + ignore_file = repo_root / ".code-review-graphignore" + if ignore_file.exists(): + for line in ignore_file.read_text(encoding="utf-8", errors="replace").splitlines(): + line = line.strip() + if line and not line.startswith("#"): + patterns.append(line) + return patterns + + +def _should_ignore(path: str, patterns: list[str]) -> bool: + """Check if a path matches any ignore pattern. + + Handles nested occurrences of ``/**`` patterns: for example, + ``node_modules/**`` also matches ``packages/app/node_modules/foo.js`` + inside monorepos. ``fnmatch`` alone treats ``*`` as not crossing ``/`` + and only matches the prefix, so we additionally test each path segment + against the bare prefix of ``/**`` patterns. See: #91 + """ + # Direct fnmatch first (cheap) + if any(fnmatch.fnmatch(path, p) for p in patterns): + return True + # Then: treat simple single-segment "dir/**" patterns as + # "this directory at any depth". + parts = PurePosixPath(path).parts + for p in patterns: + if not p.endswith("/**"): + continue + prefix = p[:-3] + # Only single-segment dir patterns (no "/" inside the prefix) + # qualify for nested matching. + if "/" in prefix or not prefix: + continue + if prefix in parts: + return True + return False + + +def _is_binary(path: Path) -> bool: + """Quick heuristic: check if file appears to be binary.""" + try: + chunk = path.read_bytes()[:8192] + return b"\x00" in chunk + except (OSError, PermissionError): + return True + + +_GIT_TIMEOUT = int(os.environ.get("CRG_GIT_TIMEOUT", "30")) # seconds, configurable + +# When True, `git ls-files --recurse-submodules` is used so that files +# inside git submodules are included in the graph. Opt-in via env var; +# can also be overridden per-call through function parameters. +_RECURSE_SUBMODULES = os.environ.get("CRG_RECURSE_SUBMODULES", "").lower() in ("1", "true", "yes") + + +def _git_branch_info(repo_root: Path) -> tuple[str, str]: + """Return (branch_name, head_sha) for the current repo state.""" + branch = "" + sha = "" + try: + result = subprocess.run( + ["git", "rev-parse", "--abbrev-ref", "HEAD"], + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + if result.returncode == 0: + branch = result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + try: + result = subprocess.run( + ["git", "rev-parse", "HEAD"], + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + if result.returncode == 0: + sha = result.stdout.strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return branch, sha + + +def _svn_revision_info(repo_root: Path) -> tuple[str, str]: + """Return (branch_path, revision_str) for the current SVN working copy.""" + branch = "" + rev = "" + try: + result = subprocess.run( + ["svn", "info", "--non-interactive"], + capture_output=True, text=True, encoding="utf-8", errors="replace", + cwd=str(repo_root), timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + if result.returncode == 0: + for line in result.stdout.splitlines(): + if line.startswith("URL: "): + url = line[5:].strip() + # Extract trunk/branches/tags segment from SVN URL + for marker in ("/branches/", "/tags/", "/trunk"): + if marker in url: + idx = url.index(marker) + branch = url[idx:].lstrip("/") + break + if not branch and url: + branch = url.rstrip("/").split("/")[-1] + elif line.startswith("Revision: "): + rev = line[10:].strip() + except (subprocess.TimeoutExpired, FileNotFoundError): + pass + return branch, rev + + +_SAFE_GIT_REF = re.compile(r"^[A-Za-z0-9_.~^/@{}\-]+$") +_SAFE_SVN_REV = re.compile(r"^r?\d+(:r?\d+|:HEAD|:BASE|:COMMITTED)?$", re.IGNORECASE) + + +def _store_vcs_metadata(repo_root: Path, store: "GraphStore") -> None: + """Persist VCS branch/revision info into the graph metadata table.""" + vcs = detect_vcs(repo_root) + if vcs == "git": + branch, sha = _git_branch_info(repo_root) + if branch: + store.set_metadata("git_branch", branch) + if sha: + store.set_metadata("git_head_sha", sha) + elif vcs == "svn": + branch, rev = _svn_revision_info(repo_root) + if branch: + store.set_metadata("svn_branch", branch) + if rev: + store.set_metadata("svn_revision", rev) + + +def get_changed_files(repo_root: Path, base: str = "HEAD~1") -> list[str]: + """Get list of changed files via git diff or svn status. + + For SVN working copies the *base* parameter is ignored; modified/added/ + deleted files are detected from ``svn status``. Pass an SVN revision + range (e.g. ``"r100:HEAD"``) as *base* to compare against a specific + revision instead. + """ + if detect_vcs(repo_root) == "svn": + return _get_svn_changed_files(repo_root, base if _SAFE_SVN_REV.match(base) else None) + # Git path + if not _SAFE_GIT_REF.match(base): + logger.warning("Invalid git ref rejected: %s", base) + return [] + try: + result = subprocess.run( + ["git", "diff", "--name-only", base, "--"], + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + if result.returncode != 0: + # Fallback: try diff against empty tree (initial commit) + result = subprocess.run( + ["git", "diff", "--name-only", "--cached"], + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + files = [f.strip() for f in result.stdout.splitlines() if f.strip()] + return files + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + +def _get_svn_changed_files(repo_root: Path, rev_range: str | None = None) -> list[str]: + """Return changed files in an SVN working copy. + + When *rev_range* is given (e.g. ``"r100:HEAD"``), ``svn diff --summarize`` + is used to list files changed between those revisions. Otherwise + ``svn status`` reports working-copy modifications. + """ + try: + if rev_range: + result = subprocess.run( + ["svn", "diff", "--summarize", "--non-interactive", "-r", rev_range], + capture_output=True, text=True, encoding="utf-8", errors="replace", + cwd=str(repo_root), timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + if result.returncode != 0: + logger.warning("svn diff --summarize failed (rc=%d): %s", + result.returncode, result.stderr[:200]) + return [] + files = [] + for line in result.stdout.splitlines(): + # Format: "M path/to/file" (first char is status) + if len(line) >= 2 and line[0] in ("M", "A", "D"): + files.append(line[1:].strip()) + return files + else: + result = subprocess.run( + ["svn", "status", "--non-interactive"], + capture_output=True, text=True, encoding="utf-8", errors="replace", + cwd=str(repo_root), timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + files = [] + for line in result.stdout.splitlines(): + if len(line) < 2: + continue + status_char = line[0] + # M=modified, A=added, D=deleted, R=replaced, C=conflicted + if status_char in ("M", "A", "D", "R", "C"): + # SVN status: 8 fixed-width columns then the path + path = line[8:].strip() if len(line) > 8 else line[1:].strip() + files.append(path) + return files + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + +def get_staged_and_unstaged(repo_root: Path) -> list[str]: + """Get all modified files (staged + unstaged + untracked).""" + if detect_vcs(repo_root) == "svn": + return _get_svn_changed_files(repo_root) + try: + result = subprocess.run( + ["git", "status", "--porcelain"], + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + files = [] + for line in result.stdout.splitlines(): + if len(line) > 3: + entry = line[3:].strip() + # Handle renamed files: "R old -> new" + if " -> " in entry: + entry = entry.split(" -> ", 1)[1] + files.append(entry) + return files + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + +def get_all_tracked_files( + repo_root: Path, + recurse_submodules: bool | None = None, +) -> list[str]: + """Get all files tracked by git or svn. + + Args: + repo_root: Repository root directory. + recurse_submodules: If True, pass ``--recurse-submodules`` to + ``git ls-files`` so that files inside git submodules are + included. When *None* (default), falls back to the + ``CRG_RECURSE_SUBMODULES`` environment variable. + (Ignored for SVN working copies.) + """ + if detect_vcs(repo_root) == "svn": + return _get_svn_all_tracked_files(repo_root) + + if recurse_submodules is None: + recurse_submodules = _RECURSE_SUBMODULES + + cmd = ["git", "ls-files"] + if recurse_submodules: + cmd.append("--recurse-submodules") + + try: + result = subprocess.run( + cmd, + capture_output=True, + text=True, encoding='utf-8', cwd=str(repo_root), + timeout=_GIT_TIMEOUT, + stdin=subprocess.DEVNULL, + ) + return [f.strip() for f in result.stdout.splitlines() if f.strip()] + except (FileNotFoundError, subprocess.TimeoutExpired): + return [] + + +def _get_svn_all_tracked_files(repo_root: Path) -> list[str]: + """Return SVN-versioned files by walking the working copy. + + Uses ``svn list -R`` to get the server-side file list, falling back to + a filesystem walk (which is also the fallback in :func:`collect_all_files`). + """ + try: + result = subprocess.run( + ["svn", "list", "--recursive", "--non-interactive"], + capture_output=True, text=True, encoding="utf-8", errors="replace", + cwd=str(repo_root), timeout=60, # svn list queries the server + stdin=subprocess.DEVNULL, + ) + if result.returncode == 0: + # svn list returns paths relative to the WC URL; directories end with "/" + files = [ + f.strip() + for f in result.stdout.splitlines() + if f.strip() and not f.strip().endswith("/") + ] + if files: + return files + except (FileNotFoundError, subprocess.TimeoutExpired): + pass + # Fallback: let collect_all_files do a filesystem walk + return [] + + +def collect_all_files( + repo_root: Path, + recurse_submodules: bool | None = None, +) -> list[str]: + """Collect all parseable files in the repo, respecting ignore patterns. + + Args: + repo_root: Repository root directory. + recurse_submodules: If True, include files from git submodules. + When *None*, falls back to ``CRG_RECURSE_SUBMODULES`` env var. + """ + ignore_patterns = _load_ignore_patterns(repo_root) + parser = CodeParser(repo_root) + files = [] + + # Prefer git ls-files for tracked files + tracked = get_all_tracked_files(repo_root, recurse_submodules) + if tracked: + candidates = tracked + else: + # Fallback: walk directory + candidates = [str(p.relative_to(repo_root)) for p in repo_root.rglob("*") if p.is_file()] + + for rel_path in candidates: + if _should_ignore(rel_path, ignore_patterns): + continue + # Skip paths that would exceed OS filename limits (macOS: 255 bytes + # per component, ~1024 total; Windows: 260 total). + try: + full_path = repo_root / rel_path + except (OSError, ValueError): + logger.debug("Skipping path that cannot be constructed: %s", rel_path) + continue + if len(str(full_path)) > 1000 or any(len(p.encode()) > 255 for p in full_path.parts): + logger.debug("Skipping overlong path: %s", rel_path[:120]) + continue + if not full_path.is_file(): + continue + if full_path.is_symlink(): + continue + if parser.detect_language(full_path) is None: + continue + if _is_binary(full_path): + continue + files.append(rel_path) + + return files + + +_MAX_DEPENDENT_HOPS = int(os.environ.get("CRG_DEPENDENT_HOPS", "2")) +_MAX_DEPENDENT_FILES = 500 + + +def _single_hop_dependents(store: GraphStore, file_path: str) -> set[str]: + """Find files that directly depend on *file_path* (single hop).""" + dependents: set[str] = set() + edges = store.get_edges_by_target(file_path) + for e in edges: + if e.kind == "IMPORTS_FROM": + dependents.add(e.file_path) + + nodes = store.get_nodes_by_file(file_path) + for node in nodes: + for e in store.get_edges_by_target(node.qualified_name): + if e.kind in ("CALLS", "IMPORTS_FROM", "INHERITS", "IMPLEMENTS"): + dependents.add(e.file_path) + + dependents.discard(file_path) + return dependents + + +class DependentList(list): + """A ``list[str]`` with a ``.truncated`` flag. + + When :func:`find_dependents` hits ``_MAX_DEPENDENT_FILES`` it truncates + the result and sets ``truncated = True`` so callers can distinguish a + complete expansion from a capped one. See issue #261. + + This is a transparent ``list`` subclass — existing callers that iterate, + ``len()``, or slice continue to work unchanged; only callers that + specifically check ``.truncated`` benefit from the signal. + """ + + truncated: bool + + def __init__(self, items: list, *, truncated: bool = False) -> None: + super().__init__(items) + self.truncated = truncated + + +def find_dependents( + store: GraphStore, + file_path: str, + max_hops: int = _MAX_DEPENDENT_HOPS, +) -> DependentList: + """Find files that import from or depend on the given file. + + Performs up to *max_hops* iterations of expansion (default 2). + Stops early if the total exceeds 500 files. + + Returns a :class:`DependentList` — a regular ``list[str]`` that also + carries a ``.truncated`` flag. When ``truncated is True`` the + returned list is capped at ``_MAX_DEPENDENT_FILES`` and the full + set of dependents was not explored. See issue #261. + """ + all_dependents: set[str] = set() + visited: set[str] = {file_path} + frontier: set[str] = {file_path} + for _hop in range(max_hops): + next_frontier: set[str] = set() + for fp in frontier: + deps = _single_hop_dependents(store, fp) + new_deps = deps - visited + all_dependents.update(new_deps) + next_frontier.update(new_deps) + visited.update(next_frontier) + frontier = next_frontier + if not frontier: + break + if len(all_dependents) > _MAX_DEPENDENT_FILES: + logger.warning( + "Dependent expansion capped at %d files for %s", + len(all_dependents), + file_path, + ) + return DependentList( + list(all_dependents)[:_MAX_DEPENDENT_FILES], + truncated=True, + ) + return DependentList(list(all_dependents)) + + +def _parse_single_file( + args: tuple[str, str], +) -> tuple[str, list, list, str | None, str]: + """Parse one file in a worker process. + + Returns ``(rel_path, nodes, edges, error_or_none, file_hash)``. + Must be a module-level function so ``ProcessPoolExecutor`` can + serialise it across processes. + """ + rel_path, repo_root_str = args + abs_path = Path(repo_root_str) / rel_path + try: + raw = abs_path.read_bytes() + fhash = hashlib.sha256(raw).hexdigest() + parser = CodeParser(Path(repo_root_str)) + nodes, edges = parser.parse_bytes(abs_path, raw) + return (rel_path, nodes, edges, None, fhash) + except Exception as e: + return (rel_path, [], [], str(e), "") + + +def full_build( + repo_root: Path, + store: GraphStore, + recurse_submodules: bool | None = None, +) -> dict: + """Full rebuild of the entire graph. + + Args: + repo_root: Repository root directory. + store: Graph database store. + recurse_submodules: If True, include files from git submodules. + When *None*, falls back to ``CRG_RECURSE_SUBMODULES`` env var. + """ + parser = CodeParser(repo_root) + files = collect_all_files(repo_root, recurse_submodules) + + # Purge stale data from files no longer on disk + existing_files = set(store.get_all_files()) + current_abs = {str(repo_root / f) for f in files} + stale_files = existing_files - current_abs + for stale in stale_files: + store.remove_file_data(stale) + # Ensure deletions are persisted before store_file_nodes_edges() + # starts its own explicit transaction via BEGIN IMMEDIATE. + if stale_files: + store.commit() + + total_nodes = 0 + total_edges = 0 + errors = [] + file_count = len(files) + + use_serial = os.environ.get("CRG_SERIAL_PARSE", "") == "1" + + if use_serial or file_count < 8: + # Serial fallback (for debugging or tiny repos) + for i, rel_path in enumerate(files, 1): + full_path = repo_root / rel_path + try: + source = full_path.read_bytes() + fhash = hashlib.sha256(source).hexdigest() + nodes, edges = parser.parse_bytes(full_path, source) + store.store_file_nodes_edges(str(full_path), nodes, edges, fhash) + total_nodes += len(nodes) + total_edges += len(edges) + except (OSError, PermissionError) as e: + errors.append({"file": rel_path, "error": str(e)}) + except Exception as e: + logger.warning("Error parsing %s: %s", rel_path, e) + errors.append({"file": rel_path, "error": str(e)}) + if i % 50 == 0 or i == file_count: + logger.info("Progress: %d/%d files parsed", i, file_count) + else: + # Parallel parsing — store calls remain serial (SQLite single-writer). + # Executor kind auto-selected: process on Linux/macOS/Windows-TTY, + # thread on Windows-MCP-stdio to avoid pipe-handle inheritance + # deadlock (issues #46, #136). Override via CRG_PARSE_EXECUTOR env. + args_list = [(rel_path, str(repo_root)) for rel_path in files] + with _make_executor(_MAX_PARSE_WORKERS) as executor: + for i, (rel_path, nodes, edges, error, fhash) in enumerate( + executor.map(_parse_single_file, args_list, chunksize=20), + 1, + ): + if error: + logger.warning("Error parsing %s: %s", rel_path, error) + errors.append({"file": rel_path, "error": error}) + continue + full_path = repo_root / rel_path + store.store_file_nodes_edges( + str(full_path), + nodes, + edges, + fhash, + ) + total_nodes += len(nodes) + total_edges += len(edges) + if i % 200 == 0 or i == file_count: + logger.info("Progress: %d/%d files parsed", i, file_count) + + store.set_metadata("last_updated", time.strftime("%Y-%m-%dT%H:%M:%S")) + store.set_metadata("last_build_type", "full") + _store_vcs_metadata(repo_root, store) + store.commit() + + rescript_stats = _run_rescript_resolver(store) + spring_stats = _run_spring_resolver(store) + temporal_stats = _run_temporal_resolver(store) + + return { + "files_parsed": len(files), + "total_nodes": total_nodes, + "total_edges": total_edges, + "errors": errors, + "rescript_resolution": rescript_stats, + "spring_resolution": spring_stats, + "temporal_resolution": temporal_stats, + } + + +def incremental_update( + repo_root: Path, + store: GraphStore, + base: str = "HEAD~1", + changed_files: list[str] | None = None, +) -> dict: + """Incremental update: re-parse changed + dependent files only.""" + parser = CodeParser(repo_root) + ignore_patterns = _load_ignore_patterns(repo_root) + + # Determine changed files + if changed_files is None: + changed_files = get_changed_files(repo_root, base) + + if not changed_files: + return { + "files_updated": 0, + "total_nodes": 0, + "total_edges": 0, + "changed_files": [], + "dependent_files": [], + } + + # Find dependent files (files that import from changed files) + dependent_files: set[str] = set() + for rel_path in changed_files: + full_path = str(repo_root / rel_path) + deps = find_dependents(store, full_path) + for d in deps: + # Convert back to relative path if needed + try: + dependent_files.add(str(Path(d).relative_to(repo_root))) + except ValueError: + dependent_files.add(d) + + # Combine changed + dependent + all_files = set(changed_files) | dependent_files + + total_nodes = 0 + total_edges = 0 + errors = [] + + # Separate deleted/unparseable files from files that need re-parsing + to_parse: list[str] = [] + removed_any = False + for rel_path in all_files: + if _should_ignore(rel_path, ignore_patterns): + continue + abs_path = repo_root / rel_path + if not abs_path.is_file(): + store.remove_file_data(str(abs_path)) + removed_any = True + continue + if parser.detect_language(abs_path) is None: + continue + # Quick hash check to skip unchanged files + try: + raw = abs_path.read_bytes() + fhash = hashlib.sha256(raw).hexdigest() + existing_nodes = store.get_nodes_by_file(str(abs_path)) + if existing_nodes and existing_nodes[0].file_hash == fhash: + continue + except (OSError, PermissionError): + pass + to_parse.append(rel_path) + + # Persist deletions before store_file_nodes_edges() opens its own + # explicit transaction — avoids nested transaction errors. + if removed_any: + store.commit() + + use_serial = os.environ.get("CRG_SERIAL_PARSE", "") == "1" + + if use_serial or len(to_parse) < 8: + for rel_path in to_parse: + abs_path = repo_root / rel_path + try: + source = abs_path.read_bytes() + fhash = hashlib.sha256(source).hexdigest() + nodes, edges = parser.parse_bytes(abs_path, source) + store.store_file_nodes_edges(str(abs_path), nodes, edges, fhash) + total_nodes += len(nodes) + total_edges += len(edges) + except (OSError, PermissionError) as e: + errors.append({"file": rel_path, "error": str(e)}) + except Exception as e: + logger.warning("Error parsing %s: %s", rel_path, e) + errors.append({"file": rel_path, "error": str(e)}) + else: + # See full-build comment above for executor kind rationale. + args_list = [(rel_path, str(repo_root)) for rel_path in to_parse] + with _make_executor(_MAX_PARSE_WORKERS) as executor: + for rel_path, nodes, edges, error, fhash in executor.map( + _parse_single_file, + args_list, + chunksize=20, + ): + if error: + logger.warning("Error parsing %s: %s", rel_path, error) + errors.append({"file": rel_path, "error": error}) + continue + store.store_file_nodes_edges( + str(repo_root / rel_path), + nodes, + edges, + fhash, + ) + total_nodes += len(nodes) + total_edges += len(edges) + + store.set_metadata("last_updated", time.strftime("%Y-%m-%dT%H:%M:%S")) + store.set_metadata("last_build_type", "incremental") + _store_vcs_metadata(repo_root, store) + store.commit() + + # Only re-run language-specific resolvers when the relevant files changed. + rescript_changed = any( + rp.endswith((".res", ".resi")) for rp in all_files + ) + rescript_stats = ( + _run_rescript_resolver(store) if rescript_changed else None + ) + + spring_changed = any(rp.endswith(".java") for rp in all_files) + spring_stats = _run_spring_resolver(store) if spring_changed else None + temporal_stats = _run_temporal_resolver(store) if spring_changed else None + + return { + "files_updated": len(all_files), + "total_nodes": total_nodes, + "total_edges": total_edges, + "changed_files": list(changed_files), + "dependent_files": list(dependent_files), + "errors": errors, + "rescript_resolution": rescript_stats, + "spring_resolution": spring_stats, + "temporal_resolution": temporal_stats, + } + + +# --------------------------------------------------------------------------- +# Watch mode +# --------------------------------------------------------------------------- + + +_DEBOUNCE_SECONDS = 0.3 + + +def watch( + repo_root: Path, + store: GraphStore, + on_files_updated: Optional[Callable] = None, +) -> None: + """Watch for file changes and auto-update the graph. + + Uses a 300ms debounce to batch rapid-fire saves into a single update. + + Args: + repo_root: Repository root to watch. + store: Graph database to update. + on_files_updated: Optional callback invoked after each debounced + batch of file updates completes. Receives the store as its + only argument. Used by the CLI to run post-processing + (FTS, flows, communities) after watch updates. + """ + import threading + + from watchdog.events import FileSystemEventHandler + from watchdog.observers import Observer + + parser = CodeParser(repo_root) + ignore_patterns = _load_ignore_patterns(repo_root) + + class GraphUpdateHandler(FileSystemEventHandler): + def __init__(self): + self._pending: set[str] = set() + self._lock = threading.Lock() + self._timer: threading.Timer | None = None + + def _should_handle(self, path: str) -> bool: + if Path(path).is_symlink(): + return False + try: + rel = str(Path(path).relative_to(repo_root)) + except ValueError: + return False + if _should_ignore(rel, ignore_patterns): + return False + if parser.detect_language(Path(path)) is None: + return False + return True + + def on_modified(self, event): + if event.is_directory: + return + if self._should_handle(event.src_path): + self._schedule(event.src_path) + + def on_created(self, event): + if event.is_directory: + return + if self._should_handle(event.src_path): + self._schedule(event.src_path) + + def on_deleted(self, event): + if event.is_directory: + return + # Only handle files we would normally track + try: + rel = str(Path(event.src_path).relative_to(repo_root)) + except ValueError: + return + if _should_ignore(rel, ignore_patterns): + return + try: + store.remove_file_data(event.src_path) + store.commit() + logger.info("Removed: %s", rel) + except Exception as e: + logger.error("Error removing %s: %s", rel, e) + + def _schedule(self, abs_path: str): + """Add file to pending set and reset the debounce timer.""" + with self._lock: + self._pending.add(abs_path) + if self._timer is not None: + self._timer.cancel() + self._timer = threading.Timer(_DEBOUNCE_SECONDS, self._flush) + self._timer.start() + + def _flush(self): + """Process all pending files after the debounce window.""" + with self._lock: + paths = list(self._pending) + self._pending.clear() + self._timer = None + + updated = 0 + for abs_path in paths: + if self._update_file(abs_path): + updated += 1 + + if updated > 0 and on_files_updated is not None: + try: + on_files_updated(store) + except Exception as e: + logger.error("Post-update callback failed: %s", e) + + def _update_file(self, abs_path: str) -> bool: + path = Path(abs_path) + if not path.is_file(): + return False + if path.is_symlink(): + return False + if _is_binary(path): + return False + try: + source = path.read_bytes() + fhash = hashlib.sha256(source).hexdigest() + nodes, edges = parser.parse_bytes(path, source) + store.store_file_nodes_edges(abs_path, nodes, edges, fhash) + store.set_metadata("last_updated", time.strftime("%Y-%m-%dT%H:%M:%S")) + store.commit() + rel = str(path.relative_to(repo_root)) + logger.info( + "Updated: %s (%d nodes, %d edges)", + rel, + len(nodes), + len(edges), + ) + return True + except Exception as e: + logger.error("Error updating %s: %s", abs_path, e) + return False + + handler = GraphUpdateHandler() + observer = Observer() + observer.schedule(handler, str(repo_root), recursive=True) + observer.start() + + logger.info("Watching %s for changes... (Ctrl+C to stop)", repo_root) + try: + import time as _time + + while True: + _time.sleep(1) + except KeyboardInterrupt: + observer.stop() + observer.join() + logger.info("Watch stopped.") + + +def start_watch_thread( + repo_root: Path, + store: GraphStore, + daemon: bool = True, +) -> threading.Thread | None: + """Start watch mode in a background thread. + + Returns the started thread, or None if watchdog is unavailable. + """ + try: + import watchdog # noqa: F401 + except ImportError: + logger.warning("watchdog not installed; auto-watch disabled") + return None + + thread = threading.Thread( + target=watch, + args=(repo_root, store), + daemon=daemon, + name="crg-watch", + ) + thread.start() + logger.info("Auto-watch started for %s", repo_root) + return thread diff --git a/code_review_graph/jedi_resolver.py b/code_review_graph/jedi_resolver.py new file mode 100644 index 0000000..8ec007e --- /dev/null +++ b/code_review_graph/jedi_resolver.py @@ -0,0 +1,303 @@ +"""Post-build Jedi enrichment for Python call resolution. + +After tree-sitter parsing, many method calls on lowercase-receiver variables +are dropped (e.g. ``svc.authenticate()`` where ``svc = factory()``). Jedi +can resolve these by tracing return types across files. + +This module runs as a post-build step: it re-walks Python ASTs to find +dropped calls, uses ``jedi.Script.goto()`` to resolve them, and adds the +resulting CALLS edges to the graph database. +""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Optional + +from .parser import CodeParser, EdgeInfo +from .parser import _is_test_file as _parser_is_test_file + +logger = logging.getLogger(__name__) + +_SELF_NAMES = frozenset({"self", "cls", "super"}) + + +def enrich_jedi_calls(store, repo_root: Path) -> dict: + """Resolve untracked Python method calls via Jedi. + + Walks Python files, finds ``receiver.method()`` calls that tree-sitter + dropped (lowercase receiver, not self/cls), resolves them with Jedi, + and inserts new CALLS edges. + + Returns stats dict with ``resolved`` count. + """ + try: + import jedi + except ImportError: + logger.info("Jedi not installed, skipping Python enrichment") + return {"skipped": True, "reason": "jedi not installed"} + + repo_root = Path(repo_root).resolve() + + # Get Python files from the graph — skip early if none + all_files = store.get_all_files() + py_files = [f for f in all_files if f.endswith(".py")] + + if not py_files: + return {"resolved": 0, "files": 0} + + # Scope the Jedi project to Python-only directories to avoid scanning + # non-Python files (e.g. node_modules, TS sources). This matters for + # polyglot monorepos where jedi.Project(path=repo_root) would scan + # thousands of irrelevant files during initialization. + py_dirs = sorted({str(Path(f).parent) for f in py_files}) + common_py_root = Path(os.path.commonpath(py_dirs)) if py_dirs else repo_root + if not str(common_py_root).startswith(str(repo_root)): + common_py_root = repo_root + project = jedi.Project( + path=str(common_py_root), + added_sys_path=[str(repo_root)], + smart_sys_path=False, + ) + + # Pre-parse all Python files to find which ones have pending method calls. + # This avoids expensive Jedi Script creation for files with nothing to resolve. + parser = CodeParser() + ts_parser = parser._get_parser("python") + if not ts_parser: + return {"resolved": 0, "files": 0} + + # Build set of method names that actually exist in project code. + # No point asking Jedi to resolve `logger.getLogger()` if no project + # file defines a function called `getLogger`. + project_func_names = { + r["name"] + for r in store._conn.execute( + "SELECT DISTINCT name FROM nodes WHERE kind IN ('Function', 'Test')" + ).fetchall() + } + + files_with_pending: list[tuple[str, bytes, list]] = [] + total_skipped = 0 + for file_path in py_files: + try: + source = Path(file_path).read_bytes() + except (OSError, PermissionError): + continue + tree = ts_parser.parse(source) + is_test = _parser_is_test_file(file_path) + pending = _find_untracked_method_calls(tree.root_node, is_test) + if pending: + # Only keep calls whose method name exists in project code + filtered = [p for p in pending if p[2] in project_func_names] + total_skipped += len(pending) - len(filtered) + if filtered: + files_with_pending.append((file_path, source, filtered)) + + if not files_with_pending: + return {"resolved": 0, "files": 0} + + logger.debug( + "Jedi: %d/%d Python files have pending calls (%d calls skipped — no project target)", + len(files_with_pending), len(py_files), total_skipped, + ) + + resolved_count = 0 + files_enriched = 0 + errors = 0 + + for file_path, source, pending in files_with_pending: + source_text = source.decode("utf-8", errors="replace") + + # Get existing CALLS edges for this file to skip duplicates + existing = set() + for edge in _get_file_call_edges(store, file_path): + existing.add((edge.source_qualified, edge.line)) + + # Get function nodes from DB for enclosing-function lookup + func_nodes = [ + n for n in store.get_nodes_by_file(file_path) + if n.kind in ("Function", "Test") + ] + + # Create Jedi script once per file + try: + script = jedi.Script(source_text, path=file_path, project=project) + except Exception as e: + logger.debug("Jedi failed to load %s: %s", file_path, e) + errors += 1 + continue + + file_resolved = 0 + for jedi_line, col, _method_name, _enclosing_name in pending: + # Find enclosing function qualified name + enclosing = _find_enclosing(func_nodes, jedi_line) + if not enclosing: + enclosing = file_path # module-level + + # Skip if we already have a CALLS edge from this source at this line + if (enclosing, jedi_line) in existing: + continue + + # Ask Jedi to resolve + try: + names = script.goto(jedi_line, col) + except Exception: # nosec B112 - Jedi may fail on malformed code + continue + + if not names: + continue + + name = names[0] + if not name.module_path: + continue + + module_path = Path(name.module_path).resolve() + + # Only emit edges for project-internal definitions + try: + module_path.relative_to(repo_root) + except ValueError: + continue + + # Build qualified target: file_path::Class.method or file_path::func + target_file = str(module_path) + parent = name.parent() + if parent and parent.type == "class": + target = f"{target_file}::{parent.name}.{name.name}" + else: + target = f"{target_file}::{name.name}" + + store.upsert_edge(EdgeInfo( + kind="CALLS", + source=enclosing, + target=target, + file_path=file_path, + line=jedi_line, + )) + existing.add((enclosing, jedi_line)) + file_resolved += 1 + + if file_resolved: + files_enriched += 1 + resolved_count += file_resolved + + if resolved_count: + store.commit() + logger.info( + "Jedi enrichment: resolved %d calls in %d files", + resolved_count, files_enriched, + ) + + return { + "resolved": resolved_count, + "files": files_enriched, + "errors": errors, + } + + +def _get_file_call_edges(store, file_path: str): + """Get all CALLS edges originating from a file.""" + conn = store._conn + rows = conn.execute( + "SELECT * FROM edges WHERE file_path = ? AND kind = 'CALLS'", + (file_path,), + ).fetchall() + from .graph import GraphEdge + return [ + GraphEdge( + id=r["id"], kind=r["kind"], + source_qualified=r["source_qualified"], + target_qualified=r["target_qualified"], + file_path=r["file_path"], line=r["line"], + extra={}, + ) + for r in rows + ] + + +def _find_enclosing(func_nodes, line: int) -> Optional[str]: + """Find the qualified name of the function enclosing a given line.""" + best = None + best_span = float("inf") + for node in func_nodes: + if node.line_start <= line <= node.line_end: + span = node.line_end - node.line_start + if span < best_span: + best = node.qualified_name + best_span = span + return best + + +def _find_untracked_method_calls(root, is_test_file: bool = False): + """Walk Python AST to find method calls the parser would have dropped. + + Returns list of (jedi_line, col, method_name, enclosing_func_name) tuples. + Jedi_line is 1-indexed, col is 0-indexed. + """ + results: list[tuple[int, int, str, Optional[str]]] = [] + _walk_calls(root, results, is_test_file, enclosing_func=None) + return results + + +def _walk_calls(node, results, is_test_file, enclosing_func): + """Recursively walk AST collecting dropped method calls.""" + # Track enclosing function scope + if node.type == "function_definition": + name = None + for child in node.children: + if child.type == "identifier": + name = child.text.decode("utf-8", errors="replace") + break + for child in node.children: + _walk_calls(child, results, is_test_file, name or enclosing_func) + return + + if node.type == "decorated_definition": + for child in node.children: + _walk_calls(child, results, is_test_file, enclosing_func) + return + + # Check for call expressions with attribute access + if node.type == "call": + first = node.children[0] if node.children else None + if first and first.type == "attribute": + _check_dropped_call(first, results, is_test_file, enclosing_func) + + for child in node.children: + _walk_calls(child, results, is_test_file, enclosing_func) + + +def _check_dropped_call(attr_node, results, is_test_file, enclosing_func): + """Check if an attribute-based call was dropped by the parser.""" + children = attr_node.children + if len(children) < 2: + return + + receiver = children[0] + # Only handle simple identifier receivers + if receiver.type != "identifier": + return + + receiver_text = receiver.text.decode("utf-8", errors="replace") + + # The parser keeps: self/cls/super calls and uppercase-receiver calls + # The parser keeps: calls handled by typed-var enrichment (but those are + # separate edges -- we check for duplicates via existing-edge set) + if receiver_text in _SELF_NAMES: + return + if receiver_text[:1].isupper(): + return + if is_test_file: + return # test files already track all calls + + # Find the method name identifier + method_node = children[-1] + if method_node.type != "identifier": + return + + row, col = method_node.start_point # 0-indexed + method_name = method_node.text.decode("utf-8", errors="replace") + results.append((row + 1, col, method_name, enclosing_func)) diff --git a/code_review_graph/main.py b/code_review_graph/main.py new file mode 100644 index 0000000..629b167 --- /dev/null +++ b/code_review_graph/main.py @@ -0,0 +1,1079 @@ +"""MCP server entry point for Code Review Graph. + +Run as: code-review-graph serve +Communicates via stdio (standard MCP transport), or use +``code-review-graph serve --http`` for Streamable HTTP on localhost (port 5555 +by default). +""" + +from __future__ import annotations + +import asyncio +import logging +import os +import sys +from pathlib import Path +from typing import Optional + +from fastmcp import FastMCP + +from .graph import GraphStore +from .incremental import find_project_root, get_db_path, start_watch_thread +from .prompts import ( + architecture_map_prompt, + debug_issue_prompt, + onboard_developer_prompt, + pre_merge_check_prompt, + review_changes_prompt, +) +from .tools import ( + apply_refactor_func, + build_or_update_graph, + cross_repo_search_func, + detect_changes_func, + embed_graph, + find_large_functions, + generate_wiki_func, + get_affected_flows_func, + get_architecture_overview_func, + get_bridge_nodes_func, + get_community_func, + get_docs_section, + get_flow, + get_hub_nodes_func, + get_impact_radius, + get_knowledge_gaps_func, + get_minimal_context, + get_review_context, + get_suggested_questions_func, + get_surprising_connections_func, + get_wiki_page_func, + list_communities_func, + list_flows, + list_graph_stats, + list_repos_func, + query_graph, + refactor_func, + run_postprocess, + semantic_search_nodes, + traverse_graph_func, +) + +logger = logging.getLogger(__name__) + +# NOTE: Thread-safe for stdio MCP (single-threaded). If adding HTTP/SSE +# transport with concurrent requests, replace with contextvars.ContextVar. +_default_repo_root: str | None = None + + +def _resolve_repo_root(repo_root: Optional[str]) -> Optional[str]: + """Resolve repo_root for a tool call. + + Order of precedence: + 1. Explicit ``repo_root`` passed by the MCP client (highest). + 2. ``--repo`` CLI flag passed to ``code-review-graph serve`` + (captured in ``_default_repo_root``). + 3. None — the underlying impl will fall back to the server's cwd. + + All MCP tools that accept ``repo_root`` should use this helper so + ``serve --repo `` applies consistently, including + ``get_docs_section_tool``. See: #222. + """ + return repo_root if repo_root else _default_repo_root + + +mcp = FastMCP( + "code-review-graph", + instructions=( + "Persistent incremental knowledge graph for token-efficient, " + "context-aware code reviews. Parses your codebase with Tree-sitter, " + "builds a structural graph, and provides smart impact analysis." + ), +) + + +@mcp.tool() +async def build_or_update_graph_tool( + full_rebuild: bool = False, + repo_root: Optional[str] = None, + base: str = "HEAD~1", + postprocess: str = "full", + recurse_submodules: Optional[bool] = None, +) -> dict: + """Build or incrementally update the code knowledge graph. + + Call this first to initialize the graph, or after making changes. + By default performs an incremental update (only changed files). + Set full_rebuild=True to re-parse every file. + + Runs the blocking full_build / incremental_update work in a thread + via ``asyncio.to_thread`` so the stdio event loop stays responsive. + Without this wrapper, long builds deadlocked on Windows because + ``ProcessPoolExecutor`` (used by parallel parsing) interacted badly + with the sync handler blocking the only event-loop thread. See: + #46, #136. + + Args: + full_rebuild: If True, re-parse all files. Default: False (incremental). + repo_root: Repository root path. Auto-detected from current directory if omitted. + base: Git ref to diff against for incremental updates. Default: HEAD~1. + postprocess: Post-processing level: "full" (default), "minimal" (signatures+FTS only), + or "none" (skip all post-processing). Use "minimal" for faster builds. + recurse_submodules: If True, include files from git submodules. + When None (default), falls back to CRG_RECURSE_SUBMODULES env var. + """ + return await asyncio.to_thread( + build_or_update_graph, + full_rebuild=full_rebuild, + repo_root=_resolve_repo_root(repo_root), + base=base, + postprocess=postprocess, + recurse_submodules=recurse_submodules, + ) + + +@mcp.tool() +async def run_postprocess_tool( + flows: bool = True, + communities: bool = True, + fts: bool = True, + repo_root: Optional[str] = None, +) -> dict: + """Run post-processing on existing graph (flows, communities, FTS index). + + Use after building with postprocess="none" or "minimal", or to re-run + expensive steps independently. Signatures are always computed. + + Offloaded to a thread via ``asyncio.to_thread`` so community + detection on large graphs doesn't block the MCP event loop. See: + #46, #136. + + Args: + flows: Run flow detection. Default: True. + communities: Run community detection. Default: True. + fts: Rebuild FTS index. Default: True. + repo_root: Repository root path. Auto-detected if omitted. + """ + return await asyncio.to_thread( + run_postprocess, + flows=flows, communities=communities, fts=fts, + repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def get_minimal_context_tool( + task: str = "", + changed_files: Optional[list[str]] = None, + repo_root: Optional[str] = None, + base: str = "HEAD~1", +) -> dict: + """Get ultra-compact context for any task (~100 tokens). Always call this first. + + Returns graph stats, risk score, top communities/flows, and suggested + next tools in a single compact response. Use this as the entry point + before any other graph tool to minimize token usage. + + Args: + task: What you are doing (e.g. "review PR #42", "debug login timeout"). + changed_files: Explicit list of changed files. Auto-detected if omitted. + repo_root: Repository root path. Auto-detected if omitted. + base: Git ref for diff comparison. Default: HEAD~1. + """ + return get_minimal_context( + task=task, changed_files=changed_files, + repo_root=_resolve_repo_root(repo_root), base=base, + ) + + +@mcp.tool() +def get_impact_radius_tool( + changed_files: Optional[list[str]] = None, + max_depth: int = 2, + repo_root: Optional[str] = None, + base: str = "HEAD~1", + detail_level: str = "standard", +) -> dict: + """Analyze the blast radius of changed files in the codebase. + + Shows which functions, classes, and files are impacted by changes. + Auto-detects changed files from git if not specified. + + Args: + changed_files: List of changed file paths (relative to repo root). Auto-detected if omitted. + max_depth: Number of hops to traverse in the dependency graph. Default: 2. + repo_root: Repository root path. Auto-detected if omitted. + base: Git ref for auto-detecting changes. Default: HEAD~1. + detail_level: "standard" for full output, "minimal" for compact summary. Default: standard. + """ + return get_impact_radius( + changed_files=changed_files, max_depth=max_depth, + repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level, + ) + + +@mcp.tool() +def query_graph_tool( + pattern: str, + target: str, + repo_root: Optional[str] = None, + detail_level: str = "standard", +) -> dict: + """Run a predefined graph query to explore code relationships. + + Available patterns: + - callers_of: Find functions that call the target + - callees_of: Find functions called by the target + - imports_of: Find what the target imports + - importers_of: Find files that import the target + - children_of: Find nodes contained in a file or class + - tests_for: Find tests for the target + - inheritors_of: Find classes inheriting from the target + - file_summary: Get all nodes in a file + + Args: + pattern: Query pattern name (see above). + target: Node name, qualified name, or file path to query. + repo_root: Repository root path. Auto-detected if omitted. + detail_level: "standard" for full output, "minimal" for compact summary. Default: standard. + """ + return query_graph( + pattern=pattern, target=target, repo_root=_resolve_repo_root(repo_root), + detail_level=detail_level, + ) + + +@mcp.tool() +def get_review_context_tool( + changed_files: Optional[list[str]] = None, + max_depth: int = 2, + include_source: bool = True, + max_lines_per_file: int = 200, + repo_root: Optional[str] = None, + base: str = "HEAD~1", + detail_level: str = "standard", +) -> dict: + """Generate a focused, token-efficient review context for code changes. + + Combines impact analysis with source snippets and review guidance. + Use this for comprehensive code reviews. + + Args: + changed_files: Files to review. Auto-detected from git diff if omitted. + max_depth: Impact radius depth. Default: 2. + include_source: Include source code snippets. Default: True. + max_lines_per_file: Max source lines per file. Default: 200. + repo_root: Repository root path. Auto-detected if omitted. + base: Git ref for change detection. Default: HEAD~1. + detail_level: "standard" for full output, "minimal" for + token-efficient summary. Default: standard. + """ + return get_review_context( + changed_files=changed_files, max_depth=max_depth, + include_source=include_source, max_lines_per_file=max_lines_per_file, + repo_root=_resolve_repo_root(repo_root), base=base, detail_level=detail_level, + ) + + +@mcp.tool() +def semantic_search_nodes_tool( + query: str, + kind: Optional[str] = None, + limit: int = 20, + repo_root: Optional[str] = None, + model: Optional[str] = None, + provider: Optional[str] = None, + detail_level: str = "standard", +) -> dict: + """Search for code entities by name, keyword, or semantic similarity. + + Uses vector embeddings for semantic search when available (run embed_graph_tool + first, with a provider of your choice: "local" needs sentence-transformers, + "openai" / "google" / "minimax" need their respective env vars). Falls back + to FTS5 / keyword matching when no matching embeddings exist for the given + provider. + + Args: + query: Search string to match against node names. + kind: Optional filter: File, Class, Function, Type, or Test. + limit: Maximum results. Default: 20. + repo_root: Repository root path. Auto-detected if omitted. + model: Embedding model for query vectors. Must match the model used + during embed_graph. Falls back to CRG_EMBEDDING_MODEL env var + (local) or CRG_OPENAI_MODEL (openai). + provider: Embedding provider: "local" (default), "openai", "google", + or "minimax". Must match the provider used during embed_graph. + detail_level: "standard" for full output, "minimal" for compact summary. Default: standard. + """ + return semantic_search_nodes( + query=query, kind=kind, limit=limit, repo_root=_resolve_repo_root(repo_root), + model=model, provider=provider, detail_level=detail_level, + ) + + +@mcp.tool() +async def embed_graph_tool( + repo_root: Optional[str] = None, + model: Optional[str] = None, + provider: Optional[str] = None, +) -> dict: + """Compute vector embeddings for all graph nodes to enable semantic search. + + Requires: pip install code-review-graph[embeddings] (local provider only; + cloud providers use stdlib urllib). + Default provider: local. Default model: all-MiniLM-L6-v2. + Override provider via `provider` param, model via `model` param or + CRG_EMBEDDING_MODEL / CRG_OPENAI_MODEL env vars. + Changing the model or provider re-embeds all nodes automatically. + + After running this, semantic_search_nodes_tool will use vector similarity + instead of keyword matching for much better results. + + Runs the blocking sentence-transformers / Gemini / HTTP inference in a + thread via ``asyncio.to_thread`` so the stdio event loop stays + responsive — without this wrapper, embedding a large graph would + silently hang the MCP server on Windows. See: #46, #136. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + model: Embedding model. For local: HuggingFace ID/path; for openai: + model ID (e.g. "text-embedding-3-small"); for google: Gemini + model ID. Falls back to CRG_EMBEDDING_MODEL / CRG_OPENAI_MODEL + env vars as appropriate. + provider: "local" (default), "openai", "google", or "minimax". + "openai" requires CRG_OPENAI_BASE_URL + CRG_OPENAI_API_KEY + + CRG_OPENAI_MODEL env vars and accepts any OpenAI-compatible + endpoint (real OpenAI, Azure, new-api, LiteLLM, vLLM, etc.). + """ + return await asyncio.to_thread( + embed_graph, + repo_root=_resolve_repo_root(repo_root), + model=model, + provider=provider, + ) + + +@mcp.tool() +def list_graph_stats_tool( + repo_root: Optional[str] = None, +) -> dict: + """Get aggregate statistics about the code knowledge graph. + + Shows total nodes, edges, languages, files, and last update time. + Useful for checking if the graph is built and up to date. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + """ + return list_graph_stats(repo_root=_resolve_repo_root(repo_root)) + + +@mcp.tool() +def get_docs_section_tool( + section_name: str, + repo_root: Optional[str] = None, +) -> dict: + """Get a specific section from the LLM-optimized documentation reference. + + Returns only the requested section content for minimal token usage. + Use this before answering any user question about the plugin. + + Available sections: usage, review-delta, review-pr, commands, legal, + watch, embeddings, languages, troubleshooting. + + Args: + section_name: The section to retrieve (e.g. "review-delta", "usage"). + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_docs_section( + section_name=section_name, + repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def find_large_functions_tool( + min_lines: int = 50, + kind: Optional[str] = None, + file_path_pattern: Optional[str] = None, + limit: int = 50, + repo_root: Optional[str] = None, +) -> dict: + """Find functions, classes, or files exceeding a line-count threshold. + + Useful for decomposition audits, code quality checks, and enforcing + size limits during code review. Results are ordered by line count. + + Args: + min_lines: Minimum line count to flag. Default: 50. + kind: Optional filter: Function, Class, File, or Test. + file_path_pattern: Filter by file path substring (e.g. "components/"). + limit: Maximum results. Default: 50. + repo_root: Repository root path. Auto-detected if omitted. + """ + return find_large_functions( + min_lines=min_lines, kind=kind, file_path_pattern=file_path_pattern, + limit=limit, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def list_flows_tool( + sort_by: str = "criticality", + limit: int = 50, + kind: Optional[str] = None, + detail_level: str = "standard", + repo_root: Optional[str] = None, +) -> dict: + """List execution flows in the codebase, sorted by criticality. + + Each flow represents a call chain starting from an entry point + (HTTP handler, CLI command, test function, etc.). Use this to + understand the main execution paths through the codebase. + + Args: + sort_by: Sort column: criticality, depth, node_count, file_count, or name. + limit: Maximum flows to return. Default: 50. + kind: Optional filter by entry point kind (e.g. "Test", "Function"). + detail_level: "standard" (default) returns full flow data; "minimal" + returns only name, criticality, and node_count per flow. + repo_root: Repository root path. Auto-detected if omitted. + """ + return list_flows( + repo_root=_resolve_repo_root(repo_root), sort_by=sort_by, limit=limit, kind=kind, + detail_level=detail_level, + ) + + +@mcp.tool() +def get_flow_tool( + flow_id: Optional[int] = None, + flow_name: Optional[str] = None, + include_source: bool = False, + repo_root: Optional[str] = None, +) -> dict: + """Get detailed information about a single execution flow. + + Returns the full call path with each step's function name, file, and + line numbers. Optionally includes source code snippets for each step. + + Provide either flow_id (from list_flows_tool) or flow_name to search by name. + + Args: + flow_id: Database ID of the flow. + flow_name: Name to search for (partial match). Ignored if flow_id given. + include_source: Include source code snippets for each step. Default: False. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_flow( + flow_id=flow_id, flow_name=flow_name, + include_source=include_source, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def get_affected_flows_tool( + changed_files: Optional[list[str]] = None, + base: str = "HEAD~1", + repo_root: Optional[str] = None, +) -> dict: + """Find execution flows affected by changed files. + + Identifies which execution flows pass through nodes in the changed files. + Useful during code review to understand which user-facing or critical paths + are impacted by a change. Auto-detects changed files from git if not specified. + + Args: + changed_files: List of changed file paths (relative to repo root). Auto-detected if omitted. + base: Git ref for auto-detecting changes. Default: HEAD~1. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_affected_flows_func( + changed_files=changed_files, base=base, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def list_communities_tool( + sort_by: str = "size", + min_size: int = 0, + detail_level: str = "standard", + repo_root: Optional[str] = None, +) -> dict: + """List detected code communities in the codebase. + + Each community represents a cluster of related code entities (functions, + classes) detected via the Leiden algorithm or file-based grouping. + Use this to understand the high-level structure of the codebase. + + Args: + sort_by: Sort column: size, cohesion, or name. + min_size: Minimum community size to include. Default: 0. + detail_level: "standard" (default) returns full community data; + "minimal" returns only name, size, and cohesion + per community. + repo_root: Repository root path. Auto-detected if omitted. + """ + return list_communities_func( + repo_root=_resolve_repo_root(repo_root), sort_by=sort_by, min_size=min_size, + detail_level=detail_level, + ) + + +@mcp.tool() +def get_community_tool( + community_name: Optional[str] = None, + community_id: Optional[int] = None, + include_members: bool = False, + repo_root: Optional[str] = None, +) -> dict: + """Get detailed information about a single code community. + + Returns community metadata including size, cohesion, dominant language, + and member list. Optionally includes full node details for each member. + + Provide either community_id (from list_communities_tool) or community_name + to search by name. + + Args: + community_name: Name to search for (partial match). Ignored if community_id given. + community_id: Database ID of the community. + include_members: Include full member node details. Default: False. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_community_func( + community_name=community_name, community_id=community_id, + include_members=include_members, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def get_architecture_overview_tool( + repo_root: Optional[str] = None, + detail_level: str = "minimal", +) -> dict: + """Generate an architecture overview based on community structure. + + Builds a high-level view of the codebase architecture by analyzing + community boundaries and cross-community coupling. Includes warnings + for high coupling between communities. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + detail_level: "minimal" (default) drops community member lists + and aggregates cross-community edges to one row per + community pair (typical reduction: 600KB -> <5KB); + "standard" returns full per-edge detail. + """ + return get_architecture_overview_func( + repo_root=_resolve_repo_root(repo_root), + detail_level=detail_level, + ) + + +@mcp.tool() +async def detect_changes_tool( + base: str = "HEAD~1", + changed_files: Optional[list[str]] = None, + include_source: bool = False, + max_depth: int = 2, + repo_root: Optional[str] = None, + detail_level: str = "standard", +) -> dict: + """Detect changes and produce risk-scored, priority-ordered review guidance. + + Primary tool for code review. Maps git diffs to affected functions, + flows, communities, and test coverage gaps. Returns risk scores and + prioritized review items. Replaces get_review_context for change-aware reviews. + + Offloaded to a thread via ``asyncio.to_thread`` — runs `git diff` + subprocesses and BFS traversals that can take several seconds on + large repos. See: #46, #136. + + Args: + base: Git ref to diff against. Default: HEAD~1. + changed_files: List of changed file paths (relative to repo root). Auto-detected if omitted. + include_source: Include source code snippets for changed functions. Default: False. + max_depth: Impact radius depth for BFS traversal. Default: 2. + repo_root: Repository root path. Auto-detected if omitted. + detail_level: "standard" for full output, "minimal" for + token-efficient summary. Default: standard. + """ + coro = asyncio.to_thread( + detect_changes_func, + base=base, changed_files=changed_files, + include_source=include_source, max_depth=max_depth, + repo_root=_resolve_repo_root(repo_root), detail_level=detail_level, + ) + tool_timeout = int(os.environ.get("CRG_TOOL_TIMEOUT", "0")) + if tool_timeout > 0: + try: + return await asyncio.wait_for(coro, timeout=tool_timeout) + except asyncio.TimeoutError: + message = ( + f"detect_changes_tool timed out after {tool_timeout}s. " + "Reduce scope with CRG_MAX_CHANGED_FUNCS / CRG_MAX_TRANSITIVE_FRONTIER, " + "or increase CRG_TOOL_TIMEOUT." + ) + return { + "status": "error", + "error": message, + "summary": message, + } + return await coro + + +@mcp.tool() +def refactor_tool( + mode: str = "rename", + old_name: Optional[str] = None, + new_name: Optional[str] = None, + kind: Optional[str] = None, + file_pattern: Optional[str] = None, + repo_root: Optional[str] = None, +) -> dict: + """Graph-powered refactoring operations. + + Unified entry point for rename previews, dead code detection, and + refactoring suggestions. + + Modes: + - rename: Preview renaming a symbol. Returns an edit list and a refactor_id + to pass to apply_refactor_tool. Requires old_name and new_name. + - dead_code: Find unreferenced functions/classes (no callers, tests, or + importers, and not entry points). + - suggest: Get community-driven refactoring suggestions (move misplaced + functions, remove dead code). + + Args: + mode: Operation mode: "rename", "dead_code", or "suggest". + old_name: (rename) Current symbol name to rename. + new_name: (rename) Desired new name for the symbol. + kind: (dead_code) Optional filter: Function or Class. + file_pattern: (dead_code) Filter by file path substring. + repo_root: Repository root path. Auto-detected if omitted. + """ + return refactor_func( + mode=mode, old_name=old_name, new_name=new_name, + kind=kind, file_pattern=file_pattern, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def apply_refactor_tool( + refactor_id: str, + repo_root: Optional[str] = None, + dry_run: bool = False, +) -> dict: + """Apply a previously previewed refactoring to source files. + + Takes a refactor_id from a prior refactor_tool(mode="rename") call and + applies the exact string replacements to the target files. Previews + expire after 10 minutes. + + Security: All edit paths are validated to be within the repo root. + Only exact string replacements are performed (no regex, no eval). + + Args: + refactor_id: The refactor ID from refactor_tool's response. + repo_root: Repository root path. Auto-detected if omitted. + dry_run: If True, return a unified diff of what would change + without touching any files. The refactor_id remains valid so + the same preview can be applied in a follow-up call without + dry_run. Use this for a human-in-the-loop review before + committing changes to disk. See: #176 + """ + return apply_refactor_func( + refactor_id=refactor_id, repo_root=_resolve_repo_root(repo_root), + dry_run=dry_run, + ) + + +@mcp.tool() +async def generate_wiki_tool( + repo_root: Optional[str] = None, + force: bool = False, +) -> dict: + """Generate a markdown wiki from the code community structure. + + Creates a wiki page for each detected community and an index page. + Pages are written to .code-review-graph/wiki/ inside the repository. + Only regenerates pages whose content has changed unless force=True. + + Offloaded to a thread via ``asyncio.to_thread`` — on large graphs + the page-generation loop touches every community and issues many + SQLite reads, which would block the MCP event loop. See: #46, #136. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + force: If True, regenerate all pages even if content unchanged. Default: False. + """ + return await asyncio.to_thread( + generate_wiki_func, + repo_root=_resolve_repo_root(repo_root), + force=force, + ) + + +@mcp.tool() +def get_wiki_page_tool( + community_name: str, + repo_root: Optional[str] = None, +) -> dict: + """Retrieve a specific wiki page by community name. + + Returns the markdown content of the wiki page for the given community. + The wiki must have been generated first via generate_wiki_tool. + + Args: + community_name: Community name to look up. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_wiki_page_func( + community_name=community_name, repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def get_hub_nodes_tool( + top_n: int = 10, + repo_root: Optional[str] = None, +) -> dict: + """Find the most connected nodes in the codebase (architectural hotspots). + + Hub nodes have the highest total degree (in + out edges). Changes to + them have disproportionate blast radius. Excludes File nodes. + + Args: + top_n: Number of top hubs to return. Default: 10. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_hub_nodes_func( + repo_root=_resolve_repo_root(repo_root), top_n=top_n, + ) + + +@mcp.tool() +def get_bridge_nodes_tool( + top_n: int = 10, + repo_root: Optional[str] = None, +) -> dict: + """Find architectural chokepoints via betweenness centrality. + + Bridge nodes sit on shortest paths between many node pairs. + If they break, multiple code regions lose connectivity. + Uses sampling approximation for graphs > 5000 nodes. + + Args: + top_n: Number of top bridges to return. Default: 10. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_bridge_nodes_func( + repo_root=_resolve_repo_root(repo_root), top_n=top_n, + ) + + +@mcp.tool() +def get_knowledge_gaps_tool( + repo_root: Optional[str] = None, +) -> dict: + """Identify structural weaknesses in the codebase graph. + + Finds isolated nodes (disconnected), thin communities (< 3 members), + untested hotspots (high-degree nodes without test coverage), and + single-file communities. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_knowledge_gaps_func( + repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def get_surprising_connections_tool( + top_n: int = 15, + repo_root: Optional[str] = None, +) -> dict: + """Find unexpected architectural coupling via composite surprise scoring. + + Scores edges by: cross-community (+0.3), cross-language (+0.2), + peripheral-to-hub (+0.2), cross-test-boundary (+0.15), and + unusual edge kinds (+0.15). + + Args: + top_n: Number of top surprises to return. Default: 15. + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_surprising_connections_func( + repo_root=_resolve_repo_root(repo_root), top_n=top_n, + ) + + +@mcp.tool() +def get_suggested_questions_tool( + repo_root: Optional[str] = None, +) -> dict: + """Auto-generate review questions from graph analysis. + + Produces prioritized questions about: bridge nodes needing tests, + untested hub nodes, surprising cross-community coupling, thin + communities, and untested hotspots. + + Args: + repo_root: Repository root path. Auto-detected if omitted. + """ + return get_suggested_questions_func( + repo_root=_resolve_repo_root(repo_root), + ) + + +@mcp.tool() +def traverse_graph_tool( + query: str, + mode: str = "bfs", + depth: int = 3, + token_budget: int = 2000, + repo_root: Optional[str] = None, +) -> dict: + """BFS/DFS traversal from best-matching node with token budget. + + Free-form graph exploration: finds the node best matching your + query, then traverses outward via BFS or DFS up to the given + depth, collecting connected nodes within the token budget. + + Args: + query: Search string to find the starting node. + mode: Traversal mode: "bfs" (breadth-first) or "dfs" + (depth-first). Default: bfs. + depth: Max traversal depth (1-6). Default: 3. + token_budget: Approximate token limit for results. + Default: 2000. + repo_root: Repository root path. Auto-detected if omitted. + """ + return traverse_graph_func( + query=query, mode=mode, depth=depth, + token_budget=token_budget, + repo_root=_resolve_repo_root(repo_root) or "", + ) + + +@mcp.tool() +def list_repos_tool() -> dict: + """List all registered repositories in the multi-repo registry. + + Returns the list of repos registered at ~/.code-review-graph/registry.json. + Use the CLI 'register' command to add repos. + """ + return list_repos_func() + + +@mcp.tool() +def cross_repo_search_tool( + query: str, + kind: Optional[str] = None, + limit: int = 20, +) -> dict: + """Search for code entities across all registered repositories. + + Runs hybrid search on each registered repo's graph database and merges + the results by score. Register repos first with the CLI 'register' command. + + Args: + query: Search string to match against node names. + kind: Optional filter: File, Class, Function, Type, or Test. + limit: Maximum results per repo. Default: 20. + """ + return cross_repo_search_func(query=query, kind=kind, limit=limit) + + +@mcp.prompt() +def review_changes(base: str = "HEAD~1") -> list[dict]: + """Pre-commit review workflow using detect_changes, affected_flows, and test gaps. + + Produces a structured code review with risk levels and actionable findings. + + Args: + base: Git ref to diff against. Default: HEAD~1. + """ + return review_changes_prompt(base=base) + + +@mcp.prompt() +def architecture_map() -> list[dict]: + """Architecture documentation using communities, flows, and Mermaid diagrams. + + Generates a comprehensive architecture map with module summaries and coupling warnings. + """ + return architecture_map_prompt() + + +@mcp.prompt() +def debug_issue(description: str = "") -> list[dict]: + """Guided debugging using search, flow tracing, and recent changes. + + Systematic debugging workflow that traces execution paths and identifies root causes. + + Args: + description: Description of the issue to debug. + """ + return debug_issue_prompt(description=description) + + +@mcp.prompt() +def onboard_developer() -> list[dict]: + """New developer orientation using stats, architecture, and critical flows. + + Creates an onboarding guide covering codebase structure, key modules, and patterns. + """ + return onboard_developer_prompt() + + +@mcp.prompt() +def pre_merge_check(base: str = "HEAD~1") -> list[dict]: + """PR readiness check with risk scoring, test gaps, and dead code detection. + + Produces a merge readiness report with risk assessment and recommendations. + + Args: + base: Git ref to diff against. Default: HEAD~1. + """ + return pre_merge_check_prompt(base=base) + + +def _apply_tool_filter(tools: str | None = None) -> None: + """Remove tools not listed in the allow-list. + + Accepts a comma-separated string of tool names to keep. When set, + every registered MCP tool whose name is **not** in the list is + removed via ``FastMCP.remove_tool()``. + + The allow-list can be supplied in two ways (first match wins): + + 1. ``tools`` argument (from ``serve --tools ...``). + 2. ``CRG_TOOLS`` environment variable. + + When neither is set, all tools remain available. + + This is useful for token-constrained environments: CRG exposes 28+ + tools by default (~8k description tokens per LLM turn). Filtering + to a working set of 5-10 tools can reduce overhead by 70-85%. + + Example:: + + # via CLI + code-review-graph serve --tools query_graph_tool,semantic_search_nodes_tool + + # via env var + CRG_TOOLS=query_graph_tool,semantic_search_nodes_tool + """ + import asyncio + import os + + raw = tools or os.environ.get("CRG_TOOLS") + if not raw: + return + allowed = {t.strip() for t in raw.split(",") if t.strip()} + if not allowed: + return + # FastMCP >=3 exposes tool enumeration via the async ``list_tools`` + # method. ``_apply_tool_filter`` is typically called from + # ``main()`` before the MCP event loop starts, but tests may invoke + # it from within a running event loop — in that case ``asyncio.run`` + # raises ``RuntimeError``. Fall back to running the coroutine on a + # dedicated short-lived loop in a worker thread. Earlier code path + # relied on ``mcp._tool_manager._tools`` which is a private + # attribute that was removed in fastmcp>=3.0. + def _list_tool_names() -> list[str]: + coro_factory = mcp.list_tools + try: + asyncio.get_running_loop() + except RuntimeError: + return [t.name for t in asyncio.run(coro_factory())] + import concurrent.futures + + def _runner() -> list[str]: + return [t.name for t in asyncio.run(coro_factory())] + + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: + return pool.submit(_runner).result() + + for name in _list_tool_names(): + if name not in allowed: + mcp.local_provider.remove_tool(name) + + + +def main( + repo_root: str | None = None, + tools: str | None = None, + auto_watch: bool = False, + *, + transport: str = "stdio", + host: str | None = None, + port: int | None = None, +) -> None: + """Run the MCP server (stdio or HTTP). + + On Windows, Python 3.8+ defaults to ``ProactorEventLoop``, which + interacts poorly with ``concurrent.futures.ProcessPoolExecutor`` + (used by ``full_build``) over a stdio MCP transport — the combination + produces silent hangs on ``build_or_update_graph_tool`` and + ``embed_graph_tool``. Switching to ``WindowsSelectorEventLoopPolicy`` + before fastmcp starts its loop avoids the deadlock. + See: #46, #136 + + Args: + repo_root: Default repository root for all tool calls. + tools: Comma-separated list of tool names to expose. + Falls back to ``CRG_TOOLS`` env var. When unset, all + tools are available. + auto_watch: Start filesystem watcher in a background daemon thread + while the MCP server runs. + transport: ``"stdio"`` (default) or ``"streamable-http"`` for local HTTP. + host: Bind address when using HTTP (required for HTTP; set by CLI). + port: Port when using HTTP (required for HTTP; set by CLI). + """ + global _default_repo_root + root = Path(repo_root) if repo_root else find_project_root() + _default_repo_root = str(root) + _apply_tool_filter(tools) + + watch_store: GraphStore | None = None + if auto_watch: + watch_store = GraphStore(get_db_path(root)) + thread = start_watch_thread(root, watch_store, daemon=True) + if thread is None: + logger.warning("Auto-watch was requested but could not be started") + + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + # Pre-warm sentence-transformers on the main thread before fastmcp's + # event loop starts. Lazy-loading ``torch`` + tokenizers inside an + # executor worker thread deadlocks ``semantic_search_nodes_tool`` on + # Windows stdio MCP (DLL init / OpenMP thread-pool registration grabs + # locks the loop needs). #385 added ``asyncio.to_thread`` to peer + # tools but cannot fix this case — the dangerous initialization has + # to happen on the main thread before any worker thread is spawned. + from .embeddings import prewarm_local_embeddings + prewarm_local_embeddings() + + try: + if transport == "stdio": + # Stdio MCP must keep stdout strictly JSON-RPC. FastMCP's banner/update + # notices corrupt the handshake stream on clients like Codex CLI. + mcp.run(transport="stdio", show_banner=False) + elif transport == "streamable-http": + if host is None or port is None: + raise ValueError("streamable-http transport requires host and port") + mcp.run(transport="streamable-http", host=host, port=port) + else: + raise ValueError(f"unsupported transport: {transport!r}") + finally: + if watch_store is not None: + watch_store.close() + + +if __name__ == "__main__": + main() diff --git a/code_review_graph/memory.py b/code_review_graph/memory.py new file mode 100644 index 0000000..81088ed --- /dev/null +++ b/code_review_graph/memory.py @@ -0,0 +1,142 @@ +"""Memory/feedback loop -- persist Q&A results for graph enrichment.""" + +from __future__ import annotations + +import logging +import re +import time +from pathlib import Path +from typing import Any + +logger = logging.getLogger(__name__) + + +def save_result( + question: str, + answer: str, + nodes: list[str] | None = None, + result_type: str = "query", + memory_dir: Path | None = None, + repo_root: Path | None = None, +) -> Path: + """Save a Q&A result as markdown for re-ingestion. + + Args: + question: The question that was asked. + answer: The answer/result. + nodes: Related node qualified names. + result_type: Type of result (query, review, debug). + memory_dir: Directory to save to. Defaults to + /.code-review-graph/memory/ + repo_root: Repository root for default memory_dir. + + Returns: + Path to the saved file. + """ + if memory_dir is None: + if repo_root is None: + raise ValueError( + "Either memory_dir or repo_root required" + ) + memory_dir = ( + repo_root / ".code-review-graph" / "memory" + ) + + memory_dir.mkdir(parents=True, exist_ok=True) + + # Generate filename from question + slug = re.sub(r"[^\w\s-]", "", question.lower()) + slug = re.sub(r"[\s_]+", "-", slug).strip("-")[:60] + timestamp = int(time.time()) + filename = f"{slug}-{timestamp}.md" + + # Build markdown with YAML frontmatter + lines = [ + "---", + f"type: {result_type}", + f"timestamp: {timestamp}", + ] + if nodes: + lines.append("nodes:") + for n in nodes[:20]: + lines.append(f" - {n}") + lines.extend([ + "---", + "", + f"# {question}", + "", + answer, + ]) + + path = memory_dir / filename + path.write_text("\n".join(lines), encoding="utf-8") + logger.info("Saved result to %s", path) + return path + + +def list_memories( + memory_dir: Path | None = None, + repo_root: Path | None = None, +) -> list[dict[str, Any]]: + """List all saved memory files. + + Returns list of dicts with: path, question, type, timestamp. + """ + if memory_dir is None: + if repo_root is None: + return [] + memory_dir = ( + repo_root / ".code-review-graph" / "memory" + ) + + if not memory_dir.exists(): + return [] + + results = [] + for f in sorted(memory_dir.glob("*.md")): + try: + text = f.read_text(encoding="utf-8") + # Parse frontmatter + meta: dict[str, Any] = {"path": str(f)} + if text.startswith("---"): + parts = text.split("---", 2) + if len(parts) >= 3: + fm_lines = parts[1].strip().split("\n") + for line in fm_lines: + if ": " in line and not line.startswith(" "): + k, v = line.split(": ", 1) + meta[k.strip()] = v.strip() + # Extract question from first heading + for line in text.split("\n"): + if line.startswith("# "): + meta["question"] = line[2:].strip() + break + results.append(meta) + except OSError: + continue + + return results + + +def clear_memories( + memory_dir: Path | None = None, + repo_root: Path | None = None, +) -> int: + """Delete all memory files. Returns count deleted.""" + if memory_dir is None: + if repo_root is None: + return 0 + memory_dir = ( + repo_root / ".code-review-graph" / "memory" + ) + + if not memory_dir.exists(): + return 0 + + count = 0 + for f in memory_dir.glob("*.md"): + f.unlink() + count += 1 + + logger.info("Cleared %d memory files", count) + return count diff --git a/code_review_graph/migrations.py b/code_review_graph/migrations.py new file mode 100644 index 0000000..1787b98 --- /dev/null +++ b/code_review_graph/migrations.py @@ -0,0 +1,284 @@ +"""Schema migration framework for the code-review-graph SQLite database. + +Manages incremental schema changes via versioned migration functions. +Each migration is idempotent (uses IF NOT EXISTS / column existence checks). +""" + +from __future__ import annotations + +import logging +import sqlite3 +from typing import Callable + +logger = logging.getLogger(__name__) + + +def get_schema_version(conn: sqlite3.Connection) -> int: + """Read the current schema version from the metadata table. + + Returns: + int: The schema version (0 if metadata table doesn't exist, 1 if not set). + """ + try: + row = conn.execute( + "SELECT value FROM metadata WHERE key = 'schema_version'" + ).fetchone() + if row is None: + return 1 + return int(row[0] if isinstance(row, (tuple, list)) else row["value"]) + except sqlite3.OperationalError: + # metadata table doesn't exist + return 0 + + +def _set_schema_version(conn: sqlite3.Connection, version: int) -> None: + """Set the schema version in the metadata table.""" + conn.execute( + "INSERT OR REPLACE INTO metadata (key, value) VALUES ('schema_version', ?)", + (str(version),), + ) + + +_KNOWN_TABLES = frozenset({ + "nodes", "edges", "metadata", "communities", "flows", "flow_memberships", "nodes_fts", + "community_summaries", "flow_snapshots", "risk_index", +}) + + +def _has_column(conn: sqlite3.Connection, table: str, column: str) -> bool: + """Check if a column exists in a table.""" + if table not in _KNOWN_TABLES: + raise ValueError(f"Unknown table: {table}") + cursor = conn.execute(f"PRAGMA table_info({table})") # noqa: S608 + columns = [row[1] if isinstance(row, tuple) else row["name"] for row in cursor] + return column in columns + + +def _table_exists(conn: sqlite3.Connection, table: str) -> bool: + """Check if a table exists.""" + if table not in _KNOWN_TABLES: + raise ValueError(f"Unknown table: {table}") + row = conn.execute( + "SELECT count(*) FROM sqlite_master WHERE type IN ('table', 'view') " + "AND name = ?", + (table,), + ).fetchone() + return row[0] > 0 + + +# --------------------------------------------------------------------------- +# Migration functions +# --------------------------------------------------------------------------- + + +def _migrate_v2(conn: sqlite3.Connection) -> None: + """v2: Add signature column to nodes table.""" + if not _has_column(conn, "nodes", "signature"): + conn.execute("ALTER TABLE nodes ADD COLUMN signature TEXT") + logger.info("Migration v2: added 'signature' column to nodes") + + +def _migrate_v3(conn: sqlite3.Connection) -> None: + """v3: Create flows and flow_memberships tables.""" + conn.execute(""" + CREATE TABLE IF NOT EXISTS flows ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + entry_point_id INTEGER NOT NULL, + depth INTEGER NOT NULL, + node_count INTEGER NOT NULL, + file_count INTEGER NOT NULL, + criticality REAL NOT NULL DEFAULT 0.0, + path_json TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')), + updated_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS flow_memberships ( + flow_id INTEGER NOT NULL, + node_id INTEGER NOT NULL, + position INTEGER NOT NULL, + PRIMARY KEY (flow_id, node_id) + ) + """) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_flows_criticality ON flows(criticality DESC)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_flows_entry ON flows(entry_point_id)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_flow_memberships_node ON flow_memberships(node_id)" + ) + logger.info("Migration v3: created flows and flow_memberships tables") + + +def _migrate_v4(conn: sqlite3.Connection) -> None: + """v4: Create communities table, add community_id to nodes.""" + conn.execute(""" + CREATE TABLE IF NOT EXISTS communities ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + level INTEGER NOT NULL DEFAULT 0, + parent_id INTEGER, + cohesion REAL NOT NULL DEFAULT 0.0, + size INTEGER NOT NULL DEFAULT 0, + dominant_language TEXT, + description TEXT, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ) + """) + if not _has_column(conn, "nodes", "community_id"): + conn.execute("ALTER TABLE nodes ADD COLUMN community_id INTEGER") + logger.info("Migration v4: added 'community_id' column to nodes") + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_nodes_community ON nodes(community_id)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_communities_parent ON communities(parent_id)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_communities_cohesion ON communities(cohesion DESC)" + ) + logger.info("Migration v4: created communities table") + + +def _migrate_v5(conn: sqlite3.Connection) -> None: + """v5: Create FTS5 virtual table for nodes.""" + if not _table_exists(conn, "nodes_fts"): + conn.execute(""" + CREATE VIRTUAL TABLE nodes_fts USING fts5( + name, qualified_name, file_path, signature, + content='nodes', content_rowid='rowid', + tokenize='porter unicode61' + ) + """) + logger.info("Migration v5: created nodes_fts FTS5 virtual table") + + +def _migrate_v6(conn: sqlite3.Connection) -> None: + """v6: Add pre-computed summary tables for token-efficient queries.""" + conn.execute(""" + CREATE TABLE IF NOT EXISTS community_summaries ( + community_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + purpose TEXT DEFAULT '', + key_symbols TEXT DEFAULT '[]', + risk TEXT DEFAULT 'unknown', + size INTEGER DEFAULT 0, + dominant_language TEXT DEFAULT '', + FOREIGN KEY (community_id) REFERENCES communities(id) + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS flow_snapshots ( + flow_id INTEGER PRIMARY KEY, + name TEXT NOT NULL, + entry_point TEXT NOT NULL, + critical_path TEXT DEFAULT '[]', + criticality REAL DEFAULT 0.0, + node_count INTEGER DEFAULT 0, + file_count INTEGER DEFAULT 0, + FOREIGN KEY (flow_id) REFERENCES flows(id) + ) + """) + conn.execute(""" + CREATE TABLE IF NOT EXISTS risk_index ( + node_id INTEGER PRIMARY KEY, + qualified_name TEXT NOT NULL, + risk_score REAL DEFAULT 0.0, + caller_count INTEGER DEFAULT 0, + test_coverage TEXT DEFAULT 'unknown', + security_relevant INTEGER DEFAULT 0, + last_computed TEXT DEFAULT '', + FOREIGN KEY (node_id) REFERENCES nodes(id) + ) + """) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_risk_index_score " + "ON risk_index(risk_score DESC)" + ) + logger.info("Migration v6: created summary tables " + "(community_summaries, flow_snapshots, risk_index)") + + +def _migrate_v7(conn: sqlite3.Connection) -> None: + """v7: Add compound edge indexes for summary and risk queries.""" + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_edges_target_kind " + "ON edges(target_qualified, kind)" + ) + conn.execute( + "CREATE INDEX IF NOT EXISTS idx_edges_source_kind " + "ON edges(source_qualified, kind)" + ) + logger.info("Migration v7: added compound edge indexes") + + +def _migrate_v8(conn: sqlite3.Connection) -> None: + """v8: Add composite index on edges for upsert_edge performance.""" + conn.execute(""" + CREATE INDEX IF NOT EXISTS idx_edges_composite + ON edges(kind, source_qualified, target_qualified, file_path, line) + """) + logger.info("Migration v8: created composite edge index") + + +def _migrate_v9(conn: sqlite3.Connection) -> None: + """v9: Add confidence scoring to edges.""" + if not _has_column(conn, "edges", "confidence"): + conn.execute( + "ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0" + ) + if not _has_column(conn, "edges", "confidence_tier"): + conn.execute( + "ALTER TABLE edges ADD COLUMN confidence_tier TEXT DEFAULT 'EXTRACTED'" + ) + logger.info("Migration v9: added edge confidence columns") + + +# --------------------------------------------------------------------------- +# Migration registry +# --------------------------------------------------------------------------- + +MIGRATIONS: dict[int, Callable[[sqlite3.Connection], None]] = { + 2: _migrate_v2, + 3: _migrate_v3, + 4: _migrate_v4, + 5: _migrate_v5, + 6: _migrate_v6, + 7: _migrate_v7, + 8: _migrate_v8, + 9: _migrate_v9, +} + +LATEST_VERSION = max(MIGRATIONS.keys()) + + +def run_migrations(conn: sqlite3.Connection) -> None: + """Run all pending migrations in order. + + Each migration runs in its own transaction. The schema_version metadata + entry is updated after each successful migration. + """ + current = get_schema_version(conn) + if current >= LATEST_VERSION: + return + + logger.info("Schema version %d -> %d: running migrations", current, LATEST_VERSION) + + for version in sorted(MIGRATIONS.keys()): + if version <= current: + continue + logger.info("Running migration v%d", version) + try: + MIGRATIONS[version](conn) + _set_schema_version(conn, version) + conn.commit() + except sqlite3.Error: + conn.rollback() + logger.error("Migration v%d failed, rolling back", version, exc_info=True) + raise + + logger.info("Migrations complete, now at schema version %d", LATEST_VERSION) diff --git a/code_review_graph/parser.py b/code_review_graph/parser.py new file mode 100644 index 0000000..c55b2e8 --- /dev/null +++ b/code_review_graph/parser.py @@ -0,0 +1,6957 @@ +"""Tree-sitter based multi-language code parser. + +Extracts structural nodes (classes, functions, imports, types) and edges +(calls, inheritance, contains) from source files. +""" + +from __future__ import annotations + +import hashlib +import json +import logging +import re +from dataclasses import dataclass, field +from pathlib import Path +from typing import NamedTuple, Optional + +import tree_sitter_language_pack as tslp + +from .custom_languages import CustomLanguage, load_custom_languages +from .tsconfig_resolver import TsconfigResolver + + +class CellInfo(NamedTuple): + """Represents a single cell in a notebook with its language.""" + cell_index: int + language: str + source: str + + +_SQL_TABLE_RE = re.compile( + r"(?:FROM|JOIN|INTO|CREATE\s+(?:OR\s+REPLACE\s+)?(?:TABLE|VIEW)|INSERT\s+OVERWRITE)" + r"\s+((?:`[^`]+`|\w+)(?:\.(?:`[^`]+`|\w+))*)", + re.IGNORECASE, +) + +# SQL keywords that can appear after FROM/JOIN but are NOT table names. +_SQL_KEYWORDS: frozenset[str] = frozenset({ + "SELECT", "WHERE", "GROUP", "ORDER", "HAVING", "LIMIT", "OFFSET", + "UNION", "INTERSECT", "EXCEPT", "AS", "ON", "USING", "SET", + "VALUES", "DEFAULT", "NULL", "TRUE", "FALSE", + "INNER", "OUTER", "LEFT", "RIGHT", "FULL", "CROSS", "NATURAL", + "LATERAL", "RECURSIVE", "ONLY", "WITH", +}) + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Data models for extracted entities +# --------------------------------------------------------------------------- + + +@dataclass +class NodeInfo: + kind: str # File, Class, Function, Type, Test + name: str + file_path: str + line_start: int + line_end: int + language: str = "" + parent_name: Optional[str] = None # enclosing class/module + params: Optional[str] = None + return_type: Optional[str] = None + modifiers: Optional[str] = None + is_test: bool = False + extra: dict = field(default_factory=dict) + + +@dataclass +class EdgeInfo: + # CALLS, IMPORTS_FROM, INHERITS, IMPLEMENTS, CONTAINS, + # TESTED_BY, DEPENDS_ON, REFERENCES + kind: str + source: str # qualified name or path + target: str # qualified name or path + file_path: str + line: int = 0 + extra: dict = field(default_factory=dict) + + +# --------------------------------------------------------------------------- +# Language extension mapping +# --------------------------------------------------------------------------- + +EXTENSION_TO_LANGUAGE: dict[str, str] = { + ".py": "python", + ".js": "javascript", + ".jsx": "javascript", + ".ts": "typescript", + ".tsx": "tsx", + ".go": "go", + ".rs": "rust", + ".java": "java", + ".cs": "csharp", + ".rb": "ruby", + ".cpp": "cpp", + ".cc": "cpp", + ".cxx": "cpp", + ".c": "c", + ".h": "c", + ".hpp": "cpp", + ".hh": "cpp", + ".kt": "kotlin", + ".swift": "swift", + ".php": "php", + ".scala": "scala", + ".sol": "solidity", + ".vue": "vue", + ".dart": "dart", + ".r": "r", # .lower() in detect_language handles .R → .r + ".mjs": "javascript", + ".astro": "typescript", + ".pl": "perl", + ".pm": "perl", + ".t": "perl", + ".xs": "c", # Perl XS: parsed as C to capture functions/structs/includes + ".lua": "lua", + ".luau": "luau", + ".m": "objc", # Objective-C (.h still maps to C; .mm defers to C++ for simplicity) + ".sh": "bash", + ".bash": "bash", + ".zsh": "bash", + ".ksh": "bash", # Korn shell — close enough to bash for tree-sitter-bash (#235) + ".ex": "elixir", + ".exs": "elixir", + ".ipynb": "notebook", + ".zig": "zig", + ".ps1": "powershell", + ".psm1": "powershell", + ".psd1": "powershell", + ".svelte": "svelte", + ".jl": "julia", + # ReScript: .res is implementation, .resi is interface. Both share one + # language label; the parser flags interface files via extra metadata. + # No tree-sitter grammar is bundled in tree_sitter_language_pack, so + # extraction is regex-based (see _parse_rescript). + ".res": "rescript", + ".resi": "rescript", + ".gd": "gdscript", + ".nix": "nix", + # SystemVerilog/Verilog + ".sv": "verilog", + ".svh": "verilog", + ".v": "verilog", + ".vh": "verilog", + ".sql": "sql", +} + +# Shebang interpreter → language mapping for extension-less Unix scripts. +# Each key is the **basename** of the interpreter path as it appears after +# ``#!`` (or after ``#!/usr/bin/env``). Only languages already registered +# above are listed — this file strictly routes extension-less scripts, it +# does NOT introduce new languages on its own. See issue #237. +SHEBANG_INTERPRETER_TO_LANGUAGE: dict[str, str] = { + # POSIX / bash-compatible shells — all routed through tree-sitter-bash + "bash": "bash", + "sh": "bash", + "zsh": "bash", + "ksh": "bash", + "dash": "bash", + "ash": "bash", + # Python (every common variant) + "python": "python", + "python2": "python", + "python3": "python", + "pypy": "python", + "pypy3": "python", + # JavaScript via Node + "node": "javascript", + "nodejs": "javascript", + # Ruby / Perl / Lua / R / PHP + "ruby": "ruby", + "perl": "perl", + "lua": "lua", + "Rscript": "r", + "php": "php", +} + +# Maximum bytes to read from the head of a file when probing for a shebang. +# 256 is enough for any reasonable shebang line (``#!/usr/bin/env python3 -u\n`` +# is ~30 chars) while keeping the worst-case read tiny even on fat binaries. +_SHEBANG_PROBE_BYTES = 256 + +# Tree-sitter node type mappings per language +# Maps (language) -> dict of semantic role -> list of TS node types +_CLASS_TYPES: dict[str, list[str]] = { + "python": ["class_definition"], + "javascript": ["class_declaration", "class"], + "typescript": ["class_declaration", "class"], + "tsx": ["class_declaration", "class"], + "go": ["type_declaration"], + "rust": ["struct_item", "enum_item", "impl_item"], + "java": ["class_declaration", "interface_declaration", "enum_declaration"], + "c": ["struct_specifier", "type_definition"], + "cpp": ["class_specifier", "struct_specifier"], + "csharp": [ + "class_declaration", "interface_declaration", + "enum_declaration", "struct_declaration", + ], + "ruby": ["class", "module"], + "r": [], # Classes detected via call pattern-matching, not AST node types + "perl": ["package_statement", "class_statement", "role_statement"], + "kotlin": ["class_declaration", "object_declaration"], + "swift": ["class_declaration", "struct_declaration", "protocol_declaration"], + "php": ["class_declaration", "interface_declaration"], + "scala": [ + "class_definition", "trait_definition", "object_definition", "enum_definition", + ], + "solidity": [ + "contract_declaration", "interface_declaration", "library_declaration", + "struct_declaration", "enum_declaration", "error_declaration", + "user_defined_type_definition", + ], + "dart": ["class_definition", "mixin_declaration", "enum_declaration"], + "lua": [], # Lua has no class keyword; table-based OOP handled via constructs handler + "luau": ["type_definition"], # Luau type aliases; table-based OOP via constructs handler + "objc": [ + "class_interface", "class_implementation", + "category_interface", "protocol_declaration", + ], + "bash": [], # Shell has no classes + # Elixir: `defmodule Name do ... end` is a ``call`` node whose first + # identifier is literally "defmodule". Dispatched via + # _extract_elixir_constructs to avoid matching every ``call`` here. + "elixir": [], + # Nix: attrset bindings aren't "classes"; dispatched via + # _extract_nix_constructs. + "nix": [], + "zig": ["container_declaration"], + "powershell": ["class_statement"], + "julia": [ + "struct_definition", "abstract_definition", "module_definition", + ], + "verilog": ["module_declaration", "interface_declaration", "class_declaration"], + # GDScript: inner classes use ``class Name:`` (class_definition); the + # file-level ``class_name Name`` gives the script itself an identity. + "gdscript": ["class_definition", "class_name_statement"], + # SQL: CREATE TABLE / CREATE VIEW are handled via _parse_sql dispatch. + "sql": [], +} + +_FUNCTION_TYPES: dict[str, list[str]] = { + "python": ["function_definition"], + "javascript": ["function_declaration", "method_definition", "arrow_function"], + "typescript": ["function_declaration", "method_definition", "arrow_function"], + "tsx": ["function_declaration", "method_definition", "arrow_function"], + "go": ["function_declaration", "method_declaration"], + "rust": ["function_item"], + "java": ["method_declaration", "constructor_declaration"], + "c": ["function_definition"], + "cpp": ["function_definition"], + "csharp": ["method_declaration", "constructor_declaration"], + "ruby": ["method", "singleton_method"], + "r": ["function_definition"], + "perl": ["subroutine_declaration_statement", "method_declaration_statement"], + "kotlin": ["function_declaration"], + "swift": ["function_declaration"], + "php": ["function_definition", "method_declaration"], + "scala": ["function_definition", "function_declaration"], + # Solidity: events and modifiers use kind="Function" because the graph + # schema has no dedicated kind for them. State variables are also modeled + # as Function nodes (public ones auto-generate getters) and distinguished + # via extra["solidity_kind"]. + "solidity": [ + "function_definition", "constructor_definition", "modifier_definition", + "event_definition", "fallback_receive_definition", + ], + # Dart: function_signature covers both top-level functions and class methods + # (class methods appear as method_signature > function_signature pairs; + # the parser recurses into method_signature generically and then matches + # function_signature inside it). + "dart": ["function_signature"], + "lua": ["function_declaration"], + "luau": ["function_declaration"], + # Objective-C: method_definition lives inside implementation_definition + # inside class_implementation. C-style function_definition is also present + # for main() and helper functions. + "objc": ["method_definition", "function_definition"], + # Bash: only function_definition; everything else is a command. + "bash": ["function_definition"], + # Elixir: def/defp/defmacro are all ``call`` nodes whose first + # identifier matches. Dispatched via _extract_elixir_constructs. + "elixir": [], + # Nix: `attrpath = expr;` bindings become Function nodes — + # handled in _extract_nix_constructs. + "nix": [], + "zig": ["fn_proto", "fn_decl"], + "powershell": ["function_statement"], + # Julia: short-form functions `f(x) = expr` parse as `assignment` nodes + # (not a dedicated definition node) and are handled in + # _extract_julia_constructs. + "julia": [ + "function_definition", + "macro_definition", + ], + "verilog": ["task_declaration", "function_declaration", "always_construct"], + # GDScript: ``func name(args) -> ReturnType:`` — includes ``static func``. + "gdscript": ["function_definition"], + # SQL: CREATE FUNCTION / CREATE PROCEDURE handled via _parse_sql dispatch. + "sql": [], +} + +_IMPORT_TYPES: dict[str, list[str]] = { + "python": ["import_statement", "import_from_statement"], + "javascript": ["import_statement"], + "typescript": ["import_statement"], + "tsx": ["import_statement"], + "go": ["import_declaration"], + "rust": ["use_declaration"], + "java": ["import_declaration"], + "c": ["preproc_include"], + "cpp": ["preproc_include"], + "csharp": ["using_directive"], + "ruby": ["call"], # require/require_relative + "r": ["call"], # library(), require(), source() — filtered downstream + "perl": ["use_statement", "require_expression"], + "kotlin": ["import_header"], + "swift": ["import_declaration"], + "php": ["namespace_use_declaration"], + "scala": ["import_declaration"], + "solidity": ["import_directive"], + # Dart: import_or_export wraps library_import > import_specification > configurable_uri + "dart": ["import_or_export"], + # Lua/Luau: require() is a function_call, handled via _extract_lua_constructs + "lua": [], + "luau": [], + # Objective-C: #import "..." and #include "..." both arrive as preproc_include + # (tree-sitter-objc doesn't distinguish via a separate preproc_import node). + "objc": ["preproc_include"], + # Bash: source / . is a command — handled in _extract_bash_source below. + "bash": [], + # Elixir: alias/import/require/use are all ``call`` nodes — + # handled in _extract_elixir_constructs. + "elixir": [], + # Nix: `import ./x.nix`, `callPackage ./y.nix {}`, and flake + # `inputs.*.url` strings become IMPORTS_FROM edges — + # handled in _extract_nix_constructs. + "nix": [], + # Zig: @import("...") is a builtin_call_expr — handled + # generically via call types below. + "zig": [], + "powershell": [], + # Julia: import/using are import_statement nodes. + "julia": ["import_statement", "using_statement"], + "verilog": ["package_import_declaration"], + # GDScript has no ``import`` keyword. The closest analogue is + # ``extends OtherClass`` / ``extends "res://path.gd"``, which establishes + # a hard dependency on the parent script. preload()/load() calls remain + # as ordinary CALLS edges. + "gdscript": ["extends_statement"], + # SQL: table references extracted as IMPORTS_FROM via _parse_sql dispatch. + "sql": [], +} + +_CALL_TYPES: dict[str, list[str]] = { + "python": ["call"], + "javascript": ["call_expression", "new_expression"], + "typescript": ["call_expression", "new_expression"], + "tsx": ["call_expression", "new_expression"], + "go": ["call_expression"], + "rust": ["call_expression", "macro_invocation"], + "java": ["method_invocation", "object_creation_expression"], + "c": ["call_expression"], + "cpp": ["call_expression"], + "csharp": ["invocation_expression", "object_creation_expression"], + "ruby": ["call", "method_call"], + "r": ["call"], + "perl": [ + "function_call_expression", "method_call_expression", + "ambiguous_function_call_expression", + ], + "kotlin": ["call_expression"], + "swift": ["call_expression"], + "php": [ + "function_call_expression", + "member_call_expression", + "scoped_call_expression", + "nullsafe_member_call_expression", + ], + "scala": ["call_expression", "instance_expression", "generic_function"], + "solidity": ["call_expression"], + "lua": ["function_call"], + "luau": ["function_call"], + # Objective-C: [receiver message:args] produces message_expression; + # C-style foo(x) produces call_expression. + "objc": ["message_expression", "call_expression"], + # Bash: every command invocation is a "command" node. + "bash": ["command"], + # Elixir: everything is a ``call`` node — dispatched via + # _extract_elixir_constructs which filters out def/defmodule/alias/etc. + # before treating what's left as a real call. + "elixir": [], + # Nix: function application is ubiquitous; only import/callPackage + # produce edges, in _extract_nix_constructs. + "nix": [], + "zig": ["call_expression", "builtin_call_expr"], + "powershell": ["command_expression"], + "julia": [ + "call_expression", + "broadcast_call_expression", + "macrocall_expression", + ], + "verilog": [ + "module_instantiation", "function_subroutine_call", "subroutine_call", "system_tf_call" + ], + # GDScript: bare calls produce ``call``; ``obj.method()`` is an + # ``attribute`` node whose right-hand side is an ``attribute_call``. + "gdscript": ["call", "attribute_call"], + # SQL: no call edges extracted (grammar too unreliable for procedure calls). + "sql": [], +} + + +def _builtin_language_names() -> frozenset[str]: + """All built-in language identifiers. + + Used to stop config-driven custom languages (languages.toml) from + shadowing a built-in language name — built-ins always win. + """ + return ( + frozenset(EXTENSION_TO_LANGUAGE.values()) + | frozenset(_CLASS_TYPES) + | frozenset(_FUNCTION_TYPES) + | frozenset(_IMPORT_TYPES) + | frozenset(_CALL_TYPES) + ) + + +# Patterns that indicate a test function +_TEST_PATTERNS = [ + re.compile(r"^test_"), + re.compile(r"^Test"), + re.compile(r"_test$"), + re.compile(r"\.test\."), + re.compile(r"\.spec\."), + re.compile(r"_spec$"), +] + +_TEST_FILE_PATTERNS = [ + re.compile(r"test_.*\.py$"), + re.compile(r".*_test\.py$"), + re.compile(r".*\.test\.[jt]sx?$"), + re.compile(r".*\.spec\.[jt]sx?$"), + re.compile(r".*_test\.go$"), + re.compile(r"tests?/"), + re.compile(r"[\\/]__tests__[\\/]"), + re.compile(r".*_test\.dart$"), + re.compile(r"test[_-].*\.[rR]$"), + re.compile(r"tests/testthat/"), + re.compile(r".*Test\.kt$"), + re.compile(r".*Test\.java$"), + re.compile(r".*_test\.resi?$"), + re.compile(r".*\.test\.resi?$"), + re.compile(r"test/runtests\.jl$"), + re.compile(r"test/.*\.jl$"), +] + +_TEST_RUNNER_NAMES = frozenset({ + "describe", "it", "test", "beforeEach", "afterEach", + "beforeAll", "afterAll", + # Mocha TDD interface: `suite` is the describe-equivalent. + # `test`, the it-equivalent, is already covered above. + "suite", +}) + +# Annotations/decorators that mark test methods (JUnit, TestNG, etc.) +_TEST_ANNOTATIONS = frozenset({ + "Test", "ParameterizedTest", "RepeatedTest", "TestFactory", + "org.junit.Test", "org.junit.jupiter.api.Test", + # Rust: built-in `#[test]` plus common async-runtime + framework + # variants. Stripped of the `#[ ]` wrapper before lookup. + "test", "tokio::test", "async_std::test", + "rstest", "rstest::rstest", "proptest", +}) + +# Spring stereotype annotations that mark classes as managed beans +_SPRING_STEREOTYPE_ANNOTATIONS = frozenset({ + "Component", "Service", "Repository", "Controller", "RestController", + "Configuration", "Indexed", "ControllerAdvice", "RestControllerAdvice", + "EventListener", +}) + +# Spring DI injection annotations (field/setter/constructor-level) +_SPRING_INJECT_ANNOTATIONS = frozenset({ + "Autowired", "Inject", "Resource", +}) + +# Lombok annotations that trigger constructor injection of final fields +_LOMBOK_CONSTRUCTOR_ANNOTATIONS = frozenset({ + "RequiredArgsConstructor", "AllArgsConstructor", +}) + +# Temporal workflow/activity interface markers +_TEMPORAL_INTERFACE_ANNOTATIONS = frozenset({ + "WorkflowInterface", "ActivityInterface", +}) + +# Temporal method-level markers +_TEMPORAL_METHOD_ANNOTATIONS = frozenset({ + "WorkflowMethod", "ActivityMethod", "SignalMethod", "QueryMethod", +}) + +# Kafka consumer annotations (annotation-based pattern) +_KAFKA_LISTENER_ANNOTATIONS = frozenset({"KafkaListener", "KafkaHandler"}) + +# Kafka consumer field types (reactive / imperative) +_KAFKA_CONSUMER_TYPES = frozenset({ + "KafkaReceiver", + "ReactiveKafkaConsumerTemplate", + "MessageListenerContainer", + "ConcurrentMessageListenerContainer", +}) + +# Kafka producer field types +_KAFKA_PRODUCER_TYPES = frozenset({ + "KafkaTemplate", + "KafkaOperations", + "ReactiveKafkaProducerTemplate", + "KafkaSender", +}) + + +# --------------------------------------------------------------------------- +# ReScript regex patterns and helpers (no tree-sitter grammar bundled) +# --------------------------------------------------------------------------- + +_RESCRIPT_IDENT = r"[A-Za-z_][A-Za-z0-9_']*" + +# `module Name =`, `module type Name =`, `module Name: {`, `module Name: (Sig) => {` +_RESCRIPT_MODULE_RE = re.compile( + r"^\s*module\s+(?:type\s+)?([A-Z][A-Za-z0-9_']*)\s*[:=]", + re.MULTILINE, +) + +# Optional leading decorator block on the same line, e.g. `@deriving(foo)`. +_RESCRIPT_DECORATOR_PREFIX = r"(?:@[A-Za-z_][A-Za-z0-9_']*(?:\([^)]*\))?\s+)*" + +# `let [rec] name` / `and name` — captures binding name. Multi-line decorators +# on prior lines don't interfere (they end with a newline and the anchor +# restarts on the next line); same-line decorators are tolerated. +_RESCRIPT_LET_RE = re.compile( + rf"^\s*{_RESCRIPT_DECORATOR_PREFIX}" + rf"(?:let\s+(?:rec\s+)?|and\s+)({_RESCRIPT_IDENT})\b", + re.MULTILINE, +) + +# `external name: sig = "..."` +_RESCRIPT_EXTERNAL_RE = re.compile( + rf"^\s*{_RESCRIPT_DECORATOR_PREFIX}external\s+({_RESCRIPT_IDENT})\s*:", + re.MULTILINE, +) + +# `type name` / `type rec name` / `type name<'a>` +_RESCRIPT_TYPE_RE = re.compile( + rf"^\s*{_RESCRIPT_DECORATOR_PREFIX}type\s+(?:rec\s+)?({_RESCRIPT_IDENT})\b", + re.MULTILINE, +) + +# `open Foo` / `include Foo.Bar` +_RESCRIPT_OPEN_RE = re.compile( + r"^\s*(open|include)\s+([A-Z][A-Za-z0-9_'.]*)", + re.MULTILINE, +) + +# `module X = Foo.Bar` with no `{` body — a module alias/re-export. Distinct +# from `module X = { ... }` (handled by _RESCRIPT_MODULE_RE + brace scan). +_RESCRIPT_MODULE_ALIAS_RE = re.compile( + r"^\s*module\s+([A-Z][A-Za-z0-9_']*)\s*=\s*" + r"([A-Z][A-Za-z0-9_']*(?:\.[A-Za-z_][A-Za-z0-9_']*)*)\s*$", + re.MULTILINE, +) + +# JSX opening tag: ``, `<=`, `<-`, or a generic-type +# parameter (we approximate by requiring the char before `<` to be space, +# newline, `{`, `(`, `,`, `>`, `}`, or BOF). +_RESCRIPT_JSX_RE = re.compile( + r"(?:^|(?<=[\s{(,>}]))" + r"<([A-Z][A-Za-z0-9_']*(?:\.[A-Z][A-Za-z0-9_']*)*)\b", + re.MULTILINE, +) + +# `@module("path")` — source module for an external binding +_RESCRIPT_MODULE_ATTR_RE = re.compile( + r'@module\(\s*"([^"]+)"\s*\)', +) + +# `Ident(`, `Mod.fn(` — anything that looks like a call site. Preceded by a +# non-identifier char to avoid matching suffixes of identifiers. +_RESCRIPT_CALL_RE = re.compile( + rf"(? str: + """Replace ReScript comments and string/backtick content with spaces. + + Newlines are preserved so absolute offsets still map back to accurate + line numbers. ReScript block comments may nest, so we track depth. + """ + out: list[str] = [] + i = 0 + n = len(text) + while i < n: + c = text[i] + nxt = text[i + 1] if i + 1 < n else "" + # Line comment + if c == "/" and nxt == "/": + while i < n and text[i] != "\n": + out.append(" ") + i += 1 + continue + # Nestable block comment + if c == "/" and nxt == "*": + depth = 1 + out.append(" ") + i += 2 + while i < n and depth > 0: + if i + 1 < n and text[i] == "/" and text[i + 1] == "*": + depth += 1 + out.append(" ") + i += 2 + elif i + 1 < n and text[i] == "*" and text[i + 1] == "/": + depth -= 1 + out.append(" ") + i += 2 + else: + out.append("\n" if text[i] == "\n" else " ") + i += 1 + continue + # Double-quoted string — blank content, keep quotes + newlines. + if c == '"': + out.append('"') + i += 1 + while i < n and text[i] != '"': + if text[i] == "\\" and i + 1 < n: + out.append(" ") + i += 2 + continue + out.append("\n" if text[i] == "\n" else " ") + i += 1 + if i < n: + out.append('"') + i += 1 + continue + # Backtick template string — blank content, preserve newlines. + if c == "`": + out.append("`") + i += 1 + while i < n and text[i] != "`": + out.append("\n" if text[i] == "\n" else " ") + i += 1 + if i < n: + out.append("`") + i += 1 + continue + out.append(c) + i += 1 + return "".join(out) + + +def _rescript_brace_depth_array(cleaned: str) -> list[int]: + """Compute brace depth at every offset in `cleaned` (comment/string-stripped). + + Returned array has length len(cleaned); `depth[i]` is the depth + immediately before the character at position i. + """ + depth = [0] * (len(cleaned) + 1) + d = 0 + for i, c in enumerate(cleaned): + depth[i] = d + if c == "{": + d += 1 + elif c == "}": + d = max(0, d - 1) + depth[len(cleaned)] = d + return depth + + +def _scan_rescript_modules(cleaned: str, offset_to_line) -> list[dict]: + """Find `module Name = { ... }` blocks and their offset/line ranges. + + Returns dicts with name, start/end offsets, start/end lines, and parent + module name (or None for top-level). + """ + modules: list[dict] = [] + n = len(cleaned) + # Module aliases (`module X = Foo.Bar`) also match _RESCRIPT_MODULE_RE but + # have no brace body — skip them here to avoid the greedy `{`-scanner + # swallowing the next unrelated block (e.g. a `let` body). + alias_starts = { + m.start() for m in _RESCRIPT_MODULE_ALIAS_RE.finditer(cleaned) + } + for match in _RESCRIPT_MODULE_RE.finditer(cleaned): + if match.start() in alias_starts: + continue + name = match.group(1) + header_start = match.start() + # Find the first `{` after the header's `:` or `=`. To avoid grabbing + # a `{` from an unrelated following statement, require that the chars + # between `match.end()` and `brace_open` contain no definition-starting + # keywords (`let`, `type`, `module`, `external`). + brace_open = cleaned.find("{", match.end()) + if brace_open == -1: + continue + between = cleaned[match.end():brace_open] + if re.search( + r"(?:^|\s)(?:let|type|module|external|and)\s", + between, + ): + continue + # Walk braces to find the matching close. + depth = 1 + j = brace_open + 1 + while j < n and depth > 0: + c = cleaned[j] + if c == "{": + depth += 1 + elif c == "}": + depth -= 1 + j += 1 + brace_close = j - 1 if depth == 0 else n - 1 + modules.append({ + "name": name, + "start_off": header_start, + "end_off": brace_close, + "body_start_off": brace_open + 1, + "start_line": offset_to_line(header_start), + "end_line": offset_to_line(brace_close), + "parent": None, + }) + + # Parent = innermost strictly-containing module. + for i, m in enumerate(modules): + parent_name = None + parent_start = -1 + for j, other in enumerate(modules): + if i == j: + continue + if ( + other["start_off"] < m["start_off"] + and other["end_off"] > m["end_off"] + and other["start_off"] > parent_start + ): + parent_name = other["name"] + parent_start = other["start_off"] + m["parent"] = parent_name + return modules + + +def _is_test_file(path: str) -> bool: + return any(p.search(path) for p in _TEST_FILE_PATTERNS) + + +def _is_test_function( + name: str, file_path: str, decorators: tuple[str, ...] = (), +) -> bool: + """A function is a test if its name matches test patterns, it lives + in a test file and has a test-runner name, or it has a @Test annotation. + """ + if any(p.search(name) for p in _TEST_PATTERNS): + return True + if _is_test_file(file_path) and name in _TEST_RUNNER_NAMES: + return True + if decorators and any(d in _TEST_ANNOTATIONS for d in decorators): + return True + return False + + +def file_hash(path: Path) -> str: + """SHA-256 hash of file contents.""" + return hashlib.sha256(path.read_bytes()).hexdigest() + + +# --------------------------------------------------------------------------- +# Parser +# --------------------------------------------------------------------------- + + +class CodeParser: + """Parses source files using Tree-sitter and extracts structural information.""" + + _MODULE_CACHE_MAX = 15_000 # Evict cache to cap memory on huge monorepos + + def __init__(self, repo_root: Optional[Path] = None) -> None: + self._parsers: dict[str, object] = {} + self._module_file_cache: dict[str, Optional[str]] = {} + self._export_symbol_cache: dict[str, Optional[str]] = {} + self._tsconfig_resolver = TsconfigResolver() + # Per-parse cache of Dart pubspec root lookups; see #87 + self._dart_pubspec_cache: dict[tuple[str, str], Optional[Path]] = {} + # Config-driven custom languages (.code-review-graph/languages.toml). + # The built-in tables stay shared module-level constants; only when a + # repo defines custom languages does this parser switch to merged + # copies, so other CodeParser instances (multi-repo registry, worker + # processes for other repos) are never affected. See #320. + self._extension_map: dict[str, str] = EXTENSION_TO_LANGUAGE + self._class_types: dict[str, list[str]] = _CLASS_TYPES + self._function_types: dict[str, list[str]] = _FUNCTION_TYPES + self._import_types: dict[str, list[str]] = _IMPORT_TYPES + self._call_types: dict[str, list[str]] = _CALL_TYPES + self._custom_languages: dict[str, CustomLanguage] = {} + if repo_root is not None: + self._custom_languages = load_custom_languages( + Path(repo_root), + builtin_extensions=EXTENSION_TO_LANGUAGE, + builtin_languages=_builtin_language_names(), + ) + if self._custom_languages: + self._extension_map = dict(EXTENSION_TO_LANGUAGE) + self._class_types = dict(_CLASS_TYPES) + self._function_types = dict(_FUNCTION_TYPES) + self._import_types = dict(_IMPORT_TYPES) + self._call_types = dict(_CALL_TYPES) + for custom in self._custom_languages.values(): + for ext in custom.extensions: + self._extension_map[ext] = custom.name + self._class_types[custom.name] = list(custom.class_node_types) + self._function_types[custom.name] = list(custom.function_node_types) + self._import_types[custom.name] = list(custom.import_node_types) + self._call_types[custom.name] = list(custom.call_node_types) + + def _get_parser(self, language: str): # type: ignore[arg-type] + if language not in self._parsers: + # Custom languages map their name onto a packaged grammar. + custom = self._custom_languages.get(language) + grammar = custom.grammar if custom is not None else language + try: + self._parsers[language] = tslp.get_parser(grammar) # type: ignore[arg-type] + except (LookupError, ValueError, ImportError) as exc: + # language not packaged, or grammar load failed + logger.debug("tree-sitter parser unavailable for %s: %s", language, exc) + return None + return self._parsers[language] + + def detect_language(self, path: Path) -> Optional[str]: + """Map a file path to its language name. + + Extension-based lookup is tried first. For extension-less files + (typical for Unix scripts like ``bin/myapp`` or ``.git/hooks/pre-commit``) + we fall back to reading the first line for a shebang. Files that + already have a known extension are never re-read — shebang probing + only runs when the extension lookup returns ``None`` **and** the path + has no suffix at all. See issue #237. + """ + suffix = path.suffix.lower() + lang = self._extension_map.get(suffix) + if lang is not None: + return lang + # Only probe shebang for files without any extension — "README", "LICENSE", + # and other extension-less text files also fall here, but the probe is a + # cheap 256-byte read that returns None when no shebang is found. + if suffix == "": + return self._detect_language_from_shebang(path) + return None + + @staticmethod + def _detect_language_from_shebang(path: Path) -> Optional[str]: + """Inspect the first line of ``path`` for a shebang interpreter. + + Returns the mapped language name or ``None`` if the file has no + shebang, is unreadable, or names an interpreter we don't map. + + Accepted shapes:: + + #!/bin/bash + #!/usr/bin/env python3 + #!/usr/bin/env -S node --experimental-vm-modules + #!/usr/bin/bash -e + + Only the basename of the interpreter is consulted. Trailing flags + after the interpreter are ignored. Windows-style ``\r\n`` line + endings are handled. Binary files read as garbage bytes simply + fail the ``#!`` prefix check and return ``None``. + """ + try: + with path.open("rb") as fh: + head = fh.read(_SHEBANG_PROBE_BYTES) + except (OSError, PermissionError): + return None + if not head.startswith(b"#!"): + return None + + # Take just the first line, stripped of leading "#!" and any + # surrounding whitespace. Split on NUL to defend against accidental + # binary content following a ``#!`` prefix. + first_line = head.split(b"\n", 1)[0].split(b"\0", 1)[0] + try: + line = first_line[2:].decode("utf-8", errors="strict").strip() + except UnicodeDecodeError: + return None + if not line: + return None + + tokens = line.split() + if not tokens: + return None + + first = tokens[0] + # `/usr/bin/env` indirection: the interpreter is the next token. + # `/usr/bin/env -S node --flag` is also valid — skip any leading + # ``-`` options after env. + if first.endswith("/env") or first == "env": + interpreter_token: Optional[str] = None + for tok in tokens[1:]: + if tok.startswith("-"): + # ``-S`` takes no argument in most envs; skip and continue. + continue + interpreter_token = tok + break + if interpreter_token is None: + return None + interpreter = interpreter_token.rsplit("/", 1)[-1] + else: + # Direct form: ``#!/bin/bash`` or ``#!/usr/local/bin/python3``. + interpreter = first.rsplit("/", 1)[-1] + + return SHEBANG_INTERPRETER_TO_LANGUAGE.get(interpreter) + + def parse_file(self, path: Path) -> tuple[list[NodeInfo], list[EdgeInfo]]: + """Parse a single file and return extracted nodes and edges.""" + try: + source = path.read_bytes() + except (OSError, PermissionError): + return [], [] + return self.parse_bytes(path, source) + + def parse_bytes(self, path: Path, source: bytes) -> tuple[list[NodeInfo], list[EdgeInfo]]: + """Parse pre-read bytes and return extracted nodes and edges. + + This avoids re-reading the file from disk, eliminating TOCTOU gaps + when the caller has already read the bytes (e.g. for hashing). + """ + language = self.detect_language(path) + if not language: + return [], [] + + # Vue SFCs: parse with vue parser, then delegate script blocks to JS/TS + if language == "vue": + return self._parse_vue(path, source) + + # Svelte SFCs: same approach as Vue — extract inside JSON to prevent premature tag closure + data_json = json.dumps(agg, default=str).replace(" + + + + +Code Review Graph + + + + + + +
+

Filter by Kind

+ + + + + +
+
+ + + + + + +
+
+ +
+
+ + +
+
+
+
Laying out graph…
+
+
+
+
🔍
+
No nodes to display
+
The graph is empty. Run code-review-graph build to index your codebase, then regenerate the visualization.
+
+ + + + +""" + +# --------------------------------------------------------------------------- +# Aggregated-mode HTML template (community / file) +# --------------------------------------------------------------------------- +# Supports community super-nodes with drill-down (double-click) and a Back +# button to return to the overview. +# NOTE: innerHTML / insertAdjacentHTML usage below mirrors the original +# _HTML_TEMPLATE and is safe because all interpolated values pass through +# escH() which escapes &, <, >, ", ', and backtick characters. + +_AGGREGATED_HTML_TEMPLATE = r""" + + + + +Code Review Graph (Aggregated) + + + + + +
+

View Mode

+
+
+
+ + + +
+
+
+
+
+ + + + + +""" diff --git a/code_review_graph/wiki.py b/code_review_graph/wiki.py new file mode 100644 index 0000000..48360bb --- /dev/null +++ b/code_review_graph/wiki.py @@ -0,0 +1,305 @@ +"""Wiki generation from community structure. + +Generates markdown pages for each detected community and an index page, +providing a navigable documentation wiki for the codebase architecture. +""" + +from __future__ import annotations + +import logging +import re +import sqlite3 +from collections import Counter +from pathlib import Path +from typing import Any + +from .communities import get_communities +from .flows import get_flows +from .graph import GraphStore, _sanitize_name + +logger = logging.getLogger(__name__) + + +def _slugify(name: str) -> str: + """Convert a community name to a safe filename slug.""" + slug = re.sub(r"[^a-z0-9]+", "-", name.lower()).strip("-") + return slug[:80] or "unnamed" + + +def _generate_community_page(store: GraphStore, community: dict[str, Any]) -> str: + """Build markdown content for a single community. + + Includes: heading, overview (size, cohesion, language), members table + (top 50), execution flows through the community, and dependencies. + + Args: + store: The graph store. + community: Community dict from get_communities(). + + Returns: + Markdown string for the community page. + """ + name = community["name"] + size = community["size"] + cohesion = community.get("cohesion", 0.0) + lang = community.get("dominant_language", "") + description = community.get("description", "") + + lines: list[str] = [] + lines.append(f"# {name}") + lines.append("") + + # Overview section + lines.append("## Overview") + lines.append("") + if description: + lines.append(f"{description}") + lines.append("") + lines.append(f"- **Size**: {size} nodes") + lines.append(f"- **Cohesion**: {cohesion:.4f}") + if lang: + lines.append(f"- **Dominant Language**: {lang}") + lines.append("") + + # Members table (top 50) + member_qns = community.get("members", []) + lines.append("## Members") + lines.append("") + if member_qns: + lines.append("| Name | Kind | File | Lines |") + lines.append("|------|------|------|-------|") + + # Fetch node details for members (limit to 50) + member_count = 0 + for qn in member_qns[:50]: + node = store.get_node(qn) + if node and node.kind != "File": + node_name = _sanitize_name(node.name) + lines.append( + f"| {node_name} | {node.kind} | {node.file_path} " + f"| {node.line_start}-{node.line_end} |" + ) + member_count += 1 + + if not member_count: + # Remove the table headers if no members were added + lines.pop() # header separator + lines.pop() # header + lines.append("No non-file members found.") + + if len(member_qns) > 50: + lines.append("") + lines.append(f"*... and {len(member_qns) - 50} more members.*") + else: + lines.append("No members found.") + lines.append("") + + # Execution flows through community + lines.append("## Execution Flows") + lines.append("") + member_set = set(member_qns) + try: + all_flows = get_flows(store, sort_by="criticality", limit=200) + community_flows: list[dict] = [] + for flow in all_flows: + # Check if this flow passes through any community member + flow_qns = store.get_flow_qualified_names(flow["id"]) + if flow_qns & member_set: + community_flows.append(flow) + + if community_flows: + for flow in community_flows[:10]: + flow_name = _sanitize_name(flow.get("name", "unnamed")) + criticality = flow.get("criticality", 0.0) + depth = flow.get("depth", 0) + lines.append( + f"- **{flow_name}** (criticality: {criticality:.2f}, depth: {depth})" + ) + if len(community_flows) > 10: + lines.append(f"- *... and {len(community_flows) - 10} more flows.*") + else: + lines.append("No execution flows pass through this community.") + except sqlite3.OperationalError as exc: + logger.debug("wiki: flows table unavailable: %s", exc) + lines.append("Execution flow data not available.") + lines.append("") + + # Dependencies (cross-community edges) + lines.append("## Dependencies") + lines.append("") + try: + outgoing_targets: Counter[str] = Counter() + incoming_sources: Counter[str] = Counter() + if member_qns: + qns = list(member_qns) + + # Outgoing: source is a member + for t in store.get_outgoing_targets(qns): + if t not in member_set: + outgoing_targets[t] += 1 + + # Incoming: target is a member + for s in store.get_incoming_sources(qns): + if s not in member_set: + incoming_sources[s] += 1 + + if outgoing_targets: + lines.append("### Outgoing") + lines.append("") + for target, count in outgoing_targets.most_common(15): + lines.append(f"- `{_sanitize_name(target)}` ({count} edge(s))") + lines.append("") + + if incoming_sources: + lines.append("### Incoming") + lines.append("") + for source, count in incoming_sources.most_common(15): + lines.append(f"- `{_sanitize_name(source)}` ({count} edge(s))") + lines.append("") + + if not outgoing_targets and not incoming_sources: + lines.append("No cross-community dependencies detected.") + lines.append("") + except sqlite3.OperationalError as exc: + logger.debug("wiki: dependency edges unavailable: %s", exc) + lines.append("Dependency data not available.") + lines.append("") + + return "\n".join(lines) + + +def generate_wiki( + store: GraphStore, + wiki_dir: str | Path, + force: bool = False, +) -> dict[str, Any]: + """Generate a markdown wiki from the community structure. + + For each community, generates a markdown page. Also generates an + index.md with links to all community pages. + + Args: + store: The graph store. + wiki_dir: Directory to write wiki pages into. + force: If True, regenerate all pages even if content unchanged. + + Returns: + Dict with pages_generated, pages_updated, pages_unchanged counts. + """ + wiki_path = Path(wiki_dir) + wiki_path.mkdir(parents=True, exist_ok=True) + + communities = get_communities(store) + + pages_generated = 0 + pages_updated = 0 + pages_unchanged = 0 + + page_entries: list[tuple[str, str, int]] = [] # (slug, name, size) + + # Track slugs we've already used in THIS run so two communities that + # slugify to the same filename don't overwrite each other (#222 follow-up). + # Previously "Data Processing" and "data processing" both became + # "data-processing.md", causing silent data loss and inflated "updated" + # counters (each collision was counted as an update while only one file + # made it to disk). + used_slugs: set[str] = set() + + for comm in communities: + name = comm["name"] + base_slug = _slugify(name) + slug = base_slug + suffix = 2 + while slug in used_slugs: + slug = f"{base_slug}-{suffix}" + suffix += 1 + used_slugs.add(slug) + + filename = f"{slug}.md" + filepath = wiki_path / filename + + content = _generate_community_page(store, comm) + + if filepath.exists() and not force: + existing = filepath.read_text(encoding="utf-8", errors="replace") + if existing == content: + pages_unchanged += 1 + page_entries.append((slug, name, comm["size"])) + continue + + already_existed = filepath.exists() + filepath.write_text(content, encoding="utf-8") + if already_existed: + pages_updated += 1 + else: + pages_generated += 1 + page_entries.append((slug, name, comm["size"])) + + # Generate index.md + index_lines: list[str] = [] + index_lines.append("# Code Wiki") + index_lines.append("") + index_lines.append( + "Auto-generated documentation from the code knowledge graph community structure." + ) + index_lines.append("") + index_lines.append(f"**Total communities**: {len(communities)}") + index_lines.append("") + index_lines.append("## Communities") + index_lines.append("") + index_lines.append("| Community | Size | Link |") + index_lines.append("|-----------|------|------|") + for slug, name, size in sorted(page_entries, key=lambda x: x[1]): + index_lines.append(f"| {name} | {size} | [{slug}.md]({slug}.md) |") + index_lines.append("") + + index_content = "\n".join(index_lines) + index_path = wiki_path / "index.md" + + if index_path.exists() and not force: + existing_index = index_path.read_text(encoding="utf-8", errors="replace") + if existing_index == index_content: + pages_unchanged += 1 + else: + index_path.write_text(index_content, encoding="utf-8") + pages_updated += 1 + else: + index_path.write_text(index_content, encoding="utf-8") + pages_generated += 1 + + return { + "pages_generated": pages_generated, + "pages_updated": pages_updated, + "pages_unchanged": pages_unchanged, + } + + +def get_wiki_page(wiki_dir: str | Path, page_name: str) -> str | None: + """Retrieve a specific wiki page by community name. + + Args: + wiki_dir: Directory containing wiki pages. + page_name: Community name (will be slugified for filename lookup). + + Returns: + Page content as a string, or None if the page does not exist. + """ + wiki_path = Path(wiki_dir) + slug = _slugify(page_name) + filepath = wiki_path / f"{slug}.md" + + if filepath.is_file(): + return filepath.read_text(encoding="utf-8", errors="replace") + + # Fallback: try exact filename match — with path traversal protection + exact_path = (wiki_path / page_name).resolve() + if exact_path.is_file() and exact_path.is_relative_to(wiki_path.resolve()): + return exact_path.read_text(encoding="utf-8", errors="replace") + + # Fallback: search for partial match + if wiki_path.is_dir(): + for p in wiki_path.iterdir(): + if p.suffix == ".md" and slug in p.stem: + return p.read_text(encoding="utf-8", errors="replace") + + return None diff --git a/diagrams/context-savings-demo.gif b/diagrams/context-savings-demo.gif new file mode 100644 index 0000000000000000000000000000000000000000..2651e393d00deaba9db33677aac5432e11673335 GIT binary patch literal 1338859 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 z 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

E{rV@Dd^eq={!r% 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%0AE#{0K@;>?Q2JEY}iAet|ppe;{7u3 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�tAiZf|(g7`-GiFOiz`U>p{oTCCn!I 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

*Z<2zpD_xMcJThN@A@9pQ588JBPM0Z+=1C^ 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

b(+vWT`bmbG*H_5$E+#SE(xMs4hHs<&yafkD> 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�Mmx1!Y`eb3J8?Pq``QvV>z( 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

-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

  • rTW(;R#TX9# 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