From 5457d4dc61a50222330a11b07f76a4199548070d Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Wed, 7 Oct 2026 13:45:49 +0700 Subject: [PATCH 1/3] feat(web): Svelte web UI + name-storage refactor, branch labels, profiler - codegraph-web: embed Svelte SPA (rust-embed, allow_missing) served as GraphQL fallback; assets built by CI/release, not committed - web: SvelteKit SPA (explore/review/documents) + graphql client - graph: persist name engine in a sibling dataset (names.lmdb/sqlite) with version stamp; collapse name_records/sorted_name_keys into one Vec - graph: branch labels in flow, stage profiler (CGPROF) - source: DiskGit trait; api/mcp dispatch diff_simulate/origin_simulate - ci/release: build web assets and wire them into all Rust build jobs; deb/rpm, Homebrew, AUR source builds build web first - bump version to 2.2.7 --- .github/workflows/ci.yml | 49 + .github/workflows/release-packages.yml | 33 + .github/workflows/release.yml | 32 + Cargo.lock | 92 +- Cargo.toml | 4 +- crates/codegraph-api/src/mermaid.rs | 194 +- crates/codegraph-api/src/tools.rs | 123 +- crates/codegraph-core/src/lib.rs | 15 +- crates/codegraph-core/src/prof.rs | 120 + crates/codegraph-core/src/semgraph.rs | 14 + crates/codegraph-extract/src/bingraph.rs | 1 + crates/codegraph-extract/src/docgraph.rs | 161 +- .../codegraph-extract/src/languages/common.rs | 91 +- crates/codegraph-extract/src/lib.rs | 2 +- crates/codegraph-graph/src/lib.rs | 251 +- crates/codegraph-graph/src/memtrack.rs | 11 +- crates/codegraph-graph/src/shared.rs | 58 + crates/codegraph-graphql/Cargo.toml | 2 + crates/codegraph-graphql/src/lib.rs | 179 +- crates/codegraph-graphql/src/mutation.rs | 208 +- crates/codegraph-graphql/src/query.rs | 222 +- crates/codegraph-graphql/src/types.rs | 118 +- crates/codegraph-mcp/src/docgraph.rs | 154 +- crates/codegraph-mcp/src/lib.rs | 7 +- crates/codegraph-source/src/git.rs | 234 ++ crates/codegraph-source/src/lib.rs | 2 + crates/codegraph-web/.gitignore | 5 + crates/codegraph-web/Cargo.toml | 19 + crates/codegraph-web/assets/.gitkeep | 0 crates/codegraph-web/src/lib.rs | 131 + crates/codegraph/src/main.rs | 16 + docs/development.md | 8 + packaging/aur/codegraph-rs-bin/PKGBUILD | 2 +- packaging/aur/codegraph-rs-git/PKGBUILD | 6 +- packaging/choco/codegraph.nuspec | 2 +- packaging/homebrew/codegraph.rb.template | 5 + packaging/winget/codegraph.yaml | 4 +- scripts/build-web.sh | 24 + scripts/install.ps1 | 4 +- web/.gitignore | 23 + web/.npmrc | 1 + web/README.md | 45 + web/package-lock.json | 3226 +++++++++++++++++ web/package.json | 37 + web/src/app.css | 34 + web/src/app.d.ts | 13 + web/src/app.html | 12 + web/src/lib/assets/favicon.svg | 1 + web/src/lib/components/ConnectPanel.svelte | 53 + web/src/lib/components/DiffView.svelte | 24 + web/src/lib/components/FileBrowser.svelte | 89 + web/src/lib/components/MermaidView.svelte | 63 + .../lib/components/RelationshipList.svelte | 37 + web/src/lib/components/Sidebar.svelte | 59 + web/src/lib/components/SymbolDetail.svelte | 354 ++ web/src/lib/components/SymbolList.svelte | 132 + web/src/lib/components/SymbolSearch.svelte | 125 + web/src/lib/components/TopBar.svelte | 89 + web/src/lib/format.ts | 45 + web/src/lib/graphql/client.ts | 36 + web/src/lib/graphql/queries.ts | 370 ++ web/src/lib/index.ts | 1 + web/src/lib/markdown.ts | 8 + web/src/lib/settings.ts | 31 + web/src/lib/workspace.svelte.ts | 66 + web/src/routes/+layout.svelte | 24 + web/src/routes/+layout.ts | 4 + web/src/routes/+page.svelte | 101 + web/src/routes/documents/+page.svelte | 293 ++ web/src/routes/review/+page.svelte | 308 ++ web/static/robots.txt | 3 + web/tsconfig.json | 12 + web/vite.config.ts | 47 + 73 files changed, 7942 insertions(+), 427 deletions(-) create mode 100644 crates/codegraph-core/src/prof.rs create mode 100644 crates/codegraph-source/src/git.rs create mode 100644 crates/codegraph-web/.gitignore create mode 100644 crates/codegraph-web/Cargo.toml create mode 100644 crates/codegraph-web/assets/.gitkeep create mode 100644 crates/codegraph-web/src/lib.rs create mode 100755 scripts/build-web.sh create mode 100644 web/.gitignore create mode 100644 web/.npmrc create mode 100644 web/README.md create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/src/app.css create mode 100644 web/src/app.d.ts create mode 100644 web/src/app.html create mode 100644 web/src/lib/assets/favicon.svg create mode 100644 web/src/lib/components/ConnectPanel.svelte create mode 100644 web/src/lib/components/DiffView.svelte create mode 100644 web/src/lib/components/FileBrowser.svelte create mode 100644 web/src/lib/components/MermaidView.svelte create mode 100644 web/src/lib/components/RelationshipList.svelte create mode 100644 web/src/lib/components/Sidebar.svelte create mode 100644 web/src/lib/components/SymbolDetail.svelte create mode 100644 web/src/lib/components/SymbolList.svelte create mode 100644 web/src/lib/components/SymbolSearch.svelte create mode 100644 web/src/lib/components/TopBar.svelte create mode 100644 web/src/lib/format.ts create mode 100644 web/src/lib/graphql/client.ts create mode 100644 web/src/lib/graphql/queries.ts create mode 100644 web/src/lib/index.ts create mode 100644 web/src/lib/markdown.ts create mode 100644 web/src/lib/settings.ts create mode 100644 web/src/lib/workspace.svelte.ts create mode 100644 web/src/routes/+layout.svelte create mode 100644 web/src/routes/+layout.ts create mode 100644 web/src/routes/+page.svelte create mode 100644 web/src/routes/documents/+page.svelte create mode 100644 web/src/routes/review/+page.svelte create mode 100644 web/static/robots.txt create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13098893fd..3d2fcca22b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,8 +14,33 @@ env: RUSTFLAGS: -D warnings jobs: + # Build the Svelte UI once; every Rust job below downloads these assets so + # `rust-embed` embeds the real web UI (parity with the release build). + web: + name: web (svelte assets) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - name: Install + check + build + run: | + npm --prefix web ci --no-audit --no-fund + npm --prefix web run check + npm --prefix web run build + - name: Upload built assets (for embedding) + uses: actions/upload-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ + if-no-files-found: error + clippy: name: clippy + needs: web runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -23,11 +48,16 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - run: cargo clippy --workspace --all-targets -- -D warnings - run: cargo clippy -p codegraph-graph --features postgres,mysql,redis --tests -- -D warnings clippy-windows: name: clippy (windows) + needs: web runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -36,10 +66,15 @@ jobs: components: clippy targets: x86_64-pc-windows-msvc - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - run: cargo clippy -p codegraph --target x86_64-pc-windows-msvc -- -D warnings test-windows: name: test (windows) + needs: web runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -47,6 +82,10 @@ jobs: with: targets: x86_64-pc-windows-msvc - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - name: Build run: cargo build -p codegraph - name: Build (fastembed feature) @@ -62,6 +101,7 @@ jobs: test: name: test (${{ matrix.os }}) + needs: web runs-on: ${{ matrix.os }} services: redis: @@ -93,6 +133,10 @@ jobs: with: components: llvm-tools-preview - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - name: Install DB clients run: sudo apt-get update && sudo apt-get install -y postgresql-client mysql-client - name: Install radare2 (binary analysis integration tests) @@ -148,6 +192,7 @@ jobs: slim: name: slim build (no visualize) + needs: web runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -155,6 +200,10 @@ jobs: with: components: clippy - uses: Swatinem/rust-cache@v2 + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - run: cargo clippy -p codegraph --no-default-features -- -D warnings - run: cargo build -p codegraph --no-default-features - run: | diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index aac99a5b82..4569d91049 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -15,9 +15,33 @@ permissions: contents: write jobs: + # Build the Svelte UI once; deb/rpm build from source and rust-embed needs the + # built assets present (see crates/codegraph-web). + web: + name: web (svelte assets) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - name: Build web assets + run: | + npm --prefix web ci --no-audit --no-fund + npm --prefix web run build + - name: Upload web assets + uses: actions/upload-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ + if-no-files-found: error + # Build .deb packages for Debian/Ubuntu (and derivatives) from the gnu targets. deb: name: deb (${{ matrix.target }}) + needs: web runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -33,6 +57,10 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - name: Install cross linker (aarch64) if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} run: | @@ -52,6 +80,7 @@ jobs: # Build .rpm packages for Fedora/RHEL (and derivatives) from the gnu targets. rpm: name: rpm (${{ matrix.target }}) + needs: web runs-on: ubuntu-22.04 strategy: fail-fast: false @@ -67,6 +96,10 @@ jobs: uses: dtolnay/rust-toolchain@stable with: targets: ${{ matrix.target }} + - uses: actions/download-artifact@v4 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - name: Install rpm tooling + cross linker (aarch64) if: ${{ matrix.target == 'aarch64-unknown-linux-gnu' }} run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cb59669f83..812514a9fb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,12 +87,39 @@ jobs: name: artifacts-plan-dist-manifest path: plan-dist-manifest.json + # Build the web UI once (platform-independent) → artifact consumed by the + # per-target build jobs so rust-embed can include it in every binary. + build-web: + name: build-web + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: 22 + cache: npm + cache-dependency-path: web/package-lock.json + - name: Build web assets + run: | + npm --prefix web ci --no-audit --no-fund + npm --prefix web run check + npm --prefix web run build + - name: Upload web assets + uses: actions/upload-artifact@v7 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ + if-no-files-found: error + # Build and packages all the platform-specific things build-local-artifacts: name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) # Let the initial task tell us to not run (currently very blunt) needs: - plan + - build-web if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} strategy: fail-fast: false @@ -136,6 +163,11 @@ jobs: pattern: artifacts-* path: target/distrib/ merge-multiple: true + - name: Fetch web assets (for rust-embed) + uses: actions/download-artifact@v8 + with: + name: codegraph-web-assets + path: crates/codegraph-web/assets/ - name: Install dependencies run: | ${{ matrix.packages_install }} diff --git a/Cargo.lock b/Cargo.lock index f8c2d803e1..1f793ca722 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -720,7 +720,7 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "codegraph" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "camino", @@ -743,7 +743,7 @@ dependencies = [ [[package]] name = "codegraph-api" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "camino", @@ -761,7 +761,7 @@ dependencies = [ [[package]] name = "codegraph-bench" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "camino", @@ -780,7 +780,7 @@ dependencies = [ [[package]] name = "codegraph-binary" -version = "2.2.6" +version = "2.2.7" dependencies = [ "camino", "codegraph-core", @@ -797,7 +797,7 @@ dependencies = [ [[package]] name = "codegraph-context" -version = "2.2.6" +version = "2.2.7" dependencies = [ "camino", "codegraph-core", @@ -812,7 +812,7 @@ dependencies = [ [[package]] name = "codegraph-core" -version = "2.2.6" +version = "2.2.7" dependencies = [ "async-graphql", "camino", @@ -823,7 +823,7 @@ dependencies = [ [[package]] name = "codegraph-docs" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "codegraph-core", @@ -841,7 +841,7 @@ dependencies = [ [[package]] name = "codegraph-extract" -version = "2.2.6" +version = "2.2.7" dependencies = [ "camino", "codegraph-binary", @@ -880,7 +880,7 @@ dependencies = [ [[package]] name = "codegraph-graph" -version = "2.2.6" +version = "2.2.7" dependencies = [ "async-trait", "bincode", @@ -910,7 +910,7 @@ dependencies = [ [[package]] name = "codegraph-graphql" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "async-graphql", @@ -921,8 +921,10 @@ dependencies = [ "codegraph-context", "codegraph-core", "codegraph-docs", + "codegraph-extract", "codegraph-graph", "codegraph-source", + "codegraph-web", "serde", "serde_json", "tempfile", @@ -934,7 +936,7 @@ dependencies = [ [[package]] name = "codegraph-installer" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "camino", @@ -950,7 +952,7 @@ dependencies = [ [[package]] name = "codegraph-mcp" -version = "2.2.6" +version = "2.2.7" dependencies = [ "anyhow", "axum", @@ -974,7 +976,7 @@ dependencies = [ [[package]] name = "codegraph-sboxes" -version = "2.2.6" +version = "2.2.7" dependencies = [ "camino", "codegraph-core", @@ -996,7 +998,7 @@ dependencies = [ [[package]] name = "codegraph-source" -version = "2.2.6" +version = "2.2.7" dependencies = [ "async-trait", "camino", @@ -1007,6 +1009,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "codegraph-web" +version = "2.2.7" +dependencies = [ + "axum", + "mime_guess", + "rust-embed", + "tokio", + "tower", +] + [[package]] name = "codspeed" version = "5.0.1" @@ -3164,6 +3177,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -4537,6 +4560,41 @@ dependencies = [ "smallvec", ] +[[package]] +name = "rust-embed" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e7760e252aaba7b09f4be00e36476cf585bdb68a53552ac954cdf504ab4bc9" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bcfc4d6f53af43755f7a723e4b6b8794fcce052a178dd8c6c1dadc5f5343097" +dependencies = [ + "mime_guess", + "proc-macro2", + "quote", + "rust-embed-utils", + "syn 2.0.117", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "8.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ffa149f6aa81b58a5b3011d01a857c4ed12c7a732d2c51947a4c7c692185f0" +dependencies = [ + "sha2 0.11.0", + "walkdir", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -5931,6 +5989,12 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" +[[package]] +name = "unicase" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357cc3acc6a036009fd6c973ed009037c732d60d0b4f6c673e9041497482a28f" + [[package]] name = "unicode-bidi" version = "0.3.18" diff --git a/Cargo.toml b/Cargo.toml index a76e78fa12..e345dfba7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,11 @@ members = [ "crates/codegraph", "crates/codegraph-docs", "crates/codegraph-source", + "crates/codegraph-web", ] [workspace.package] -version = "2.2.6" +version = "2.2.7" edition = "2021" rust-version = "1.80" license = "MIT" @@ -101,6 +102,7 @@ axum = "0.8" tower = "0.5" tower-http = { version = "0.6", features = ["compression-gzip"] } rust-embed = "8" +mime_guess = "2" open = "5" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 7bb351bdab..4a1e529275 100644 --- a/crates/codegraph-api/src/mermaid.rs +++ b/crates/codegraph-api/src/mermaid.rs @@ -27,11 +27,16 @@ use std::collections::{HashMap, HashSet}; /// `LOOP_BACK` vẽ cạnh quay về header; `SWITCH_CASE` toả ra từ node trước switch. pub fn control_flow(flow: &FlowResult) -> String { let calls: HashMap = flow.calls.iter().map(|c| (c.position, c)).collect(); + let labels: HashMap = flow + .branch_labels + .iter() + .map(|b| (b.position, b.label.as_str())) + .collect(); let mut out = String::from("flowchart TD\n"); for (i, &raw) in flow.chain.iter().enumerate() { let desc = flow.chain_desc.get(i).map(String::as_str).unwrap_or(""); - let (open, close, label) = node_style(&flow.chain, i, raw, desc, &calls); + let (open, close, label) = node_style(&flow.chain, i, raw, desc, &calls, &labels); out.push_str(&format!(" c{i}{open}\"{}\"{close}\n", sanitize(&label))); } @@ -52,6 +57,7 @@ fn node_style( raw: u64, desc: &str, calls: &HashMap, + labels: &HashMap, ) -> (&'static str, &'static str, String) { if is_marker(raw) { let name = marker_name(raw).unwrap_or("MARKER"); @@ -61,10 +67,19 @@ fn node_style( | "SWITCH_END" | "RECURSIVE_CALL" => ("([", "])"), _ => ("[", "]"), }; + // Nhãn trigger: ưu tiên `branch_labels` (điều kiện/case persist), rồi + // fallback suy từ guard của call trong nhánh (dữ liệu cũ chưa có label). + let branch = labels + .get(&i) + .map(|s| s.to_string()) + .or_else(|| match name { + "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "LOOP" => guard_for(chain, i, calls), + _ => None, + }); let label = match name { - "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" => match guard_for(chain, i, calls) { - Some(g) => format!("{name}: {g}"), - None => name.to_string(), + "IF_TRUE" | "IF_FALSE" | "SWITCH_CASE" | "LOOP" => match branch { + Some(g) if !g.is_empty() => format!("{name}: {g}"), + _ => name.to_string(), }, _ => name.to_string(), }; @@ -182,6 +197,83 @@ fn structural_edges(chain: &[u64]) -> Vec<(usize, usize)> { edges } +/// Control-flow của `head` với 2 màu so với `base` (branch review): +/// +/// - call/step **chỉ có ở head** (không thấy ở base) → node `added` (xanh lá); +/// - call **chỉ có ở base** (đã bị xoá ở head) → node `removed` (đỏ), nối từ +/// root bằng cạnh nét đứt; +/// - còn lại giữ style mặc định. +/// +/// Dùng để render "branch A vs branch B" trong màn review — thay đổi nổi bật +/// bằng 2 màu, vẫn là giao diện mermaid. +pub fn control_flow_diff(head: &FlowResult, base: &FlowResult) -> String { + let calls: HashMap = head.calls.iter().map(|c| (c.position, c)).collect(); + let labels: HashMap = head + .branch_labels + .iter() + .map(|b| (b.position, b.label.as_str())) + .collect(); + let base_names: HashSet<&str> = base + .calls + .iter() + .map(|c| c.to_name.as_str()) + .filter(|n| !n.is_empty()) + .collect(); + let head_names: HashSet<&str> = head + .calls + .iter() + .map(|c| c.to_name.as_str()) + .filter(|n| !n.is_empty()) + .collect(); + + let mut out = String::from("flowchart TD\n"); + let mut added_idx = Vec::new(); + for (i, &raw) in head.chain.iter().enumerate() { + let desc = head.chain_desc.get(i).map(String::as_str).unwrap_or(""); + let (open, close, label) = node_style(&head.chain, i, raw, desc, &calls, &labels); + out.push_str(&format!(" c{i}{open}\"{}\"{close}\n", sanitize(&label))); + if let Some(c) = calls.get(&i) { + if !c.to_name.is_empty() && !base_names.contains(c.to_name.as_str()) { + added_idx.push(i); + } + } + } + + let mut edges = structural_edges(&head.chain); + edges.sort_unstable(); + edges.dedup(); + for (a, b) in edges { + out.push_str(&format!(" c{a} --> c{b}\n")); + } + + // Call có ở base nhưng không còn ở head → node đỏ nét đứt từ root. + let mut removed_idx = Vec::new(); + let mut k = 0usize; + for c in &base.calls { + if !c.to_name.is_empty() && !head_names.contains(c.to_name.as_str()) { + out.push_str(&format!( + " r{k}[[\"{} · removed\"]]\n", + sanitize(&c.to_name) + )); + out.push_str(&format!(" c0 -.-> r{k}\n")); + removed_idx.push(k); + k += 1; + } + } + + if !added_idx.is_empty() || !removed_idx.is_empty() { + out.push_str(" classDef added fill:#14532d,stroke:#22c55e,color:#dcfce7\n"); + out.push_str(" classDef removed fill:#450a0a,stroke:#ef4444,color:#fecaca\n"); + for i in &added_idx { + out.push_str(&format!(" class c{i} added\n")); + } + for i in &removed_idx { + out.push_str(&format!(" class r{i} removed\n")); + } + } + out +} + /// Call graph (callers + callees) quanh một symbol, BFS tới `depth` hop. pub async fn call_graph(api: &GraphApi, start: SymbolId, depth: u32) -> anyhow::Result { let (nodes, edges) = build_call_graph(api, start, depth, None).await?; @@ -312,7 +404,7 @@ fn sanitize(s: &str) -> String { #[cfg(test)] mod tests { use super::*; - use codegraph_core::{ScopeLevel, Symbol, SymbolKind}; + use codegraph_core::{BranchLabel, ScopeLevel, Symbol, SymbolKind}; fn flow(chain: Vec, desc: Vec<&str>, calls: Vec) -> FlowResult { FlowResult { @@ -335,9 +427,28 @@ mod tests { chain, chain_desc: desc.into_iter().map(String::from).collect(), calls, + branch_labels: Vec::new(), } } + /// flow kèm branch_labels (position, label). + fn flow_labelled( + chain: Vec, + desc: Vec<&str>, + calls: Vec, + labels: Vec<(usize, &str)>, + ) -> FlowResult { + let mut f = flow(chain, desc, calls); + f.branch_labels = labels + .into_iter() + .map(|(position, label)| BranchLabel { + position, + label: label.into(), + }) + .collect(); + f + } + fn call(pos: usize, name: &str, to_id: Option, line: u32) -> FlowCall { FlowCall { position: pos, @@ -479,6 +590,44 @@ mod tests { assert!(m.contains("c1{\"IF_TRUE: dirty\"}"), "{m}"); } + #[test] + fn branch_labels_override_guard_and_label_switch_and_loop() { + // if + match + while, nhãn persist ở đúng vị trí marker. + let chain = vec![ + 100, + MARKER_IF_TRUE, + 0, + MARKER_BRANCH_END, + MARKER_SWITCH_CASE, + 0, + MARKER_SWITCH_END, + MARKER_LOOP, + 0, + MARKER_LOOP_BACK, + ]; + let f = flow_labelled( + chain, + vec![ + "root", "IF_TRUE", "a", "BRANCH_END", "SWITCH_CASE", "b", "SWITCH_END", "LOOP", "c", + "LOOP_BACK", + ], + vec![ + call(2, "a", Some(1), 2), + call(5, "b", Some(2), 5), + call(8, "c", Some(3), 8), + ], + vec![ + (1, "x > 0"), + (4, "Cmd::Init"), + (7, "i < n"), + ], + ); + let m = control_flow(&f); + assert!(m.contains("c1{\"IF_TRUE: x > 0\"}"), "{m}"); + assert!(m.contains("c4{\"SWITCH_CASE: Cmd::Init\"}"), "{m}"); + assert!(m.contains("c7([\"LOOP: i < n\"])"), "{m}"); + } + #[test] fn root_has_no_incoming_edge() { let chain = vec![100, 0]; @@ -491,4 +640,39 @@ mod tests { assert!(!m.contains("--> c0\n"), "{m}"); assert!(m.contains("c0 --> c1"), "{m}"); } + + #[test] + fn diff_colors_added_and_removed_calls() { + // base: root → a(), b() + let base = flow( + vec![100, 0, 0], + vec!["root", "a", "b"], + vec![call(1, "a", Some(101), 2), call(2, "b", Some(102), 3)], + ); + // head: root → a(), c() (b xoá, c thêm) + let head = flow( + vec![100, 0, 0], + vec!["root", "a", "c"], + vec![call(1, "a", Some(101), 2), call(2, "c", Some(103), 3)], + ); + let m = control_flow_diff(&head, &base); + // c (index 2) là added; b (không còn) là removed node r0. + assert!(m.contains("classDef added"), "{m}"); + assert!(m.contains("class c2 added"), "{m}"); + assert!(m.contains("r0[[\"b · removed\"]]"), "{m}"); + assert!(m.contains("class r0 removed"), "{m}"); + // a không đổi → không được tô. + assert!(!m.contains("class c1 added"), "{m}"); + } + + #[test] + fn diff_without_changes_has_no_classes() { + let f = flow( + vec![100, 0], + vec!["root", "a"], + vec![call(1, "a", Some(101), 2)], + ); + let m = control_flow_diff(&f, &f); + assert!(!m.contains("classDef"), "{m}"); + } } diff --git a/crates/codegraph-api/src/tools.rs b/crates/codegraph-api/src/tools.rs index 06f7bb8f01..0ea6ed0692 100644 --- a/crates/codegraph-api/src/tools.rs +++ b/crates/codegraph-api/src/tools.rs @@ -12,6 +12,7 @@ use codegraph_core::{is_marker, Error, Result, SymbolKind, SymbolMatch}; use codegraph_extract::Orchestrator; use codegraph_graph::{GraphIndex, SharedGraphIndex}; use codegraph_sboxes::{compile_with_mocks, BranchPolicy, SboxConfig}; +use codegraph_source::Git; use serde::Serialize; use serde_json::{json, Value}; use std::sync::Arc; @@ -335,11 +336,11 @@ async fn run_sim( })) } -/// Build index của cây git tại `base_ref` (`git archive` → temp dir → +/// Build index của cây git tại `base_ref` (`git export tree` → temp dir → /// parse+ingest vào `GraphIndex::in_memory`). Luôn trả kèm tmp dir để caller /// dọn dẹp, kể cả khi thất bại (trả `None` + `note` lý do). async fn build_before_index( - root: &Utf8Path, + git: &dyn Git, base_ref: &str, ) -> Result<(Option, Utf8PathBuf, String)> { let millis = std::time::SystemTime::now() @@ -351,35 +352,12 @@ async fn build_before_index( ) .map_err(|p| Error::Invalid(format!("temp path not UTF-8: {p:?}")))?; let tree = tmp.join("tree"); - let tar = tmp.join("tree.tar"); - if let Err(e) = std::fs::create_dir_all(&tree) { + if let Err(e) = std::fs::create_dir_all(tree.as_std_path()) { return Ok((None, tmp, format!("temp dir failed: {e}"))); } - let st = match std::process::Command::new("git") - .args(["archive", "--format=tar"]) - .arg(base_ref) - .arg("-o") - .arg(&tar) - .current_dir(root.as_std_path()) - .status() - { - Ok(s) => s, - Err(e) => return Ok((None, tmp, format!("git unavailable: {e}"))), - }; - if !st.success() { - return Ok((None, tmp, format!("git archive `{base_ref}` failed"))); - } - let ok = std::process::Command::new("tar") - .arg("-xf") - .arg(&tar) - .arg("-C") - .arg(&tree) - .status() - .map(|s| s.success()) - .unwrap_or(false); - if !ok { - return Ok((None, tmp, "tar extract failed".into())); + if let Err(e) = git.export_tree(base_ref, &tree).await { + return Ok((None, tmp, e.to_string())); } let mut before = GraphIndex::in_memory(); @@ -392,12 +370,94 @@ async fn build_before_index( } } -/// Diff → simulate: chạy sandbox trên flow entry cho cả bản "trước" (git +/// Tìm symbol Function/Method theo tên (contains) trong một index cụ thể. +async fn find_fn(idx: &GraphIndex, name: &str) -> Option { + idx.search_symbol_paged_resumable( + name, + None, + SymbolMatch::Contains, + codegraph_graph::Pagination { + limit: 20, + offset: 0, + }, + None, + None, + ) + .await + .ok()? + .page + .into_iter() + .find(|s| matches!(s.kind, SymbolKind::Function | SymbolKind::Method)) +} + +/// Branch compare: diff giữa `base` và `head` (mặc định = working tree) + báo +/// cáo tác động lên index hiện tại, kèm (tuỳ chọn) mermaid flow 2 màu cho +/// `entry` — so sánh call thêm/xoá giữa 2 nhánh. Read-only. +pub async fn dispatch_branch_compare( + root: &Utf8Path, + shared: Arc, + git: &dyn Git, + args: Value, +) -> Result { + let base = arg_str(&args, "base")?; + let head = args.get("head").and_then(|v| v.as_str()); + let diff = git.diff(base, head).await?; + let parsed = codegraph_graph::diff::parse_unified_diff(&diff) + .map_err(|e| Error::Invalid(e.to_string()))?; + + let idx = shared.ensure_fresh().await; + let report = idx.diff_assess(&parsed, Some(root.as_std_path())).await; + + // Mermaid flow 2 màu cho entry (nếu có): head = index hiện tại, + // base = index tạm từ `git export `. + let mut mermaid = Value::Null; + let mut entry_out = Value::Null; + if let Some(entry) = args.get("entry").and_then(|v| v.as_str()) { + entry_out = json!(entry); + let (base_idx, tmp, note) = build_before_index(git, base).await?; + let head_flow = match find_fn(&idx, entry).await { + Some(s) => idx.flow(s.id).await.ok(), + None => None, + }; + let base_flow = match &base_idx { + Some(b) => match find_fn(b, entry).await { + Some(s) => b.flow(s.id).await.ok(), + None => None, + }, + None => None, + }; + if let (Some(hf), Some(bf)) = (&head_flow, &base_flow) { + mermaid = json!(crate::mermaid::control_flow_diff(hf, bf)); + } else if let Some(hf) = &head_flow { + mermaid = json!(crate::mermaid::control_flow(hf)); + } + if !note.is_empty() { + mermaid = json!({ "diagram": mermaid, "base_index_note": note }); + } + let _ = std::fs::remove_dir_all(&tmp); + } + + emit_value( + root.as_str(), + json!({ + "draft": true, + "tool": "codegraph_git_branch_compare", + "base": base, + "head": head, + "diff": diff, + "report": report, + "entry": entry_out, + "mermaid": mermaid, + }), + ) +} + /// archive tại `base_ref`) và bản "sau" (index hiện tại = post-MR), so sánh /// trace. Read-only — không mutate index. pub async fn dispatch_diff_simulate( root: &Utf8Path, shared: Arc, + git: &dyn Git, args: Value, ) -> Result { let diff = arg_str(&args, "diff")?; @@ -441,7 +501,7 @@ pub async fn dispatch_diff_simulate( }; // Build index "trước" + tmp dir (caller dọn tmp kể cả khi thất bại). - let (before_idx, tmp, build_note) = build_before_index(root, &base_ref).await?; + let (before_idx, tmp, build_note) = build_before_index(git, &base_ref).await?; let result = async { let before = match &before_idx { @@ -478,6 +538,7 @@ pub async fn dispatch_diff_simulate( pub async fn dispatch_origin_simulate( root: &Utf8Path, shared: Arc, + git: &dyn Git, args: Value, ) -> Result { let entry = arg_str(&args, "entry")?; @@ -489,7 +550,7 @@ pub async fn dispatch_origin_simulate( let (call_args, mocks, config) = parse_run_options(root, &args)?; let idx = shared.ensure_fresh().await; - let (origin_idx, tmp, build_note) = build_before_index(root, &git_ref).await?; + let (origin_idx, tmp, build_note) = build_before_index(git, &git_ref).await?; let result = async { let origin = match &origin_idx { diff --git a/crates/codegraph-core/src/lib.rs b/crates/codegraph-core/src/lib.rs index fb64d3d553..2bc9b085b7 100644 --- a/crates/codegraph-core/src/lib.rs +++ b/crates/codegraph-core/src/lib.rs @@ -4,17 +4,18 @@ //! semgraph (`semgraph` module) — wire breaking đã chốt ở plan. mod error; +pub mod prof; mod route; mod semgraph; pub use error::{Error, Result}; pub use route::StorageRoute; pub use semgraph::{ - is_marker, marker_id, marker_name, Annotation, CallRecord, CallSite, CallSiteResult, ClassInfo, - DbStats as SemgraphStats, DependenciesReport, Dependency, EdgeMeta, EffectCallPattern, - EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, - ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, SymbolMatch, - MARKER_BRANCH_END, MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, MARKER_LOOP, - MARKER_LOOP_BACK, MARKER_REC_CALL, MARKER_RETURN, MARKER_SWITCH_CASE, MARKER_SWITCH_END, - MARKER_THROW, SYMBOL_BASE, + is_marker, marker_id, marker_name, Annotation, BranchLabel, CallRecord, CallSite, + CallSiteResult, ClassInfo, DbStats as SemgraphStats, DependenciesReport, Dependency, EdgeMeta, + EffectCallPattern, EffectRule, EffectType, FileInfo, FlowCall, FlowResult, FunctionScope, + MemberInfo, ResolveResult, ScopeLevel, SearchFlowResult, Symbol, SymbolId, SymbolKind, + SymbolMatch, MARKER_BRANCH_END, MARKER_BREAK, MARKER_CONTINUE, MARKER_IF_FALSE, MARKER_IF_TRUE, + MARKER_LOOP, MARKER_LOOP_BACK, MARKER_REC_CALL, MARKER_RETURN, MARKER_SWITCH_CASE, + MARKER_SWITCH_END, MARKER_THROW, SYMBOL_BASE, }; diff --git a/crates/codegraph-core/src/prof.rs b/crates/codegraph-core/src/prof.rs new file mode 100644 index 0000000000..c5a08891c8 --- /dev/null +++ b/crates/codegraph-core/src/prof.rs @@ -0,0 +1,120 @@ +//! Sink cho log đo `[CGPROF]` — chọn nơi ghi bằng env, không hardcode stderr. +//! +//! Vì sao không `eprintln!` thẳng: `codegraph serve --mcp` nói JSON-RPC qua +//! **stdout**, còn **stderr do MCP host sở hữu** (Claude Code, Codex… pipe nó +//! của tiến trình con để đưa vào log riêng). Chạy tay trong terminal thì thấy +//! `[CGPROF]`, chạy qua host thì im lặng — dễ tưởng profiler hỏng. Sink cho +//! phép ghi ra file để vẫn đọc/tail được trong cả hai trường hợp. +//! +//! Cấu hình (mặc định **tắt** để server im lặng): +//! - `CODEGRAPH_PROFILE=stderr` (hoặc `1`) → ghi stderr. +//! - `CODEGRAPH_PROFILE_FILE=` → append vào file, `tail -f` được. +//! - `CODEGRAPH_PROFILE=` → tắc dụng như `CODEGRAPH_PROFILE_FILE`. +//! - `0` / `off` / `false` (hoặc unset) → tắt. + +use std::fmt::Arguments; +use std::fs::OpenOptions; +use std::io::Write; +use std::sync::OnceLock; +use std::time::Instant; + +/// Nơi ghi log `[CGPROF]`, đọc 1 lần từ env rồi cache cho cả process. +#[derive(Debug, Clone, PartialEq, Eq)] +enum Sink { + Off, + Stderr, + File(String), +} + +fn sink() -> &'static Sink { + static SINK: OnceLock = OnceLock::new(); + SINK.get_or_init(|| { + if let Some(path) = env_value("CODEGRAPH_PROFILE_FILE") { + return Sink::File(path); + } + match env_value("CODEGRAPH_PROFILE") { + None => Sink::Off, + Some(v) if is_off(&v) => Sink::Off, + Some(v) if is_stderr(&v) => Sink::Stderr, + // Giá trị còn lại coi như path — tiện khi không muốn gõ biến thứ hai. + Some(path) => Sink::File(path), + } + }) +} + +fn env_value(name: &str) -> Option { + let v = std::env::var(name).ok()?; + let v = v.trim().to_string(); + (!v.is_empty()).then_some(v) +} + +fn is_off(v: &str) -> bool { + matches!( + v.to_ascii_lowercase().as_str(), + "0" | "off" | "false" | "none" + ) +} + +fn is_stderr(v: &str) -> bool { + matches!( + v.to_ascii_lowercase().as_str(), + "1" | "true" | "on" | "stderr" + ) +} + +/// Profiling đang bật hay không — call site có thể check để né format. +pub fn enabled() -> bool { + *sink() != Sink::Off +} + +/// Mở đo một stage. Trả [`Instant`] **luôn** (kể cả khi tắt) để call site +/// `stage_done` không cần branch. +pub fn stage(label: &str) -> Instant { + let started = Instant::now(); + if enabled() { + write(format_args!("[CGPROF] {label} start")); + } + started +} + +/// Đóng stage mở bằng [`stage`] với `started`. +pub fn stage_done(label: &str, started: Instant) { + if enabled() { + write(format_args!( + "[CGPROF] {label} took {:?}", + started.elapsed() + )); + } +} + +fn write(args: Arguments<'_>) { + match sink() { + Sink::Off => {} + Sink::Stderr => { + let _ = writeln!(std::io::stderr(), "{args}"); + } + // Mở file lỗi (path sai, không quyền ghi) → im lặng, không được làm + // hỏng request đang serve. + Sink::File(path) => { + if let Ok(mut f) = OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(f, "{args}"); + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn off_values_recognized() { + assert!(is_off("0")); + assert!(is_off("OFF")); + assert!(is_off("false")); + assert!(!is_off("stderr")); + assert!(is_stderr("1")); + assert!(is_stderr("STDERR")); + assert!(!is_stderr("off")); + } +} diff --git a/crates/codegraph-core/src/semgraph.rs b/crates/codegraph-core/src/semgraph.rs index 3421a0b120..984b15350d 100644 --- a/crates/codegraph-core/src/semgraph.rs +++ b/crates/codegraph-core/src/semgraph.rs @@ -396,6 +396,20 @@ pub struct FlowResult { pub chain_desc: Vec, /// Danh sách call-site (kể cả unresolved — hiện tên thô). pub calls: Vec, + /// Nhãn trigger của marker nhánh (if/loop/switch) theo vị trí trong `chain` + /// — cho UI render `IF_TRUE: x > 0`, `LOOP: i < n`, `SWITCH_CASE: Cmd::Init`. + #[serde(default)] + pub branch_labels: Vec, +} + +/// Nhãn trigger tại một vị trí marker trong chain. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[cfg_attr(feature = "graphql", derive(async_graphql::SimpleObject))] +pub struct BranchLabel { + /// Index trong `FlowResult.chain` (vị trí marker). + pub position: usize, + /// Text trigger: điều kiện `if`/`while`, giá trị `case`/`match arm`. + pub label: String, } /// Một call-site trong flow. diff --git a/crates/codegraph-extract/src/bingraph.rs b/crates/codegraph-extract/src/bingraph.rs index d693bb83a3..fb884e280c 100644 --- a/crates/codegraph-extract/src/bingraph.rs +++ b/crates/codegraph-extract/src/bingraph.rs @@ -864,6 +864,7 @@ impl BinaryGraph { chain, chain_desc, calls, + branch_labels: Vec::new(), }) } diff --git a/crates/codegraph-extract/src/docgraph.rs b/crates/codegraph-extract/src/docgraph.rs index 7d63199f7b..1ab6ef9e89 100644 --- a/crates/codegraph-extract/src/docgraph.rs +++ b/crates/codegraph-extract/src/docgraph.rs @@ -2,11 +2,11 @@ //! `.codegraph/config.toml` (dùng chung cho CLI `init`/`doc` và MCP server). use crate::config::ExtractConfig; -use camino::Utf8Path; +use camino::{Utf8Path, Utf8PathBuf}; use codegraph_core::{Error, Result}; use codegraph_docs::{DocConfig, DocumentGraph, StorageConfig}; -use std::sync::Arc; -use tokio::sync::RwLock as TokioRwLock; +use std::sync::{Arc, RwLock}; +use tokio::sync::{Mutex, RwLock as TokioRwLock}; /// Mở document graph theo config: dataset riêng cho docs (mặc định /// `.codegraph/docs.sqlite` với sqlite — tries của docs đụng namespace với code @@ -39,3 +39,158 @@ pub async fn open_doc_graph(root: &Utf8Path) -> Result { .await .map_err(|e| Error::Db(format!("open document graph: {e}"))) } + +/// Doc graph dùng chung: sẵn sàng (in-memory seed / đã open) hoặc lazy theo root. +/// +/// Mở `DocumentGraph` từ storage tốn thời gian tuyến tính với số node — repo +/// document lớn có thể vượt startup timeout. Nên server giữ root lúc khởi +/// động; lần doc op đầu tiên mới trigger open + rebuild (dưới `rebuild_lock`, +/// N call đồng thời chỉ 1 lần open), các call sau dùng handle đã cache. +pub struct SharedDocGraph { + state: RwLock, + /// Serialize open+rebuild — N doc call đồng thời chỉ 1 lần open. + rebuild_lock: Mutex<()>, +} + +enum SharedDocGraphState { + /// Handle sẵn sàng — trả ngay, không chờ. + Ready(Arc>), + /// Chưa open — lần `graph()` đầu mở storage + rebuild từ root này. + Lazy(Utf8PathBuf), +} + +impl SharedDocGraph { + /// Bọc handle đã sẵn sàng (in-memory seed). + pub fn ready(graph: Arc>) -> Self { + Self { + state: RwLock::new(SharedDocGraphState::Ready(graph)), + rebuild_lock: Mutex::new(()), + } + } + + /// Doc graph in-memory rỗng (khi session chưa bind root). + pub fn in_memory() -> Self { + let storage: Arc> = + Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())); + Self::ready(Arc::new(TokioRwLock::new(DocumentGraph::new( + storage, + DocConfig::default(), + )))) + } + + /// Lazy theo workspace root — chưa open gì. Lỗi open khi `graph()` được + /// gọi → fallback in-memory (doc tools vẫn dùng được per-session). + pub fn lazy(root: Utf8PathBuf) -> Self { + Self { + state: RwLock::new(SharedDocGraphState::Lazy(root)), + rebuild_lock: Mutex::new(()), + } + } + + /// Handle dùng được: fast path trả handle cached; lazy thì open+rebuild + /// đúng một lần dưới rebuild_lock rồi cache. + pub async fn graph(&self) -> Arc> { + if let SharedDocGraphState::Ready(g) = &*self.state.read().unwrap() { + return g.clone(); + } + let _guard = self.rebuild_lock.lock().await; + if let SharedDocGraphState::Ready(g) = &*self.state.read().unwrap() { + return g.clone(); + } + let root = match &*self.state.read().unwrap() { + SharedDocGraphState::Lazy(root) => root.clone(), + SharedDocGraphState::Ready(g) => return g.clone(), + }; + let graph = match open_doc_graph(&root).await { + Ok(g) => Arc::new(TokioRwLock::new(g)), + Err(e) => { + tracing::warn!("doc graph open failed ({e}) — fallback in-memory"); + Arc::new(TokioRwLock::new(DocumentGraph::new( + Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())), + DocConfig::default(), + ))) + } + }; + *self.state.write().unwrap() = SharedDocGraphState::Ready(graph.clone()); + graph + } + + /// Doc graph đã open chưa (`false` trên lazy instance chưa `graph()` nào). + pub fn is_ready(&self) -> bool { + matches!(&*self.state.read().unwrap(), SharedDocGraphState::Ready(_)) + } + + /// Re-point sang root mới (gọi khi session `init` bind workspace khác). + /// Reset về trạng thái Lazy — lần doc op tiếp theo mở lại từ đĩa. + pub fn reinit(&self, root: Utf8PathBuf) { + *self.state.write().unwrap() = SharedDocGraphState::Lazy(root); + } +} + +#[cfg(test)] +mod shared_tests { + use super::*; + + /// Lazy instance chưa open gì — `is_ready` false. + #[test] + fn lazy_starts_not_ready() { + let shared = SharedDocGraph::lazy("/nonexistent-root-xyz".into()); + assert!(!shared.is_ready()); + } + + /// Ready instance trả đúng handle. + #[tokio::test] + async fn ready_returns_cached_handle() { + let storage: Arc> = + Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())); + let graph = Arc::new(TokioRwLock::new(DocumentGraph::new( + storage, + DocConfig::default(), + ))); + let shared = SharedDocGraph::ready(graph.clone()); + assert!(shared.is_ready()); + let got = shared.graph().await; + assert!(Arc::ptr_eq(&graph, &got)); + } + + /// N call `graph()` đồng thời trên lazy instance chỉ open 1 lần. + #[tokio::test] + async fn lazy_concurrent_calls_share_single_open() { + let shared = Arc::new(SharedDocGraph::lazy("/nonexistent-root-xyz".into())); + let (a, b) = { + let (s1, s2) = (shared.clone(), shared.clone()); + tokio::join!( + async move { s1.graph().await }, + async move { s2.graph().await } + ) + }; + assert!(Arc::ptr_eq(&a, &b)); + assert!(shared.is_ready()); + } + + /// Lazy instance trên root có docs.sqlite đã seed: lần `graph()` đầu + /// rebuild từ storage và thấy đúng dữ liệu. + #[tokio::test] + async fn lazy_graph_rebuilds_from_persisted_docs() { + let dir = tempfile::tempdir().unwrap(); + let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + + let doc_file = dir.path().join("sample.json"); + std::fs::write(&doc_file, r#"{"name": "app", "replicas": 2}"#).unwrap(); + { + let mut graph = open_doc_graph(&root).await.unwrap(); + graph + .ingest_file(doc_file.to_string_lossy().as_ref(), None) + .await + .unwrap(); + } + + let shared = SharedDocGraph::lazy(root); + assert!(!shared.is_ready()); + let graph = shared.graph().await; + let stats = graph.read().await.stats().await.unwrap(); + assert_eq!(stats.docs, 1); + assert!(stats.nodes > 0); + assert!(shared.is_ready()); + } +} diff --git a/crates/codegraph-extract/src/languages/common.rs b/crates/codegraph-extract/src/languages/common.rs index fa30e415c8..6117ba347b 100644 --- a/crates/codegraph-extract/src/languages/common.rs +++ b/crates/codegraph-extract/src/languages/common.rs @@ -641,6 +641,10 @@ fn walk_chain( } else { Some(cond_text) }; + // Nhãn trigger của nhánh if (`IF_TRUE: `); else dùng phủ định. + if let Some(c) = &if_cond { + emit_branch_label(ctx, c); + } // Calls TRONG condition (`if (a() && b(c()))`) được emit ngay sau IF_TRUE // — trước đây rớt khỏi chain (không tìm/search được). if let Some(cn) = cond_node { @@ -659,6 +663,9 @@ fn walk_chain( walk_alternative(ctx, &alt, depth, in_loop, if_cond); } else { chain_push(ctx, MARKER_IF_FALSE); + if let Some(c) = &if_cond { + emit_branch_label(ctx, &format!("!{c}")); + } walk_alternative(ctx, &alt, depth, in_loop, negate_cond(if_cond)); } } @@ -676,10 +683,17 @@ fn walk_chain( // Condition của loop (while/for/do): emit calls trong condition + giữ // text làm metadata — trước đây loop mất cả calls lẫn text. let cond_node = loop_condition_node(node, ctx.spec); - let loop_cond = cond_node + // Text điều kiện của chính loop (`while (x)`, `for (i=0; i(node: &Node<'a>, spec: &'static LangSpec) -> Vec> { out } -/// Case label là string literal (`case 'optimize_text':`) — dispatch key theo +/// Case label **string literal** (`case 'optimize_text':`) — dispatch key theo /// chuỗi, không phải call thật. Emit placeholder `0` + CallRecord với /// `call_name = literal` (bỏ quote) để `search_by_call` index được. Không có /// symbol tương ứng trong repo → không resolve được → giữ unresolved call. -fn emit_case_label_call(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition: Option) { +/// +/// (Nhãn hiển thị cho UI — kể cả pattern `Cmd::Init`/`1`/`_` — do +/// [`emit_branch_label`] gắn ở vị trí marker, không đi qua đường này.) +fn emit_case_label(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition: Option) { // Field `value` là expression của case (`case X:` → X). Fallback: named child // đầu tiên (một số grammar không đặt field). let value = case @@ -983,6 +1005,57 @@ fn emit_case_label_call(ctx: &mut ChainCtx, case: &Node, in_loop: u32, condition }); } +/// Nhãn **trigger** của một case/switch (`Cmd::Init`, `Some(v)`, `1`, `_`, +/// `'store'`) — text thô của pattern. Không dùng cho search (đã có đường +/// string-literal ở [`emit_case_label`]); chỉ để mermaid hiện `SWITCH_CASE: `. +fn case_pattern_text(case: &Node, src: &[u8]) -> Option { + let value = case + .child_by_field_name("value") + .or_else(|| case.child_by_field_name("pattern")) + .or_else(|| { + named_children(case) + .into_iter() + .find(|c| !is_case_body_kind(c.kind())) + })?; + text(&value, src).map(|t| t.trim().to_string()) +} + +/// Node kind là *thân* của một case (không phải pattern) — dùng để fallback lấy +/// pattern khi grammar không đặt field. Bỏ qua block/statement-list/body. +fn is_case_body_kind(kind: &str) -> bool { + kind.contains("block") + || kind.contains("statement") + || kind == "declaration_list" + || kind == "compound_statement" +} + +/// Gắn **nhãn nhánh** vào đúng vị trí marker trong chain (không thêm node): +/// thêm một `CallRecord` với `position = `, `condition = label`, +/// `call_name = ""`. Marker không bao giờ có call thật ở cùng position nên record +/// này tách hẳn khỏi call — không tạo edge, không lọt vào call-name index, và +/// `flow()` đọc lại thành `branch_labels` cho UI (`IF_TRUE: x > 0`, `LOOP: i < n`, +/// `SWITCH_CASE: Cmd::Init`). +fn emit_branch_label(ctx: &mut ChainCtx, label: &str) { + let name = label.trim(); + if name.is_empty() { + return; + } + let position = ctx.chain.len().saturating_sub(1); // marker vừa push + ctx.calls.push(CallRecord { + caller_id: ctx.func_id, + call_name: String::new(), + position, + arg_exprs: Vec::new(), + line: 0, + condition: Some(name.to_string()), + is_loop_body: false, + effect: codegraph_core::EffectType::None, + effect_desc: None, + target_class: None, + target_method: None, + }); +} + /// Node kind của một string literal — chấp nhận các tên theo từng grammar /// (TS `string`, Java `string_literal`, Go `interpreted_string_literal`...). fn is_string_literal_kind(kind: &str) -> bool { diff --git a/crates/codegraph-extract/src/lib.rs b/crates/codegraph-extract/src/lib.rs index cb7dff45eb..4b5ba95a62 100644 --- a/crates/codegraph-extract/src/lib.rs +++ b/crates/codegraph-extract/src/lib.rs @@ -20,7 +20,7 @@ pub mod bingraph; #[cfg(feature = "binary")] pub use bingraph::{BinFlag, BinPage, BinSymbolRow, BinaryGraph, ListOrder, NameMatch}; pub use config::{DocGraphSection, ExtractConfig, HeaderLanguage, DEFAULT_CONFIG_TOML}; -pub use docgraph::open_doc_graph; +pub use docgraph::{open_doc_graph, SharedDocGraph}; pub use orchestrator::{ExtractStats, Orchestrator}; pub use project::{init_project, project_db_path, project_dir, CODEGRAPH_DIR}; diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index f31ff38e69..04632c6d16 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -18,8 +18,10 @@ //! đọc chain, skip marker/self/0. Persistent — dùng chung storage với entity //! store (`rt_*` radix tables + `sg_*` entity tables trong cùng sqlite). //! - **Name engine** `Search`: key = tên symbol (lowercase bytes), record = -//! synthetic id (1-based vào `name_records`). Luôn **in-memory** (như -//! `SearchIndex` của semgraph) — rebuild từ `name_index` khi open/ingest. +//! synthetic id (1-based vào `name_records`). Persist ở dataset riêng +//! (`names.lmdb`/`names.sqlite` cạnh DB chính) + version stamp — mở lại index +//! chỉ tốn `load_name_records`; backend không có dataset file riêng +//! (postgres/mysql/memory) fallback in-memory và rebuild từ `name_index`. //! //! `name_index: HashMap>` là nguồn mở rộng symbol trùng //! tên: radix chỉ lưu mỗi tên khác nhau 1 lần (insert_chain trả `Duplicated` với @@ -61,10 +63,10 @@ pub use crate::storage::{ }; use crate::vector_index::VectorIndex; use codegraph_core::{ - CallRecord, CallSite, CallSiteResult, ClassInfo, DependenciesReport, Dependency, EdgeMeta, - EffectType, Error, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, ResolveResult, - SYMBOL_BASE, SearchFlowResult, SemgraphStats, StorageRoute, Symbol, SymbolKind, SymbolMatch, - is_marker, marker_name, + BranchLabel, CallRecord, CallSite, CallSiteResult, ClassInfo, DependenciesReport, Dependency, + EdgeMeta, EffectType, Error, FileInfo, FlowCall, FlowResult, FunctionScope, MemberInfo, + ResolveResult, SYMBOL_BASE, SearchFlowResult, SemgraphStats, StorageRoute, Symbol, SymbolKind, + SymbolMatch, is_marker, marker_name, }; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, HashMap, HashSet}; @@ -145,6 +147,51 @@ pub async fn open_doc_storage(dsn: &str) -> Result>> { open_keyspace_storage(dsn, "codegraph:docs").await } +/// DSN dataset thứ hai (name engine) suy ra từ DSN chính — cùng backend, file +/// cạnh bên nhau (`db.lmdb` → `names.lmdb`). Mirror pattern của docs +/// (`db.lmdb` → `docs.lmdb`) nên không phải viết adapter namespace. +/// +/// `None` = backend không có dataset file riêng (postgres/mysql/memory) — +/// giống `doc_storage_dsn`, name engine fallback in-memory. +#[cfg(any(feature = "sqlite", feature = "lmdb", feature = "redis"))] +fn name_dsn(main_dsn: &str) -> Option { + if let Some(path) = main_dsn.strip_prefix("sqlite://") { + return Some(format!("sqlite://{}", sibling_file(path, "names.sqlite"))); + } + if let Some(path) = main_dsn.strip_prefix("lmdb://") { + return Some(format!("lmdb://{}", sibling_file(path, "names.lmdb"))); + } + // Redis không tách file — cùng DSN, khác namespace ố do keyspace. + if main_dsn.starts_with("redis://") || main_dsn.starts_with("rediss://") { + return Some(main_dsn.to_string()); + } + None +} + +/// Thay tên file cuối của `path` (giữ nguyên thư mục cha) — `names.lmdb` +/// đặt cạnh `db.lmdb` dù `a/b/db.lmdb` → `a/b/names.lmdb`. +#[cfg(any(feature = "sqlite", feature = "lmdb", feature = "redis"))] +fn sibling_file(path: &str, file: &str) -> String { + match path.rfind('/') { + Some(i) => format!("{}/{}", &path[..i], file), + None => file.to_string(), + } +} + +/// Mở dataset riêng cho name engine. `None` = không mừ đưọc (fallback +/// in-memory trong `new_with_storage`) — không chến nhạp và náy. +#[cfg(any(feature = "sqlite", feature = "lmdb", feature = "redis"))] +async fn open_name_store(main_dsn: &str) -> Option>> { + let dsn = name_dsn(main_dsn)?; + match open_keyspace_storage(&dsn, "codegraph:names").await { + Ok(store) => Some(store), + Err(e) => { + eprintln!("[codegraph] name engine dataset unavailable ({e}) — fallback in-memory"); + None + } + } +} + /// Mở storage cho một dataset theo DSN + keyspace (backend redis dùng prefix /// này để tách dữ liệu; sqlite/lmdb tách bằng file riêng nên bỏ qua keyspace). /// Dùng chung cho docs (`codegraph:docs`) và binary graph (`codegraph:binary`). @@ -249,7 +296,8 @@ struct CallRef<'a> { /// /// `storage` giữ cả entity store (`sg_*` / InMemory maps) lẫn radix của chain /// engine (`rt_*`); `chains` (Search) đọc ghi qua chính `storage` đó. Name -/// engine `names` luôn chạy trên storage in-memory riêng — không persist. +/// engine `names` chạy trên dataset **riêng** (`name_store`) — persist khi +/// backend có dataset file riêng, fallback in-memory nếu không. pub struct GraphIndex { /// Entity + chain engine storage. storage: Arc>, @@ -258,11 +306,15 @@ pub struct GraphIndex { /// Name engine: key = tên lowercase (bytes), record = 1-based vào /// `name_records`. names: Search, - /// `record - 1` → tên (song song với thứ tự insert name engine). + /// Dataset riêng của name engine (`names.lmdb`/`names.sqlite` cạnh + /// `db.lmdb`) — `None` = in-memory (không persist, luôn rebuild). + /// Giữ handle để đọc/ghi version stamp, bỏ qua việc dựng lại trie. + name_store: Option>>, + /// Tên distinct (lowercase) **đã sort**; `name_records[record - 1]` là tên + /// của engine record `record` (name engine insert theo đúng thứ tự sort + /// này). Cũng dùng trực tiếp cho scan Prefix/Suffix/Exact — không cần sort + /// lại (resume chỉ lưu vị trí, không lưu mảng). name_records: Vec, - /// Các tên distinct (lowercase) **đã sort** — dùng cho scan Prefix/Suffix/ - /// Exact không cần sort lại (resume chỉ lưu vị trí, không lưu mảng). - sorted_name_keys: Vec, /// symbol id → Symbol (registry — nguồn chân lý in-memory). symbols: HashMap, /// tên (lowercase) → symbol ids (mở rộng trùng tên khi search/resolve). @@ -305,7 +357,7 @@ pub enum SearchCursorPhase { /// engine trả records không theo thứ tự tên — phase A xong phải sort /// trước khi chuyển sang `Expand`. Engine(SearchResume), - /// Mode `Prefix`/`Suffix`/`Exact`: đang quét `sorted_name_keys` từ + /// Mode `Prefix`/`Suffix`/`Exact`: đang quét `name_records` (đã sort) từ /// `name_pos` (mảng đã sort sẵn — resume chỉ cần lưu vị trí). ScanNames { name_pos: usize }, /// Phase A xong: stream ids theo từng tên đã sort — filter `kind`, gom @@ -434,7 +486,7 @@ impl GraphIndex { /// model. Nếu process đã set config fastembed trước đó mà model lỗi → panic. pub fn in_memory() -> Self { let storage: Box = Box::new(InMemoryStorage::default()); - Self::new_with_storage(storage).expect("in_memory embedding backend init failed") + Self::new_with_storage(storage, None).expect("in_memory embedding backend init failed") } /// Mở index từ một backend persistent bằng DSN — rebuild từ entity store. @@ -549,8 +601,9 @@ impl GraphIndex { let storage = crate::storage::lmdb::LmdbStorage::open(path) .await .map_err(serr)?; + let name_store = open_name_store(&format!("lmdb://{path}")).await; let storage: Box = Box::new(storage); - let mut idx = Self::new_with_storage(storage)?; + let mut idx = Self::new_with_storage(storage, name_store)?; idx.rebuild().await?; Ok(idx) } @@ -560,8 +613,9 @@ impl GraphIndex { let storage = crate::storage::sqlite::SqliteStorage::open(path) .await .map_err(serr)?; + let name_store = open_name_store(&format!("sqlite://{path}")).await; let storage: Box = Box::new(storage); - let mut idx = Self::new_with_storage(storage)?; + let mut idx = Self::new_with_storage(storage, name_store)?; idx.rebuild().await?; Ok(idx) } @@ -591,8 +645,9 @@ impl GraphIndex { crate::storage::redis::RedisStorage::new(client, &format!("codegraph:idx:{db}")) .await .map_err(serr)?; + let name_store = open_name_store(dsn).await; let storage: Box = Box::new(storage); - let mut idx = Self::new_with_storage(storage)?; + let mut idx = Self::new_with_storage(storage, name_store)?; idx.rebuild().await?; Ok(idx) } @@ -634,8 +689,10 @@ impl GraphIndex { .ensure_registered(shard, route.root()) .await .map_err(serr)?; + // RDBMS không có dataset file riêng — name engine + // fallback in-memory (giống `doc_storage_dsn`). let storage: Box = Box::new(storage); - let mut idx = Self::new_with_storage(storage)?; + let mut idx = Self::new_with_storage(storage, None)?; idx.rebuild().await?; Ok(idx) } @@ -653,8 +710,10 @@ impl GraphIndex { .ensure_registered(shard, route.root()) .await .map_err(serr)?; + // RDBMS không có dataset file riêng — name engine + // fallback in-memory (giống `doc_storage_dsn`). let storage: Box = Box::new(storage); - let mut idx = Self::new_with_storage(storage)?; + let mut idx = Self::new_with_storage(storage, None)?; idx.rebuild().await?; Ok(idx) } @@ -670,15 +729,21 @@ impl GraphIndex { } } - fn new_with_storage(storage: Box) -> Result { + fn new_with_storage( + storage: Box, + name_store: Option>>, + ) -> Result { // Wrap mọi backend bằng LRU read-cache để giảm gọi xuống storage // (SQL/remote) cho các read path nóng (node/children/chain/entity). const CACHE_CAPACITY: usize = 8192; let storage = CachedStorage::wrap(storage, CACHE_CAPACITY); - // Name engine luôn in-memory (như semgraph SearchIndex) — storage riêng - // để record id (1..N) không đụng record của chain engine (func ids). - let name_storage = - CachedStorage::wrap(Box::new(InMemoryStorage::default()), CACHE_CAPACITY); + // Name engine dùng dataset RIÉNG (cùng backend theo config) — record id + // (1..N) không đụng func id của chain trie, và `Search::clear()` + // không cần global nữa. Không mừ dataset (in-memory / RDBMS) + // → fallback in-memory như cư. + let name_storage = name_store.clone().unwrap_or_else(|| { + CachedStorage::wrap(Box::new(InMemoryStorage::default()), CACHE_CAPACITY) + }); // Embedding chỉ bật khi config `[embedding].backend = "fastembed"` (opt-in). // Nếu bật mà model tải thất bại → lỗi rõ ràng (KHÔNG fallback silent). let (backend, enabled) = if embedding_enabled() { @@ -691,10 +756,10 @@ impl GraphIndex { }; Ok(Self { chains: Search::new(CHAIN_SHARDING, storage.clone()), - names: Search::new(CHAIN_SHARDING, name_storage), + names: Search::new(CHAIN_SHARDING, name_storage.clone()), + name_store, storage, name_records: Vec::new(), - sorted_name_keys: Vec::new(), symbols: HashMap::new(), name_index: HashMap::new(), scope_index: HashMap::new(), @@ -719,6 +784,19 @@ impl GraphIndex { )] // chỉ open() dùng — không backend thì không ai gọi. async fn rebuild(&mut self) -> Result<()> { + // Đo bằng sink `codegraph_core::prof` — stderr bị MCP host nuốt mất + // khi `serve --mcp`, nên log chỉ ra được khi bật qua env (xem prof.rs). + macro_rules! cg_t { + ($n:expr) => {{ + codegraph_core::prof::stage($n) + }}; + } + macro_rules! cg_d { + ($t:expr, $n:expr) => {{ + codegraph_core::prof::stage_done($n, $t); + }}; + } + let __t = cg_t!("rebuild TOTAL"); self.next_id = self .storage .read() @@ -726,6 +804,7 @@ impl GraphIndex { .load_next_id() .await .map_err(serr)?; + let __t_load_all_symbols = cg_t!("load_all_symbols"); let symbols = self .storage .read() @@ -733,7 +812,11 @@ impl GraphIndex { .load_all_symbols() .await .map_err(serr)?; + cg_d!(__t_load_all_symbols, "load_all_symbols"); + let __t_all_chains = cg_t!("all_chains"); let chains_raw = self.storage.read().await.all_chains().await.map_err(serr)?; + cg_d!(__t_all_chains, "all_chains"); + let __t_all_call_name_indexes = cg_t!("all_call_name_indexes"); let call_names_raw = self .storage .read() @@ -741,6 +824,8 @@ impl GraphIndex { .all_call_name_indexes() .await .map_err(serr)?; + cg_d!(__t_all_call_name_indexes, "all_call_name_indexes"); + let __t_all_call_records = cg_t!("all_call_records"); let call_records_raw = self .storage .read() @@ -748,6 +833,8 @@ impl GraphIndex { .all_call_records() .await .map_err(serr)?; + cg_d!(__t_all_call_records, "all_call_records"); + let __t_load_all_files = cg_t!("load_all_files"); self.files = self .storage .read() @@ -755,24 +842,29 @@ impl GraphIndex { .load_all_files() .await .map_err(serr)?; + cg_d!(__t_load_all_files, "load_all_files"); self.version = self.storage.read().await.version().await.map_err(serr)?; // Registry (scope ids trong entity đã là global — persist sau remap). self.symbols.clear(); self.name_index.clear(); self.scope_index.clear(); + let __t_a = cg_t!("index_symbol_loop"); for sym in symbols { self.index_symbol(sym); } + cg_d!(__t_a, "index_symbol_loop"); // Chains. + let __t_b = cg_t!("chains_decode"); self.chains_map.clear(); for (func_id, bytes) in chains_raw { self.chains_map .insert(func_id, crate::storage::decode_chain(&bytes)); } - // Call-name index. + cg_d!(__t_b, "chains_decode"); + let __t_c = cg_t!("call_names_json"); self.call_names.clear(); for (name, bytes) in call_names_raw { if let Ok(sites) = serde_json::from_slice::>(&bytes) { @@ -787,12 +879,28 @@ impl GraphIndex { recs.insert(func, r); } } + cg_d!(__t_c, "call_names_json"); + let __t_d = cg_t!("rebuild_edges"); self.rebuild_edges(&recs); + cg_d!(__t_d, "rebuild_edges"); // Engines. + let __t_e = cg_t!("rebuild_chain_engine"); self.rebuild_chain_engine(None).await?; - self.rebuild_name_engine(None).await?; + cg_d!(__t_e, "rebuild_chain_engine"); + let __t_f = cg_t!("rebuild_name_engine"); + // Name trie đã persist ở dataset riêng — nếu version khớp thì dựng + // lại 2 Vec phụ, không clear + insert lại ~70k tên (11-20s). + if self.name_engine_is_current().await { + self.load_name_records(); + } else { + self.rebuild_name_engine(None).await?; + self.stamp_name_engine().await; + } + cg_d!(__t_f, "rebuild_name_engine"); + let __t_g = cg_t!("rebuild_vector_index"); self.rebuild_vector_index(None).await?; + cg_d!(__t_g, "rebuild_vector_index"); // Persist counts để `codegraph_status` đọc O(1) (không rebuild lại). { let mut st = self.storage.write().await; @@ -806,6 +914,7 @@ impl GraphIndex { .await .map_err(serr)?; } + cg_d!(__t, "rebuild TOTAL"); Ok(()) } @@ -900,6 +1009,54 @@ impl GraphIndex { Ok(()) } + /// Slot record dành cho version stamp — name record bắt đầu từ 1 nên không + /// va chạm. Stamp = `[version: u64 LE][count: u64 LE]`. + const NAME_STAMP_SLOT: usize = 0; + + /// Name trie trên đĩa còn đúng version + khớp số record đã ghi (chống dataset + /// bị xoá giữa chừng → coi như stale). + async fn name_engine_is_current(&self) -> bool { + let Some(store) = &self.name_store else { + return false; + }; + let guard = store.read().await; + let Ok(Some(bytes)) = guard.get_meta(Self::NAME_STAMP_SLOT).await else { + return false; + }; + if bytes.len() != 16 { + return false; + } + let version = u64::from_le_bytes(bytes[0..8].try_into().unwrap_or([0; 8])); + let count = u64::from_le_bytes(bytes[8..16].try_into().unwrap_or([0; 8])) as usize; + if version != self.version { + return false; + } + guard.get_key_len(count).await.ok().flatten().is_some() + } + + /// Ghi stamp sau khi trie dựng lại — `version` + số tên distinct. + async fn stamp_name_engine(&self) { + let Some(store) = &self.name_store else { + return; + }; + let mut bytes = [0u8; 16]; + bytes[0..8].copy_from_slice(&self.version.to_le_bytes()); + bytes[8..16].copy_from_slice(&(self.name_index.len() as u64).to_le_bytes()); + let mut guard = store.write().await; + if let Err(e) = guard.set_meta(Self::NAME_STAMP_SLOT, &bytes).await { + eprintln!("[codegraph] name engine stamp failed: {e}"); + } + } + + /// Dựng lại `name_records` từ `name_index` (nằm sẵn trong RAM) — thay cho + /// vòng trie insert khi đã bỏ qua rebuild. Mảng giữ thứ tự sort để vừa map + /// record→tên vừa dùng cho scan Prefix/Suffix/Exact. + fn load_name_records(&mut self) { + let mut distinct: Vec<&String> = self.name_index.keys().collect(); + distinct.sort(); + self.name_records = distinct.into_iter().cloned().collect(); + } + /// Rebuild name engine từ `name_index` (clear + insert mỗi tên distinct). async fn rebuild_name_engine(&mut self, progress: Option<&dyn IngestProgress>) -> Result<()> { self.names.clear().await.map_err(serr_search)?; @@ -910,7 +1067,6 @@ impl GraphIndex { p.phase("rebuild name-search engine", distinct.len()); } let mut record = 0usize; - self.sorted_name_keys = distinct.iter().map(|s| s.to_string()).collect(); for name in distinct { record += 1; let metas: Vec> = vec![None; name.len()]; @@ -1024,7 +1180,6 @@ impl GraphIndex { self.edges.clear(); self.files.clear(); self.name_records.clear(); - self.sorted_name_keys.clear(); self.next_id = SYMBOL_BASE; // ── Phase 1: register + remap ── @@ -1104,6 +1259,10 @@ impl GraphIndex { self.rebuild_name_engine(p).await?; self.rebuild_vector_index(p).await?; self.version += 1; + // Stamp SAU khi bump version: đây là chỗ duy nhất ghi name trie ra + // đĩa, nên `rebuild()` lúc mở index chỉ cần đọc stamp là bỏ qua + // `rebuild_name_engine` (11-32s) — thay vì dựng lại mỗi lần mở. + self.stamp_name_engine().await; { let mut st = self.storage.write().await; st.save_next_id(self.next_id).await.map_err(serr)?; @@ -1400,6 +1559,13 @@ impl GraphIndex { let (c, caller) = (entry.rec, entry.caller); recs_by_caller.entry(caller).or_default().push(i as u32); + // Record nhãn nhánh (`call_name` rỗng, `condition` = trigger) vẫn được + // persist để `flow()` đọc lại — nhưng KHÔNG vào call-name index + // (không phải call thật). + if c.call_name.is_empty() { + continue; + } + // Call-site index: key theo tên thô + alias type-qualified (nếu có). let site = CallSite { caller_id: caller, @@ -1792,6 +1958,21 @@ impl GraphIndex { let rec_by_pos: HashMap = recs.iter().map(|r| (r.position, r)).collect(); + // Nhãn trigger của marker nhánh: record có `call_name` rỗng + `condition` + // đặt tại đúng vị trí marker (xem `emit_branch_label` phía extractor). + let branch_labels: Vec = recs + .iter() + .filter(|r| { + r.call_name.is_empty() + && r.condition.is_some() + && chain.get(r.position).is_some_and(|&e| is_marker(e)) + }) + .map(|r| BranchLabel { + position: r.position, + label: r.condition.clone().unwrap_or_default(), + }) + .collect(); + let chain_desc = chain .iter() .enumerate() @@ -1819,6 +2000,9 @@ impl GraphIndex { } if e == 0 { if let Some(rec) = rec_by_pos.get(&i) { + if rec.call_name.is_empty() { + continue; // record nhãn nhánh, không phải call + } calls.push(FlowCall { position: i, to_name: rec.call_name.clone(), @@ -1868,6 +2052,7 @@ impl GraphIndex { chain, chain_desc, calls, + branch_labels, }) } @@ -2334,7 +2519,7 @@ impl GraphIndex { /// true, cursor: Some(phase dở) }` — caller gọi lại với `resume = /// Some(cursor)` để tiếp tục từ đúng vị trí (không lặp phần đã duyệt). /// - /// - Phase A (Contains: engine resumable; khác: scan `sorted_name_keys`) + /// - Phase A (Contains: engine resumable; khác: scan `name_records`) /// sinh danh sách tên khớp **đã sort** — chỉ hoàn tất sau khi quét hết /// (không thể sort từng phần vì tên mới có thể chèn vào giữa). /// - Phase B (`Expand`) stream ids theo tên đã sort — `name_index[name]` @@ -2447,10 +2632,10 @@ impl GraphIndex { timed_out = true; break; } - if pos >= self.sorted_name_keys.len() { + if pos >= self.name_records.len() { break; } - let name = &self.sorted_name_keys[pos]; + let name = &self.name_records[pos]; let ok = match mode { SymbolMatch::Prefix => name.starts_with(&q), SymbolMatch::Suffix => name.ends_with(&q), @@ -2692,7 +2877,7 @@ impl GraphIndex { edges: mt::edges_mem(&self.edges), name_index: mt::name_index_mem(&self.name_index), scope_index: mt::scope_index_mem(&self.scope_index), - name_keys: mt::name_keys_mem(&self.name_records, &self.sorted_name_keys), + name_keys: mt::name_keys_mem(&self.name_records), files: mt::files_mem(&self.files), caches, } diff --git a/crates/codegraph-graph/src/memtrack.rs b/crates/codegraph-graph/src/memtrack.rs index 61b68bb603..74286093a2 100644 --- a/crates/codegraph-graph/src/memtrack.rs +++ b/crates/codegraph-graph/src/memtrack.rs @@ -171,14 +171,13 @@ pub fn scope_index_mem(m: &HashMap>) -> StructureMem { chains_map_mem(m) } -/// RAM của `name_records: Vec` + `sorted_name_keys: Vec`. +/// RAM của `name_records: Vec`. #[allow(clippy::ptr_arg)] // xem `vec_bytes` — cần `capacity()` của `Vec`. -pub fn name_keys_mem(records: &Vec, sorted: &Vec) -> StructureMem { +pub fn name_keys_mem(records: &Vec) -> StructureMem { StructureMem { - entries: (records.len() + sorted.len()) as u64, - fixed_bytes: vec_bytes::(records) + vec_bytes::(sorted), - heap_bytes: records.iter().map(str_bytes).sum::() - + sorted.iter().map(str_bytes).sum::(), + entries: records.len() as u64, + fixed_bytes: vec_bytes::(records), + heap_bytes: records.iter().map(str_bytes).sum::(), } } diff --git a/crates/codegraph-graph/src/shared.rs b/crates/codegraph-graph/src/shared.rs index 04dd58e8b0..2d4890a8a5 100644 --- a/crates/codegraph-graph/src/shared.rs +++ b/crates/codegraph-graph/src/shared.rs @@ -167,6 +167,10 @@ impl SharedGraphIndex { /// /// Fresh (ready + đúng version) → trả ngay. Stale hoặc chưa build → rebuild /// đồng bộ dưới `rebuild_lock` rồi trả snapshot mới. + /// + /// **Chặn** tới khi rebuild xong (với repo lớn có thể hàng chục giây) — dùng + /// cho CLI/one-shot. Server (`codegraph serve`) không nên gọi trực tiếp: + /// xem [`Self::fresh_snapshot`] (non-blocking) + warm task lúc khởi động. pub async fn ensure_fresh(self: &Arc) -> Arc { // Fast path: snapshot mới nhất sẵn sàng. { @@ -191,6 +195,17 @@ impl SharedGraphIndex { self.state.read().await.index.clone() } + /// Snapshot hiện tại **nếu đã build và khớp version trên đĩa**, `None` khi + /// stale / chưa build. Không bao giờ rebuild và không chặn — dùng cho server + /// để báo "đang refresh" thay vì treo request. + pub async fn fresh_snapshot(&self) -> Option> { + let state = self.state.read().await; + if state.ready && self.is_fresh(state.version).await { + return Some(state.index.clone()); + } + None + } + /// Build index từ route hiện tại rồi swap snapshot (gọi trong `rebuild_lock`). /// `GraphIndex::open_route` tự route theo scheme — không cần nhánh cfg. async fn rebuild_inner(&self) -> Result<()> { @@ -355,6 +370,49 @@ mod tests { /// Re-index ngoài (bump version) → ensure_fresh phát hiện stale → rebuild. #[cfg(feature = "sqlite")] + /// Name engine đã persist ở dataset riêng + stamp version → mở lại index + /// không phải dựng lại trie (đây là phần chậm nhất của `rebuild`). + #[cfg(feature = "sqlite")] + #[tokio::test] + async fn name_engine_stamp_survives_reopen() { + let dir = tempfile::tempdir().unwrap(); + let db_path = dir.path().join("db.sqlite"); + let db_str = format!("sqlite://{}", db_path.to_string_lossy()); + + { + let mut idx = GraphIndex::open(&db_str).await.unwrap(); + let r = mk_result( + "a.ts", + vec![sym("alpha", SYMBOL_BASE), sym("beta", SYMBOL_BASE + 1)], + vec![SYMBOL_BASE, SYMBOL_BASE + 1], + ); + idx.ingest(&[r]).await.unwrap(); + assert!(idx.name_engine_is_current().await, "stamp sau ingest"); + } + + // Dataset riêng của name engine phải tồn tại cạnh db chính. + assert!(dir.path().join("names.sqlite").exists(), "names.sqlite"); + + // Mở lại → stamp khớp version → bỏ qua rebuild trie. + let idx = GraphIndex::open(&db_str).await.unwrap(); + assert!( + idx.name_engine_is_current().await, + "stamp phải khớp sau reopen" + ); + assert_eq!(idx.name_records.len(), 2); + assert_eq!(idx.name_records, vec!["alpha", "beta"]); + + // Re-index → version bump → stamp cũ thành stale. + let mut idx = GraphIndex::open(&db_str).await.unwrap(); + let r = mk_result( + "b.ts", + vec![sym("gamma", SYMBOL_BASE + 9)], + vec![SYMBOL_BASE + 9], + ); + idx.ingest(&[r]).await.unwrap(); + assert!(idx.name_engine_is_current().await, "stamp mới sau ingest"); + } + #[tokio::test] async fn sqlite_stale_version_rebuilds() { let dir = tempfile::tempdir().unwrap(); diff --git a/crates/codegraph-graphql/Cargo.toml b/crates/codegraph-graphql/Cargo.toml index 2f5ed245c0..8a4aae90f5 100644 --- a/crates/codegraph-graphql/Cargo.toml +++ b/crates/codegraph-graphql/Cargo.toml @@ -11,7 +11,9 @@ codegraph-core = { path = "../codegraph-core", features = ["graphql"] } codegraph-context = { path = "../codegraph-context" } codegraph-graph = { path = "../codegraph-graph", features = ["sqlite"] } codegraph-docs = { path = "../codegraph-docs" } +codegraph-extract = { path = "../codegraph-extract" } codegraph-source = { path = "../codegraph-source" } +codegraph-web = { path = "../codegraph-web" } async-graphql = { workspace = true } async-graphql-axum = { workspace = true } diff --git a/crates/codegraph-graphql/src/lib.rs b/crates/codegraph-graphql/src/lib.rs index 6099bd7dd8..470daeaaa2 100644 --- a/crates/codegraph-graphql/src/lib.rs +++ b/crates/codegraph-graphql/src/lib.rs @@ -24,11 +24,9 @@ use axum::{ use camino::Utf8PathBuf; use codegraph_api::session::{OutputStyle, Session}; use codegraph_api::SearchSessionStore; -use codegraph_docs::{DocConfig, DocumentGraph}; -use codegraph_graph::InMemoryStorage; +use codegraph_extract::SharedDocGraph; use std::net::SocketAddr; use std::sync::Arc; -use tokio::sync::RwLock as TokioRwLock; use tower_http::cors::{Any, CorsLayer}; pub use types::*; @@ -43,7 +41,10 @@ pub struct AppState { /// resolver này trả lỗi rõ ràng. Đây là config mức server (`--mermaid`). pub mermaid: bool, /// Document graph cho structured document operations (HCL, YAML, JSON, TOML). - pub doc_graph: Arc>, + /// Lazy theo root (persist qua `[docgraph]`); in-memory khi chưa bind root. + pub doc_graph: Arc, + /// Module UI đang bật (từ config) — frontend lọc sidebar theo danh sách. + pub ui_modules: Vec, } /// Cấu hình cho [`serve`]. @@ -62,6 +63,26 @@ pub struct ServeConfig { pub allow_hosts: Vec, /// Bật Mermaid diagram output (tương ứng flag `--mermaid` ở CLI). pub mermaid: bool, + /// Phục vụ web UI nhúng tại `/` (tương ứng cờ tắt `--no-web`). + pub web: bool, + /// Module UI được bật (mặc định = tất cả). Tắt module để thu gọn sidebar. + pub ui_modules: Vec, +} + +/// Module UI mặc định (khi `--ui-modules` không truyền). +pub const DEFAULT_UI_MODULES: [&str; 3] = ["explore", "review", "documents"]; + +/// Module UI đang bật (trả về cho frontend). +pub fn enabled_modules(cfg_modules: &[String]) -> Vec { + if cfg_modules.is_empty() { + DEFAULT_UI_MODULES.iter().map(|s| s.to_string()).collect() + } else { + cfg_modules + .iter() + .map(|s| s.trim().to_lowercase()) + .filter(|s| !s.is_empty()) + .collect() + } } /// Chạy GraphQL server (blocking — bind + serve đến khi shutdown). @@ -70,18 +91,36 @@ pub async fn serve(cfg: ServeConfig) -> anyhow::Result<()> { Some(ref r) => Session::with_root_and_format(r.clone(), cfg.format).await?, None => Session::new_with_format(cfg.format), }; - let storage: Arc> = - Arc::new(TokioRwLock::new(InMemoryStorage::default())); - let doc_graph = Arc::new(TokioRwLock::new(DocumentGraph::new( - storage, - DocConfig::default(), - ))); + // Doc graph lazy theo root (nếu có) — persist qua `[docgraph]`/`[storage]`. + let doc_graph = Arc::new(match cfg.root.clone() { + Some(root) => SharedDocGraph::lazy(root), + None => SharedDocGraph::in_memory(), + }); let state = Arc::new(AppState { session: Arc::new(session), search_sessions: Arc::new(SearchSessionStore::new()), mermaid: cfg.mermaid, doc_graph, + ui_modules: crate::enabled_modules(&cfg.ui_modules), }); + + // Warm index nền ngay lúc khởi động. `GraphIndex::rebuild` mất hàng chục + // giây với repo lớn; để nó chạy ở task tách rời thay vì chặn request đầu + // tiên của UI (request đó sẽ phải chờ `ensure_fresh`). + { + let warm = state.clone(); + tokio::spawn(async move { + match warm.session.ensure_ready().await { + Ok(sgi) => { + let _ = sgi.ensure_fresh().await; + } + Err(e) => { + tracing::debug!("index warm skipped: {e}"); + } + } + }); + } + let app = build_app(&cfg, state); let listener = tokio::net::TcpListener::bind(cfg.addr).await?; @@ -94,7 +133,7 @@ pub async fn serve(cfg: ServeConfig) -> anyhow::Result<()> { /// port). Đây là nơi gắn CORS, api-key auth middleware và GraphQL handler. pub(crate) fn build_app(cfg: &ServeConfig, state: Arc) -> Router { let schema = Schema::build(query::Query, mutation::Mutation, EmptySubscription) - .data(state) + .data(state.clone()) .finish(); // Clone các giá trị cần thiết vào owned data để middleware closure không @@ -119,10 +158,18 @@ pub(crate) fn build_app(cfg: &ServeConfig, state: Arc) -> Router { } }; - Router::new() + let mut app = Router::new() .route("/health", get(health)) .route_service("/graphql", GraphQL::new(schema)) - .layer(cors) + .with_state(state); + + // Web UI nhúng: mọi route không khớp (không phải /graphql, /health) trả + // về SPA. Tắt bằng `--no-web` để chỉ còn API. + if cfg.web { + app = app.fallback_service(codegraph_web::web_router()); + } + + app.layer(cors) .layer(from_fn(move |req: Request, next: Next| { let api_key = api_key.clone(); async move { @@ -148,8 +195,30 @@ pub(crate) fn build_app(cfg: &ServeConfig, state: Arc) -> Router { })) } -async fn health() -> impl IntoResponse { - (StatusCode::OK, "ok") +/// Health check — `200` khi index đã refresh xong, `503` khi đang rebuild. +/// +/// Rebuild index mất hàng chục giây với repo lớn. Trong lúc đó server +/// chưa phục vục để query đước data — báo `503` để client/UI +/// retry thay vì treo chờ. +async fn health( + axum::extract::State(state): axum::extract::State>, +) -> impl IntoResponse { + let sgi = match state.session.ensure_ready().await { + Ok(sgi) => sgi, + Err(e) => { + return ( + StatusCode::SERVICE_UNAVAILABLE, + format!("no session bound: {e}"), + ) + } + }; + if sgi.fresh_snapshot().await.is_none() { + return ( + StatusCode::SERVICE_UNAVAILABLE, + "index chưa refresh xong (đang rebuild từ storage)".to_string(), + ); + } + (StatusCode::OK, "ok".to_string()) } #[cfg(test)] @@ -159,24 +228,18 @@ mod tests { use axum::http::{Request, StatusCode}; use codegraph_api::session::{OutputStyle, Session}; use codegraph_api::SearchSessionStore; + use codegraph_extract::SharedDocGraph; use tower::ServiceExt; - use codegraph_graph::InMemoryStorage; - use tokio::sync::RwLock as TokioRwLock; - fn make_state(mermaid: bool) -> Arc { let session = Session::new_with_format(OutputStyle::Minimal); - let storage: Arc> = - Arc::new(TokioRwLock::new(InMemoryStorage::default())); - let doc_graph = Arc::new(TokioRwLock::new(DocumentGraph::new( - storage, - DocConfig::default(), - ))); + let doc_graph = Arc::new(SharedDocGraph::in_memory()); Arc::new(AppState { session: Arc::new(session), search_sessions: Arc::new(SearchSessionStore::new()), mermaid, doc_graph, + ui_modules: vec![], }) } @@ -188,6 +251,9 @@ mod tests { format: OutputStyle::Minimal, allow_hosts: vec![], mermaid, + // Test không phụ thuộc asset đã build — tắt web cho ổn định. + web: false, + ui_modules: vec![], } } @@ -209,17 +275,80 @@ mod tests { (status, json) } + /// Session chưa bind workspace → server chưa phục vục để query index → `503`. #[tokio::test] - async fn health_ok() { + async fn health_503_without_session() { let app = build_app(&cfg(false), make_state(false)); let req = Request::builder() .uri("/health") .body(Body::empty()) .unwrap(); let res = app.oneshot(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); + } + + /// Bind workspace + warm index xong — giửe `200`. + #[tokio::test] + async fn health_ok_after_warm() { + use codegraph_api::session::DetailLevel; + + let dir = tempfile::tempdir().unwrap(); + let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let session = Session::new_with_format(OutputStyle::Minimal); + session + .init(root, false, DetailLevel::default(), None) + .await + .unwrap(); + // Đúng warm y nhết `serve()` lúc khởi động. + let sgi = session.ensure_ready().await.unwrap(); + let _ = sgi.ensure_fresh().await; + + let state = Arc::new(AppState { + session: Arc::new(session), + search_sessions: Arc::new(SearchSessionStore::new()), + mermaid: false, + doc_graph: Arc::new(SharedDocGraph::in_memory()), + ui_modules: vec![], + }); + let app = build_app(&cfg(false), state); + let req = Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(); + let res = app.oneshot(req).await.unwrap(); assert_eq!(res.status(), StatusCode::OK); } + /// Query cần index mà index đang rebuild → lỗi rõ ràng ngay, không treo request. + #[tokio::test] + async fn graphql_query_fails_fast_while_index_warming() { + use codegraph_api::session::DetailLevel; + + let dir = tempfile::tempdir().unwrap(); + let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); + let session = Session::new_with_format(OutputStyle::Minimal); + session + .init(root, false, DetailLevel::default(), None) + .await + .unwrap(); + // KHÔNG warm — index chưa build, đúng trong trạng thái `serve()` lúc khởi động. + let state = Arc::new(AppState { + session: Arc::new(session), + search_sessions: Arc::new(SearchSessionStore::new()), + mermaid: false, + doc_graph: Arc::new(SharedDocGraph::in_memory()), + ui_modules: vec![], + }); + let app = build_app(&cfg(false), state); + let (_status, json) = post_graphql( + &app, + "{ graphcodeListSymbols(kind: FUNCTION, limit: 1) { total } }", + ) + .await; + let msg = json["errors"][0]["message"].as_str().unwrap(); + assert!(msg.contains("refresh"), "unexpected error: {msg}"); + } + #[tokio::test] async fn graphql_endpoint_responds() { let app = build_app(&cfg(false), make_state(false)); diff --git a/crates/codegraph-graphql/src/mutation.rs b/crates/codegraph-graphql/src/mutation.rs index 521a908a8a..2b3528fddb 100644 --- a/crates/codegraph-graphql/src/mutation.rs +++ b/crates/codegraph-graphql/src/mutation.rs @@ -3,15 +3,14 @@ //! string (output phức tạp, ít dùng cho UI; passthrough qua `serde_json::Value`). //! + Document ingest/search/hydrate/list/stats. -use async_graphql::{Context, Object, Result as GqlResult}; +use async_graphql::{Context, Object, Result as GqlResult, ID}; use camino::Utf8PathBuf; use codegraph_api::session::{DetailLevel, OutputStyle}; use codegraph_api::tools; -use codegraph_docs::{DocConfig, DocumentGraph}; use serde_json::{json, Value}; use std::sync::Arc; -use tokio::sync::RwLock as TokioRwLock; +use crate::types::*; use crate::AppState; pub struct Mutation; @@ -40,9 +39,11 @@ impl Mutation { let format = format.as_deref().and_then(OutputStyle::parse); let outcome = state .session - .init(root, do_index, detail, format) + .init(root.clone(), do_index, detail, format) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; + // Re-point doc graph sang root mới (lazy open lần doc op tiếp theo). + state.doc_graph.reinit(root); let v = json!({ "root": outcome.root, "dir": outcome.dir, @@ -91,11 +92,7 @@ impl Mutation { /// `args: JSON` = `{ node?, name?, args?: [i64], mocks?: {callee: rhai}, branchPolicy?, loopCap? }`. async fn graphcode_sandbox(&self, ctx: &Context<'_>, args: Value) -> GqlResult { let state = ctx.data::>()?; - let sgi = state - .session - .ensure_ready() - .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; + let sgi = crate::query::fresh_index(ctx).await?; let root = state .session .root() @@ -110,11 +107,7 @@ impl Mutation { /// `args: JSON` = `{ diff: "...", entry?, baseRef?, ... }`. async fn graphcode_diff(&self, ctx: &Context<'_>, args: Value) -> GqlResult { let state = ctx.data::>()?; - let sgi = state - .session - .ensure_ready() - .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; + let sgi = crate::query::fresh_index(ctx).await?; let root = state .session .root() @@ -129,133 +122,156 @@ impl Mutation { /// `{ diff, entry?, baseRef?, args?, mocks?, branchPolicy?, loopCap? }`. async fn graphcode_diff_simulate(&self, ctx: &Context<'_>, args: Value) -> GqlResult { let state = ctx.data::>()?; - let sgi = state - .session - .ensure_ready() - .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; + let sgi = crate::query::fresh_index(ctx).await?; let root = state .session .root() .await .ok_or_else(|| async_graphql::Error::new("session root unavailable"))?; - tools::dispatch_diff_simulate(&root, sgi, args) + let git = codegraph_source::DiskGit::new(root.clone()); + tools::dispatch_diff_simulate(&root, sgi, &git, args) .await .map_err(|e| async_graphql::Error::new(e.to_string())) } - /// Ref → simulate: so sánh trace trên `git archive ` vs working tree. + /// Ref → simulate: so sánh trace trên `git export ` vs working tree. /// `args: JSON` = `{ entry, ref?, args?, mocks?, branchPolicy?, loopCap? }`. async fn graphcode_origin_simulate(&self, ctx: &Context<'_>, args: Value) -> GqlResult { let state = ctx.data::>()?; - let sgi = state + let sgi = crate::query::fresh_index(ctx).await?; + let root = state .session - .ensure_ready() + .root() .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; + .ok_or_else(|| async_graphql::Error::new("session root unavailable"))?; + let git = codegraph_source::DiskGit::new(root.clone()); + tools::dispatch_origin_simulate(&root, sgi, &git, args) + .await + .map_err(|e| async_graphql::Error::new(e.to_string())) + } + + /// Branch compare: diff giữa `base` và `head` + báo cáo tác động + (tuỳ + /// chọn) mermaid flow 2 màu cho `entry`. `args: JSON` = `{ base, head?, entry? }`. + async fn graphcode_branch_compare(&self, ctx: &Context<'_>, args: Value) -> GqlResult { + let state = ctx.data::>()?; + let sgi = crate::query::fresh_index(ctx).await?; let root = state .session .root() .await .ok_or_else(|| async_graphql::Error::new("session root unavailable"))?; - tools::dispatch_origin_simulate(&root, sgi, args) + let git = codegraph_source::DiskGit::new(root.clone()); + tools::dispatch_branch_compare(&root, sgi, &git, args) .await .map_err(|e| async_graphql::Error::new(e.to_string())) } // ── Document mutations ── - /// Ingest a document file into the document graph. + /// Ingest một file document (YAML/JSON/TOML/HCL) vào doc graph persist. + /// Trả doc_id (u64 dạng string). async fn graphdoc_ingest( &self, ctx: &Context<'_>, path: String, format: Option, - ) -> GqlResult { + ) -> GqlResult { let state = ctx.data::>()?; - let source = - std::fs::read_to_string(&path).map_err(|e| async_graphql::Error::new(e.to_string()))?; - let ext = std::path::Path::new(&path) - .extension() - .and_then(|e| e.to_str()) - .map(|e| e.to_lowercase()) - .unwrap_or_default(); - let fmt: String = match format { - Some(f) => f, - None => match ext.as_str() { - "tf" | "hcl" => "hcl".to_string(), - "yaml" | "yml" => "yaml".to_string(), - "json" => "json".to_string(), - "toml" => "toml".to_string(), - _ => { - return Err(async_graphql::Error::new(format!( - "unknown format for extension .{ext}" - ))) - } - }, - }; - let _doc_graph = state.doc_graph.clone(); - let parser: Box = match fmt.as_str() { - "hcl" => Box::new(codegraph_docs::parsers::HclParser), - "yaml" => Box::new(codegraph_docs::parsers::YamlParser), - "json" => Box::new(codegraph_docs::parsers::JsonParser), - "toml" => Box::new(codegraph_docs::parsers::TomlParser), - _ => { - return Err(async_graphql::Error::new(format!( - "unsupported format: {fmt}" - ))) - } - }; - let storage: Arc> = - Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())); - let mut graph = DocumentGraph::new(storage, DocConfig::default()); - let doc_id = 1; // doc in-memory per-mutation — id không quan trọng - let doc = parser - .parse(&path, &source, doc_id) - .map_err(|e| async_graphql::Error::new(e.to_string()))?; - let inserted = graph - .upsert_document(doc) + let graph = state.doc_graph.graph().await; + let mut graph = graph.write().await; + let doc_id = graph + .ingest_file(&path, format.as_deref()) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; - Ok(format!("ingested {path} → doc_id={inserted}")) + Ok(ID::from(doc_id)) } - /// Search document nodes. - async fn graphdoc_search( + /// Ingest hàng loạt mọi file document trong thư mục (đệ quy, theo extension). + async fn graphdoc_ingest_dir( &self, ctx: &Context<'_>, - _pattern: String, - depth: Option, - ) -> GqlResult { + path: String, + limit: Option, + ) -> GqlResult { let state = ctx.data::>()?; - let depth = depth.unwrap_or(1).max(1) as usize; - let ids = state - .doc_graph - .read() - .await - .search_path(&[codegraph_docs::tokenize::DocToken::root()], Some(depth)) - .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; - let mut results = Vec::new(); - for id in &ids { - if let Some(payload) = state.doc_graph.read().await.hydrate(*id).await { - results.push(json!({ "id": payload.id, "path": payload.path, "kind": format!("{:?}", payload.kind) })); + let limit = limit.unwrap_or(500).max(1) as usize; + const EXTS: [&str; 6] = ["yaml", "yml", "json", "toml", "tf", "hcl"]; + let mut files = Vec::new(); + let mut stack = vec![std::path::PathBuf::from(&path)]; + while let Some(dir) = stack.pop() { + let entries = match std::fs::read_dir(&dir) { + Ok(e) => e, + Err(_) => continue, + }; + for entry in entries.flatten() { + let p = entry.path(); + if p.is_dir() { + stack.push(p); + } else if p + .extension() + .and_then(|e| e.to_str()) + .is_some_and(|ext| EXTS.contains(&ext.to_ascii_lowercase().as_str())) + { + files.push(p); + } } } - Ok(serde_json::to_string_pretty(&results) - .map_err(|e| async_graphql::Error::new(e.to_string()))?) + files.sort(); + if files.len() > limit { + files.truncate(limit); + } + let total = files.len(); + let graph = state.doc_graph.graph().await; + let mut graph = graph.write().await; + let mut ingested = 0usize; + let mut failed = 0usize; + for f in &files { + let Some(p) = f.to_str() else { continue }; + match graph.ingest_file(p, None).await { + Ok(_) => ingested += 1, + Err(_) => failed += 1, + } + } + Ok(DocIngestSummary { + requested: total, + ingested, + failed, + }) } - /// Get document stats. - async fn graphdoc_stats(&self, ctx: &Context<'_>) -> GqlResult { + /// Xoá một document theo id. + async fn graphdoc_remove(&self, ctx: &Context<'_>, id: ID) -> GqlResult { let state = ctx.data::>()?; - let stats = state - .doc_graph - .read() + let id = id + .parse::() + .map_err(|_| async_graphql::Error::new("invalid id"))?; + let graph = state.doc_graph.graph().await; + let mut graph = graph.write().await; + graph + .remove_document(id) .await - .stats() + .map_err(|e| async_graphql::Error::new(e.to_string()))?; + Ok(true) + } + + /// Mine structural patterns (kind chain) trên toàn bộ node lá scalar. + async fn graphdoc_mine_patterns( + &self, + ctx: &Context<'_>, + top_k: Option, + min_count: Option, + max_depth: Option, + ) -> GqlResult> { + let state = ctx.data::>()?; + let top_k = top_k.unwrap_or(20).max(1) as usize; + let min_count = min_count.unwrap_or(3).max(1) as usize; + let max_depth = max_depth.unwrap_or(4).max(1) as usize; + let graph = state.doc_graph.graph().await; + let mut graph = graph.write().await; + let mined = graph + .mine_patterns(top_k, min_count, max_depth) .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; - Ok(format!("documents: {}\nnodes: {}", stats.docs, stats.nodes)) + Ok(mined.into_iter().map(Into::into).collect()) } } diff --git a/crates/codegraph-graphql/src/query.rs b/crates/codegraph-graphql/src/query.rs index 287a2f7b9c..f87e21ab78 100644 --- a/crates/codegraph-graphql/src/query.rs +++ b/crates/codegraph-graphql/src/query.rs @@ -9,6 +9,7 @@ use codegraph_core::{ SemgraphStats, Symbol, SymbolKind, SymbolMatch, }; use codegraph_docs::tokenize::DocToken; +use codegraph_source::GitRepo; use std::sync::Arc; use crate::types::*; @@ -20,17 +21,34 @@ fn parse_id(id: &ID) -> GqlResult { .map_err(|_| async_graphql::Error::new(format!("invalid id: {id:?}"))) } -/// Build một `GraphApi` trên snapshot index mới nhất của session hiện tại. -async fn api_for(ctx: &Context<'_>) -> GqlResult { +/// Session đã bind root + index đã refresh xong. +/// +/// `GraphIndex::rebuild` mất hàng chục giây với repo lớn. Server warm index +/// ố task nến lúc khởi động, nên Ồ đây ta **không chờ** rebuild +/// mà trả lỗi rõ ràng — request treo chờ rebuild làm UI trông như treo máy. +pub(crate) async fn fresh_index( + ctx: &Context<'_>, +) -> GqlResult> { let state = ctx.data::>()?; let sgi = state .session .ensure_ready() .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; + if sgi.fresh_snapshot().await.is_none() { + return Err(async_graphql::Error::new( + "index chưa refresh xong (đang rebuild từ storage) — thử lại sau vài giây", + )); + } + Ok(sgi) +} + +/// Build một `GraphApi` trên snapshot index mới nhất của session hiện tại. +async fn api_for(ctx: &Context<'_>) -> GqlResult { + let sgi = fresh_index(ctx).await?; Ok(GraphApi::new_with_sessions( sgi, - state.search_sessions.clone(), + ctx.data::>()?.search_sessions.clone(), )) } @@ -293,7 +311,22 @@ impl Query { /// Thông số index (symbols/chains/edges/files/next_id) — health check. async fn status(&self, ctx: &Context<'_>) -> GqlResult { - Ok(api_for(ctx).await?.stats_cached().await) + let state = ctx.data::>()?; + let sgi = state + .session + .ensure_ready() + .await + .map_err(|e| async_graphql::Error::new(e.to_string()))?; + // Đọc counts từ `sg_stats` trên đĩa (O(1)) — không cần index in-memory, + // nên vẫn trả lỗi đẵ server cóng đang rebuild. UI poll query này + // để biết khi náo warm xong. + Ok(sgi.stats_cached().await.unwrap_or(SemgraphStats { + symbols: 0, + chains: 0, + edges: 0, + files: 0, + next_id: 0, + })) } /// Class info: symbol + fields + methods. @@ -326,6 +359,24 @@ impl Query { }) } + /// Liệt kê symbol theo kind (mọi SymbolKind), phân trang. Dùng cho Browse. + async fn graphcode_list_symbols( + &self, + ctx: &Context<'_>, + kind: SymbolKind, + limit: Option, + offset: Option, + ) -> GqlResult { + let (limit, offset) = paging(limit, offset); + let (items, total) = api_for(ctx).await?.list_by_kind(kind, limit, offset).await; + let has_more = (offset as usize + items.len()) < total; + Ok(ListResult { + items, + total: total as u64, + has_more, + }) + } + /// Scope của function (parameters + locals). async fn graphcode_function_scope( &self, @@ -364,55 +415,160 @@ impl Query { Ok(api_for(ctx).await?.dependencies().await) } + /// Liệt kê branch local của workspace (cho branch compare trong Review). + async fn git_branches(&self, ctx: &Context<'_>) -> GqlResult> { + let state = ctx.data::>()?; + let root = state + .session + .root() + .await + .ok_or_else(|| async_graphql::Error::new("session root unavailable"))?; + let git = codegraph_source::DiskGit::new(root); + // Không phải repo (hoặc lỗi) → trả rỗng thay vì lỗi cứng. + Ok(git.branches().await.unwrap_or_default()) + } + // ── Document queries ── - /// List all documents in the document graph. - async fn graphdoc_list(&self, ctx: &Context<'_>) -> GqlResult> { + /// Module UI đang bật (từ config server) — sidebar lọc theo danh sách này. + async fn ui_config(&self, ctx: &Context<'_>) -> GqlResult> { let state = ctx.data::>()?; - let stats = state - .doc_graph - .read() - .await + Ok(state.ui_modules.clone()) + } + + /// Thống kê document graph (số doc + node). + async fn graphdoc_stats(&self, ctx: &Context<'_>) -> GqlResult { + let state = ctx.data::>()?; + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + let stats = graph .stats() .await .map_err(|e| async_graphql::Error::new(e.to_string()))?; - Ok(vec![DocStatsView { + Ok(DocStatsView { docs: stats.docs, nodes: stats.nodes, - }]) + }) + } + + /// Liệt kê mọi document (path, format, số node). + async fn graphdoc_list(&self, ctx: &Context<'_>) -> GqlResult> { + let state = ctx.data::>()?; + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + Ok(graph.list_docs().into_iter().map(Into::into).collect()) } - /// Search document nodes by pattern string. + /// Search node theo pattern đường dẫn (vd `spec.replicas`) hoặc fuzzy key. + /// Trả node đã hydrate (có path) — depth = số tầng con kế tiếp. async fn graphdoc_search( &self, ctx: &Context<'_>, - _pattern: String, + pattern: String, depth: Option, ) -> GqlResult> { let state = ctx.data::>()?; let depth = depth.unwrap_or(1).max(1) as usize; - let tokens = vec![DocToken::root()]; - let ids = state - .doc_graph - .read() - .await - .search_path(&tokens, Some(depth)) - .await - .map_err(|e| async_graphql::Error::new(e.to_string()))?; + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + + // Pattern "spec.replicas" → [root, FIELD(spec), FIELD(replicas)]. + let mut tokens = vec![DocToken::root()]; + let mut unknown_seg = false; + let mut fuzzy_seg: Option = None; + for seg in pattern.split('.') { + if let Some(fz) = seg.strip_prefix('~') { + fuzzy_seg = Some(fz.to_string()); + break; + } + match graph.intern_id(seg) { + Some(id) => tokens.push(DocToken::field(id)), + None => { + unknown_seg = true; + break; + } + } + } + + let mut ids: Vec = Vec::new(); + if !unknown_seg && fuzzy_seg.is_none() { + let mut found = graph + .search_path(&tokens, Some(tokens.len() - 1 + depth)) + .await + .unwrap_or_default(); + found.extend(graph.search_path_scan(&tokens, 100)); + found.sort_unstable(); + found.dedup(); + ids = found; + } + + if ids.is_empty() { + // Fallback: fuzzy key substring. + let last = fuzzy_seg + .clone() + .unwrap_or_else(|| pattern.rsplit('.').next().unwrap_or(&pattern).to_string()); + let hits = graph.search_key_fuzzy(&last, 50); + let mut results = Vec::new(); + for h in hits { + if let Some(payload) = graph.hydrate_depth(h.node.id, Some(1)).await { + results.push(payload.into()); + } + } + return Ok(results); + } + + let mut results = Vec::new(); + for id in ids.iter().take(100) { + if let Some(payload) = graph.hydrate_depth(*id, Some(1)).await { + results.push(payload.into()); + } + } + Ok(results) + } + + /// Hydrate một node theo id (cây con theo max_depth). + async fn graphdoc_hydrate( + &self, + ctx: &Context<'_>, + id: ID, + max_depth: Option, + ) -> GqlResult> { + let state = ctx.data::>()?; + let id = id + .parse::() + .map_err(|_| async_graphql::Error::new("invalid id"))?; + let max_depth = max_depth.map(|d| d.max(0) as usize); + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + Ok(graph.hydrate_depth(id, max_depth).await.map(Into::into)) + } + + /// Search theo giá trị scalar (substring). + async fn graphdoc_search_value( + &self, + ctx: &Context<'_>, + query: String, + limit: Option, + ) -> GqlResult> { + let state = ctx.data::>()?; + let limit = limit.unwrap_or(50).max(1) as usize; + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + let hits = graph.search_value_substring(&query, limit); let mut results = Vec::new(); - for id in &ids { - if let Some(payload) = state.doc_graph.read().await.hydrate(*id).await { - results.push(DocNodePayload { - id: payload.id, - path: payload.path, - kind: format!("{:?}", payload.kind), - value: payload.value.map(|v| format!("{:?}", v)), - key: payload.key, - doc: payload.doc, - children: vec![], - }); + for n in hits { + if let Some(payload) = graph.hydrate_depth(n.id, Some(1)).await { + results.push(payload.into()); } } Ok(results) } + + /// Liệt kê structural patterns đã mine. + async fn graphdoc_list_patterns(&self, ctx: &Context<'_>) -> GqlResult> { + let state = ctx.data::>()?; + let graph = state.doc_graph.graph().await; + let graph = graph.read().await; + Ok(graph.list_patterns().into_iter().map(Into::into).collect()) + } } diff --git a/crates/codegraph-graphql/src/types.rs b/crates/codegraph-graphql/src/types.rs index 3788fb869e..74a2f25c2f 100644 --- a/crates/codegraph-graphql/src/types.rs +++ b/crates/codegraph-graphql/src/types.rs @@ -10,6 +10,7 @@ use async_graphql::{Enum, InputObject, SimpleObject}; use codegraph_context::Format as CoreCtxFormat; use codegraph_core::{CallSiteResult, SearchFlowResult, Symbol, SymbolKind, SymbolMatch}; +use codegraph_docs::{Kind as DocKind, NodePayload, Scalar}; // ==================== Pagination wrappers ==================== @@ -143,7 +144,7 @@ pub enum MermaidKind { // ==================== Document types ==================== -/// Định dạng tài liệu hỗ trợ. +/// Định dạng tài liệu hỗ trợ (input cho ingest). #[derive(Enum, Copy, Clone, Eq, PartialEq, Debug)] #[graphql(rename_items = "SCREAMING_SNAKE_CASE")] pub enum DocFormat { @@ -153,7 +154,22 @@ pub enum DocFormat { Yaml, } -/// Node payload trong document graph — dạng GraphQL-friendly. +/// Nhãn hiển thị của một `Kind` trong document graph. +fn kind_label(k: DocKind) -> String { + format!("{k:?}").to_uppercase() +} + +/// Chuỗi hiển thị của một scalar. +fn scalar_label(s: &Scalar) -> String { + match s { + Scalar::String(v) => v.clone(), + Scalar::Number(n) => n.to_string(), + Scalar::Bool(b) => b.to_string(), + Scalar::Null => "null".to_string(), + } +} + +/// Node document đã hydrate — cây con. #[derive(SimpleObject, Clone, Debug)] pub struct DocNodePayload { pub id: u64, @@ -161,13 +177,109 @@ pub struct DocNodePayload { pub kind: String, pub value: Option, pub key: Option, + pub index: Option, pub doc: u64, pub children: Vec, } -/// Summary của document graph (GraphQL view). +impl From for DocNodePayload { + fn from(p: NodePayload) -> Self { + Self { + id: p.id, + path: p.path, + kind: kind_label(p.kind), + value: p.value.as_ref().map(scalar_label), + key: p.key, + index: p.index, + doc: p.doc, + children: p.children.into_iter().map(Into::into).collect(), + } + } +} + +/// Node document dạng phẳng (kết quả search) — không kèm children. +#[derive(SimpleObject, Clone, Debug)] +pub struct DocNodeView { + pub id: u64, + pub path: Vec, + pub kind: String, + pub value: Option, + pub key: Option, + pub index: Option, + pub doc: u64, +} + +impl From for DocNodeView { + fn from(n: codegraph_docs::Node) -> Self { + // path không có sẵn trên Node thô — dựng từ key/index chain (rỗng ở đây, + // caller có path từ hydrate khi cần). + Self { + id: n.id, + path: Vec::new(), + kind: kind_label(n.kind), + value: n.value.as_ref().map(scalar_label), + key: n.key, + index: n.index, + doc: n.doc, + } + } +} + +/// Summary của một document. +#[derive(SimpleObject, Clone, Debug)] +pub struct DocInfoView { + pub doc_id: u64, + pub path: String, + pub format: String, + pub root_node_id: u64, + pub nodes: usize, +} + +impl From for DocInfoView { + fn from(d: codegraph_docs::graph::DocInfo) -> Self { + Self { + doc_id: d.doc_id, + path: d.path, + format: d.format, + root_node_id: d.root_node_id, + nodes: d.nodes, + } + } +} + +/// Summary của document graph. #[derive(SimpleObject, Clone, Debug)] pub struct DocStatsView { pub docs: usize, pub nodes: usize, } + +/// Một structural pattern đã mine. +#[derive(SimpleObject, Clone, Debug)] +pub struct DocPatternView { + pub pattern_id: u64, + pub tokens: Vec, + pub node_count: usize, + pub doc_count: usize, + pub doc_freq: f64, +} + +impl From for DocPatternView { + fn from(p: codegraph_docs::graph::PatternEntry) -> Self { + Self { + pattern_id: p.pattern_id, + tokens: p.tokens, + node_count: p.node_count, + doc_count: p.doc_count, + doc_freq: p.doc_freq, + } + } +} + +/// Kết quả ingest hàng loạt document. +#[derive(SimpleObject, Clone, Debug)] +pub struct DocIngestSummary { + pub requested: usize, + pub ingested: usize, + pub failed: usize, +} diff --git a/crates/codegraph-mcp/src/docgraph.rs b/crates/codegraph-mcp/src/docgraph.rs index aeaae373fd..991b315a6b 100644 --- a/crates/codegraph-mcp/src/docgraph.rs +++ b/crates/codegraph-mcp/src/docgraph.rs @@ -1,153 +1,5 @@ -//! SharedDocGraph — document graph dùng chung cho MCP server, mở **lazily**. -//! -//! Mở `DocumentGraph` từ storage (`DocumentGraph::open` → rebuild toàn bộ -//! tries từ node/doc JSON) tốn thời gian tuyến tính với số node — với repo -//! document lớn có thể vượt startup timeout của MCP client. Nên server chỉ -//! giữ root lúc khởi động; lần doc tool đầu tiên mới trigger open + rebuild -//! (dưới rebuild_lock — N call đồng thời chỉ 1 lần open), các call sau dùng -//! handle đã cache. +//! Doc graph dùng chung — nay sống ở `codegraph_extract::docgraph` để cả MCP +//! và GraphQL dùng lại. Re-export để giữ API nội bộ ổn định. -use camino::Utf8PathBuf; -use codegraph_docs::DocumentGraph; -use std::sync::{Arc, RwLock}; -use tokio::sync::{Mutex, RwLock as TokioRwLock}; +pub use codegraph_extract::SharedDocGraph; -/// Doc graph dùng chung: sẵn sàng (in-memory seed / đã open) hoặc lazy theo root. -pub struct SharedDocGraph { - state: RwLock, - /// Serialize open+rebuild — N doc call đồng thời chỉ 1 lần open. - rebuild_lock: Mutex<()>, -} - -enum SharedDocGraphState { - /// Handle sẵn sàng — trả ngay, không chờ. - Ready(Arc>), - /// Chưa open — lần `graph()` đầu mở storage + rebuild từ root này. - Lazy(Utf8PathBuf), -} - -impl SharedDocGraph { - /// Bọc handle đã sẵn sàng (in-memory seed của `CodegraphServer::new`). - pub fn ready(graph: Arc>) -> Self { - Self { - state: RwLock::new(SharedDocGraphState::Ready(graph)), - rebuild_lock: Mutex::new(()), - } - } - - /// Lazy theo workspace root — chưa open gì. Lỗi open khi `graph()` được - /// gọi → fallback in-memory (doc tools vẫn dùng được per-session), giữ - /// nguyên hành vi của đường eager cũ. - pub fn lazy(root: Utf8PathBuf) -> Self { - Self { - state: RwLock::new(SharedDocGraphState::Lazy(root)), - rebuild_lock: Mutex::new(()), - } - } - - /// Handle dùng được: fast path trả handle cached; lazy thì open+rebuild - /// đúng một lần dưới rebuild_lock rồi cache. - pub async fn graph(&self) -> Arc> { - if let SharedDocGraphState::Ready(g) = &*self.state.read().unwrap() { - return g.clone(); - } - let _guard = self.rebuild_lock.lock().await; - if let SharedDocGraphState::Ready(g) = &*self.state.read().unwrap() { - return g.clone(); - } - let root = match &*self.state.read().unwrap() { - SharedDocGraphState::Lazy(root) => root.clone(), - SharedDocGraphState::Ready(g) => return g.clone(), - }; - let graph = match codegraph_extract::open_doc_graph(&root).await { - Ok(g) => Arc::new(TokioRwLock::new(g)), - Err(e) => { - tracing::warn!("doc graph open failed ({e}) — fallback in-memory"); - Arc::new(TokioRwLock::new(DocumentGraph::new( - Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())), - codegraph_docs::DocConfig::default(), - ))) - } - }; - *self.state.write().unwrap() = SharedDocGraphState::Ready(graph.clone()); - graph - } - - /// Doc graph đã open chưa (`false` trên lazy instance chưa `graph()` nào). - pub fn is_ready(&self) -> bool { - matches!(&*self.state.read().unwrap(), SharedDocGraphState::Ready(_)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use codegraph_docs::DocConfig; - - /// Lazy instance chưa open gì — `is_ready` false, init không chạm storage. - #[test] - fn lazy_starts_not_ready() { - let shared = SharedDocGraph::lazy("/nonexistent-root-xyz".into()); - assert!(!shared.is_ready()); - } - - /// Ready instance trả đúng handle, `graph()` không đổi instance. - #[tokio::test] - async fn ready_returns_cached_handle() { - let storage: Arc> = - Arc::new(TokioRwLock::new(codegraph_graph::InMemoryStorage::default())); - let graph = Arc::new(TokioRwLock::new(DocumentGraph::new( - storage, - DocConfig::default(), - ))); - let shared = SharedDocGraph::ready(graph.clone()); - assert!(shared.is_ready()); - let got = shared.graph().await; - assert!(Arc::ptr_eq(&graph, &got)); - } - - /// N call `graph()` đồng thời trên lazy instance chỉ open 1 lần — call sau - /// dùng chung handle đã cache (root không tồn tại → fallback in-memory). - #[tokio::test] - async fn lazy_concurrent_calls_share_single_open() { - let shared = Arc::new(SharedDocGraph::lazy("/nonexistent-root-xyz".into())); - let (a, b) = { - let (s1, s2) = (shared.clone(), shared.clone()); - tokio::join!( - async move { s1.graph().await }, - async move { s2.graph().await } - ) - }; - assert!(Arc::ptr_eq(&a, &b)); - assert!(shared.is_ready()); - } - - /// Lazy instance trên root có docs.sqlite đã seed: lần `graph()` đầu - /// rebuild từ storage và thấy đúng dữ liệu (đường của MCP sau khi - /// `with_root_and_format` không chạm storage, doc tool đầu mới open). - #[tokio::test] - async fn lazy_graph_rebuilds_from_persisted_docs() { - let dir = tempfile::tempdir().unwrap(); - let root = camino::Utf8PathBuf::from_path_buf(dir.path().to_path_buf()).unwrap(); - - // "CLI process": ingest 1 doc JSON vào dataset mặc định của root. - let doc_file = dir.path().join("sample.json"); - std::fs::write(&doc_file, r#"{"name": "app", "replicas": 2}"#).unwrap(); - { - let mut graph = codegraph_extract::open_doc_graph(&root).await.unwrap(); - graph - .ingest_file(doc_file.to_string_lossy().as_ref(), None) - .await - .unwrap(); - } - - // "Server process": lazy open thấy lại doc đã persist. - let shared = SharedDocGraph::lazy(root); - assert!(!shared.is_ready()); - let graph = shared.graph().await; - let stats = graph.read().await.stats().await.unwrap(); - assert_eq!(stats.docs, 1); - assert!(stats.nodes > 0); - assert!(shared.is_ready()); - } -} diff --git a/crates/codegraph-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index 34444c9497..27c2836c47 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -498,10 +498,13 @@ impl CodegraphServer { codegraph_api::tools::dispatch_diff(&root, sgi.clone(), args.clone()).await } "codegraph_graphcode_diff_simulate" => { - codegraph_api::tools::dispatch_diff_simulate(&root, sgi.clone(), args.clone()).await + let git = codegraph_source::DiskGit::new(root.clone()); + codegraph_api::tools::dispatch_diff_simulate(&root, sgi.clone(), &git, args.clone()) + .await } "codegraph_graphcode_origin_simulate" => { - codegraph_api::tools::dispatch_origin_simulate(&root, sgi.clone(), args.clone()) + let git = codegraph_source::DiskGit::new(root.clone()); + codegraph_api::tools::dispatch_origin_simulate(&root, sgi.clone(), &git, args.clone()) .await } _ => { diff --git a/crates/codegraph-source/src/git.rs b/crates/codegraph-source/src/git.rs new file mode 100644 index 0000000000..4a145a3981 --- /dev/null +++ b/crates/codegraph-source/src/git.rs @@ -0,0 +1,234 @@ +//! Git layer — đường **đọc** dữ liệu VCS (branch / diff / export tree). +//! +//! Đối xứng với [`Source`](crate::Source): tách theo nhóm thao tác để provider +//! mới chỉ implement tầng nó cần, và chỗ đã dùng `&dyn Git` không phải sửa. +//! +//! - [`GitRepo`] — nhận diện repo + liệt kê branch. +//! - [`GitDiff`] — unified diff giữa 2 ref. +//! - [`GitArchive`] — export cây tại một ref ra thư mục (cho "before index"). +//! +//! `DiskGit` là impl mặc định, gọi `git` qua tiến trình ngoài — đây là chỗ duy +//! nhất trong hệ biết tới `Command::new("git")`. Provider từ xa (GitHub API, +//! object store) implement trait với I/O thật; test dùng impl giả trả diff cố +//! định mà không cần repo. + +use async_trait::async_trait; +use camino::{Utf8Path, Utf8PathBuf}; +use codegraph_core::{Error, Result}; + +/// Tầng nền — mọi thao tác git đều biết root của nó. +pub trait GitInfo { + /// Workspace root (thư mục chứa repo). + fn root(&self) -> &Utf8Path; +} + +/// Tầng **nhận diện + discovery**: repo nào, có branch gì. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) +#[async_trait] +pub trait GitRepo: GitInfo { + /// Có phải git repository không (`rev-parse --is-inside-work-tree`). + async fn is_repo(&self) -> bool; + + /// Liệt kê branch local (short names, mới nhất trước). Rỗng nếu không repo. + async fn branches(&self) -> Result>; + + /// Branch hiện tại (`HEAD` detached → `None`). + async fn current_branch(&self) -> Result> { + Ok(None) + } +} + +/// Tầng **diff**: unified diff giữa 2 ref. +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) +#[async_trait] +pub trait GitDiff: GitInfo { + /// Unified diff `base..head` (`head = None` → so với working tree), ở dạng + /// tương thích `parse_unified_diff`. Lỗi nếu `base`/`head` không tồn tại. + async fn diff(&self, base: &str, head: Option<&str>) -> Result; +} + +/// Tầng **export tree**: lấy nội dung cây tại một ref ra đĩa (để parse lại +/// thành index "trước" khi mô phỏng/so sánh nhánh). +#[allow(clippy::double_must_use)] // async_trait sinh `must_use` trùng (clippy 1.99) +#[async_trait] +pub trait GitArchive: GitInfo { + /// Ghi nội dung cây tại `git_ref` vào thư mục `dest` (đã tồn tại). Mặc định: + /// lỗi — provider không hỗ trợ export không cần override. + async fn export_tree(&self, git_ref: &str, dest: &Utf8Path) -> Result<()> { + let _ = (git_ref, dest); + Err(Error::Invalid(format!( + "git provider tại {} không hỗ trợ export_tree", + self.root() + ))) + } +} + +/// Trait gộp — phần lớn nơi chỉ cần cái này (`&dyn Git`). +pub trait Git: GitRepo + GitDiff + GitArchive + Send + Sync {} +impl Git for T {} + +/// Git trên filesystem local — impl mặc định, gọi `git`/`tar` ngoài tiến trình. +pub struct DiskGit { + root: Utf8PathBuf, +} + +impl DiskGit { + pub fn new(root: impl Into) -> Self { + Self { root: root.into() } + } + + /// Chạy một lệnh git, trả stdout; lỗi kèm stderr để dễ chẩn đoán. + fn run(&self, args: &[&str]) -> Result { + let out = std::process::Command::new("git") + .arg("-C") + .arg(self.root.as_str()) + .args(args) + .output() + .map_err(|e| Error::Invalid(format!("git unavailable: {e}")))?; + if !out.status.success() { + let err = String::from_utf8_lossy(&out.stderr); + return Err(Error::Invalid(format!( + "git {} failed: {}", + args.first().copied().unwrap_or(""), + err.trim() + ))); + } + Ok(String::from_utf8_lossy(&out.stdout).into_owned()) + } +} + +impl GitInfo for DiskGit { + fn root(&self) -> &Utf8Path { + &self.root + } +} + +#[async_trait] +impl GitRepo for DiskGit { + async fn is_repo(&self) -> bool { + self.run(&["rev-parse", "--is-inside-work-tree"]) + .map(|s| s.trim() == "true") + .unwrap_or(false) + } + + async fn branches(&self) -> Result> { + let text = self.run(&["branch", "--format=%(refname:short)", "--sort=-committerdate"])?; + Ok(text + .lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect()) + } + + async fn current_branch(&self) -> Result> { + let text = self.run(&["rev-parse", "--abbrev-ref", "HEAD"])?; + let b = text.trim(); + if b.is_empty() || b == "HEAD" { + Ok(None) + } else { + Ok(Some(b.to_string())) + } + } +} + +#[async_trait] +impl GitDiff for DiskGit { + async fn diff(&self, base: &str, head: Option<&str>) -> Result { + let mut args: Vec<&str> = vec!["diff", "--no-color", "--unified=3", base]; + if let Some(h) = head { + args.push(h); + } + self.run(&args) + } +} + +#[async_trait] +impl GitArchive for DiskGit { + async fn export_tree(&self, git_ref: &str, dest: &Utf8Path) -> Result<()> { + // `git archive` → tar tạm → `tar -xf` vào dest (giữ hành vi cũ). + let millis = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_millis()) + .unwrap_or(0); + let tar = Utf8PathBuf::from_path_buf( + std::env::temp_dir().join(format!("codegraph-git-{}-{millis}.tar", std::process::id())), + ) + .map_err(|p| Error::Invalid(format!("temp path not UTF-8: {p:?}")))?; + + let status = std::process::Command::new("git") + .arg("-C") + .arg(self.root.as_str()) + .args(["archive", "--format=tar"]) + .arg(git_ref) + .arg("-o") + .arg(tar.as_str()) + .status() + .map_err(|e| Error::Invalid(format!("git unavailable: {e}")))?; + if !status.success() { + return Err(Error::Invalid(format!("git archive `{git_ref}` failed"))); + } + + let ok = std::process::Command::new("tar") + .arg("-xf") + .arg(tar.as_str()) + .arg("-C") + .arg(dest.as_str()) + .status() + .map(|s| s.success()) + .unwrap_or(false); + let _ = std::fs::remove_file(tar.as_std_path()); + if !ok { + return Err(Error::Invalid("tar extract failed".into())); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Trên thư mục không phải repo: `is_repo` false, các lệnh khác lỗi sạch + /// (không panic). Đây là hợp đồng để caller xử lý "không có git". + #[tokio::test] + async fn non_repo_is_graceful() { + let d = tempfile::tempdir().unwrap(); + let root = Utf8PathBuf::from_path_buf(d.path().to_path_buf()).unwrap(); + let git = DiskGit::new(root); + assert!(!git.is_repo().await); + assert!(git.branches().await.is_err()); + } + + /// Export tree mặc định của provider không hỗ trợ → lỗi rõ ràng. + #[tokio::test] + async fn default_export_tree_errors() { + struct NoArchive(Utf8PathBuf); + impl GitInfo for NoArchive { + fn root(&self) -> &Utf8Path { + &self.0 + } + } + #[async_trait] + impl GitRepo for NoArchive { + async fn is_repo(&self) -> bool { + false + } + async fn branches(&self) -> Result> { + Ok(vec![]) + } + } + #[async_trait] + impl GitDiff for NoArchive { + async fn diff(&self, _base: &str, _head: Option<&str>) -> Result { + Ok(String::new()) + } + } + // GitArchive dùng default. + impl GitArchive for NoArchive {} + + let na = NoArchive(Utf8PathBuf::from("/tmp")); + let dest = Utf8PathBuf::from("/tmp"); + let err = na.export_tree("HEAD", &dest).await.unwrap_err(); + assert!(format!("{err}").contains("không hỗ trợ")); + } +} diff --git a/crates/codegraph-source/src/lib.rs b/crates/codegraph-source/src/lib.rs index 2c6542514e..9f947febe5 100644 --- a/crates/codegraph-source/src/lib.rs +++ b/crates/codegraph-source/src/lib.rs @@ -39,9 +39,11 @@ use camino::{Utf8Path, Utf8PathBuf}; use codegraph_core::{Error, Result}; mod disk; +mod git; mod registry; pub use disk::{disk_walker, DiskSource}; +pub use git::{DiskGit, Git, GitArchive, GitDiff, GitInfo, GitRepo}; pub use registry::{SourceConfig, SourceKind, SourceRegistry}; /// Một entry do Source liệt kê ra. diff --git a/crates/codegraph-web/.gitignore b/crates/codegraph-web/.gitignore new file mode 100644 index 0000000000..29362fc7af --- /dev/null +++ b/crates/codegraph-web/.gitignore @@ -0,0 +1,5 @@ +# Output build của web UI (SvelteKit adapter-static) — sinh bởi +# scripts/build-web.sh hoặc job `build-web` trong CI/release. +# KHÔNG commit: mọi clone sạch build lại từ web/ rồi nhúng qua rust-embed. +/assets/* +!/assets/.gitkeep diff --git a/crates/codegraph-web/Cargo.toml b/crates/codegraph-web/Cargo.toml new file mode 100644 index 0000000000..e1eff1fc03 --- /dev/null +++ b/crates/codegraph-web/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "codegraph-web" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Embedded Svelte web UI assets for the codegraph GraphQL server." + +[lints.rust] +warnings = "deny" + +[dependencies] +axum = { workspace = true } +rust-embed = { workspace = true } +mime_guess = "2" + +[dev-dependencies] +tower = { workspace = true, features = ["util"] } +tokio = { workspace = true, features = ["macros", "rt"] } diff --git a/crates/codegraph-web/assets/.gitkeep b/crates/codegraph-web/assets/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/codegraph-web/src/lib.rs b/crates/codegraph-web/src/lib.rs new file mode 100644 index 0000000000..c67475785d --- /dev/null +++ b/crates/codegraph-web/src/lib.rs @@ -0,0 +1,131 @@ +//! Web UI nhúng — phục vụ asset Svelte (đã build) từ trong binary. +//! +//! `codegraph serve --graphql` mount [`web_router`] làm fallback: mọi route +//! không phải `/graphql` / `/health` trả về app SPA. Asset được `rust-embed` +//! nhúng lúc compile từ `assets/` (build bằng `scripts/build-web.sh`; output +//! KHÔNG commit). Folder rỗng/thiếu vẫn compile được (`allow_missing`) — khi +//! đó fallback trả hướng dẫn build. + +use axum::body::Body; +use axum::http::{header, HeaderValue, StatusCode, Uri}; +use axum::response::{IntoResponse, Response}; +use axum::Router; +use rust_embed::RustEmbed; + +/// Asset nhúng — sinh từ `web/` bằng `scripts/build-web.sh` (hoặc job +/// `build-web` trong CI/release). KHÔNG commit output build (`assets/` nằm +/// trong `.gitignore`); `allow_missing` cho phép clone sạch — nơi folder chỉ +/// có `.gitkeep` hoặc chưa tồn tại — vẫn compile với embed rỗng. +#[derive(RustEmbed)] +#[folder = "assets"] +#[allow_missing = true] +struct WebAssets; + +/// Router phục vụ UI (mount làm fallback của server GraphQL). +/// +/// Mọi request GET không khớp file sẽ trả `200.html` (SPA shell) để router +/// phía client xử lý. Asset dưới `_app/immutable/` được cache vĩnh viễn +/// (tên có content-hash). +pub fn web_router() -> Router { + Router::new().fallback(serve) +} + +/// Có asset UI được nhúng không (folder build đã tồn tại lúc compile). +pub fn has_assets() -> bool { + WebAssets::get("index.html").is_some() || WebAssets::get("200.html").is_some() +} + +/// Chuẩn hoá đường dẫn → tên file trong `assets/`. +fn normalize(path: &str) -> String { + let p = path.trim_start_matches('/'); + if p.is_empty() { + "index.html".to_string() + } else { + p.to_string() + } +} + +/// Đọc một file asset, set content-type + cache-control. +fn asset_response(name: &str) -> Option { + let asset = WebAssets::get(name)?; + let mime = mime_guess::from_path(name).first_or_octet_stream(); + let cache = if name.starts_with("_app/immutable/") { + // content-hash trong tên → immutable. + "public, max-age=31536000, immutable" + } else { + "no-cache" + }; + Some( + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap()) + .header(header::CACHE_CONTROL, HeaderValue::from_static(cache)) + .body(Body::from(asset.data.into_owned())) + .unwrap(), + ) +} + +async fn serve(uri: Uri) -> Response { + let name = normalize(uri.path()); + + if let Some(res) = asset_response(&name) { + return res; + } + + // SPA fallback: shell cho client-side routing. + if let Some(res) = asset_response("200.html").or_else(|| asset_response("index.html")) { + return res; + } + + // Chưa build web — hướng dẫn. + ( + StatusCode::NOT_FOUND, + [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], + "Web UI chưa được build vào binary này.\n\ + Chạy `scripts/build-web.sh` rồi build lại codegraph.\n", + ) + .into_response() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn normalize_root_to_index() { + assert_eq!(normalize("/"), "index.html"); + assert_eq!(normalize(""), "index.html"); + assert_eq!(normalize("/_app/immutable/x.js"), "_app/immutable/x.js"); + } + + #[test] + fn has_assets_matches_built_folder() { + // Sau `scripts/build-web.sh` folder có index/200.html → true; clone sạch + // (chỉ .gitkeep) → false. Không assert cứng, chỉ đảm bảo không panic. + let _ = has_assets(); + } + + #[tokio::test] + async fn fallback_serves_shell_or_guidance() { + use axum::body::Body; + use axum::http::Request; + use tower::ServiceExt; + + let app = web_router(); + let res = app + .oneshot( + Request::builder() + .uri("/some/spa/route") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + // Có asset → 200 (shell); chưa build → 404 (hướng dẫn). Cả hai đều hợp lệ. + assert!( + res.status() == StatusCode::OK || res.status() == StatusCode::NOT_FOUND, + "unexpected status: {}", + res.status() + ); + } +} diff --git a/crates/codegraph/src/main.rs b/crates/codegraph/src/main.rs index 18e6a7df86..e13cd23969 100644 --- a/crates/codegraph/src/main.rs +++ b/crates/codegraph/src/main.rs @@ -103,6 +103,14 @@ enum Cmd { /// `--graphql` và `--mcp`/`--http`. #[arg(long)] mermaid: bool, + /// Tắt web UI nhúng (mặc định BẬT khi `--graphql`): UI Svelte phục vụ + /// tại `/`, gọi chính `/graphql`. Dùng `--no-web` để chỉ chạy API. + #[arg(long = "no-web")] + no_web: bool, + /// Module UI được bật (lặp được, phân cách dấu phẩy). Mặc định = tất + /// cả: explore, review, documents. Tắt module để thu gọn sidebar. + #[arg(long = "ui-modules")] + ui_modules: Vec, /// Serve qua Streamable HTTP (POST/GET/DELETE + SSE) thay vì stdio — /// mount ở cả `/` và `/mcp`. Default bind 0.0.0.0:8123 (docker-friendly). #[arg(long)] @@ -190,6 +198,8 @@ async fn main() -> Result<()> { mcp, graphql, mermaid, + no_web, + ui_modules, http, addr, allow_host, @@ -203,6 +213,8 @@ async fn main() -> Result<()> { mcp, graphql, mermaid, + no_web, + ui_modules, http, addr, allow_host, @@ -624,6 +636,8 @@ async fn cmd_serve( mcp: bool, graphql: bool, mermaid: bool, + no_web: bool, + ui_modules: Vec, http: bool, addr: std::net::SocketAddr, allow_host: Vec, @@ -662,6 +676,8 @@ async fn cmd_serve( format, allow_hosts: allowed, mermaid, + web: !no_web, + ui_modules, }; return codegraph_graphql::serve(cfg).await; } diff --git a/docs/development.md b/docs/development.md index 8849f11d46..3e818c51d3 100644 --- a/docs/development.md +++ b/docs/development.md @@ -263,6 +263,14 @@ RUST_LOG=codegraph_mcp=debug codegraph serve --mcp # Watcher debug RUST_LOG=codegraph=debug codegraph serve --mcp + +# Profiling [CGPROF] — đo từng stage của index rebuild (mặc định tắt). +# `serve --mcp` nói JSON-RPC qua stdout và stderr bị MCP host sở hữu (Claude +# Code, Codex… pipe stderr của tiến trình con vào log riêng), nên muốn đọc +# timing thì ghi ra file thay vì stderr. +CODEGRAPH_PROFILE=stderr codegraph serve --mcp # ra stderr (chạy tay trong terminal) +CODEGRAPH_PROFILE_FILE=/tmp/cgprof.log codegraph serve --mcp && tail -f /tmp/cgprof.log +CODEGRAPH_PROFILE=0 codegraph serve --mcp # tắt tường minh ``` --- diff --git a/packaging/aur/codegraph-rs-bin/PKGBUILD b/packaging/aur/codegraph-rs-bin/PKGBUILD index abd48ee187..a8ac08cde4 100644 --- a/packaging/aur/codegraph-rs-bin/PKGBUILD +++ b/packaging/aur/codegraph-rs-bin/PKGBUILD @@ -1,6 +1,6 @@ # Maintainer: Hung Pham pkgname=codegraph-rs-bin -pkgver=2.2.6 +pkgver=2.2.7 pkgrel=1 pkgdesc="Local-first code intelligence: tree-sitter knowledge graph + MCP server (prebuilt binary)" arch=('x86_64' 'aarch64') diff --git a/packaging/aur/codegraph-rs-git/PKGBUILD b/packaging/aur/codegraph-rs-git/PKGBUILD index 5c2d47e13f..e38997bdc3 100644 --- a/packaging/aur/codegraph-rs-git/PKGBUILD +++ b/packaging/aur/codegraph-rs-git/PKGBUILD @@ -7,7 +7,7 @@ arch=('x86_64' 'aarch64') url="https://github.com/hungpham10/codegraph-rs" license=('MIT') depends=('gcc-libs' 'sqlite') -makedepends=('rust' 'cargo' 'git') +makedepends=('rust' 'cargo' 'git' 'nodejs' 'npm') provides=('codegraph') conflicts=('codegraph' 'codegraph-rs-bin') source=("$pkgname::git+$url.git") @@ -33,6 +33,10 @@ build() { export LIBSQLITE3_SYS_USE_PKG_CONFIG=1 export CFLAGS+=" -ffat-lto-objects" export RUSTFLAGS+=" --remap-path-prefix=$srcdir=/build" + # Web UI (Svelte) được rust-embed nhúng lúc build codegraph; output không + # commit nên phải build trước từ web/ (xem crates/codegraph-web). + npm --prefix web ci --no-audit --no-fund + npm --prefix web run build cargo build --frozen --release -p codegraph } diff --git a/packaging/choco/codegraph.nuspec b/packaging/choco/codegraph.nuspec index dc9decb5d3..706becfda8 100644 --- a/packaging/choco/codegraph.nuspec +++ b/packaging/choco/codegraph.nuspec @@ -2,7 +2,7 @@ codegraph - 2.2.6 + 2.2.7 codegraph Hung Pham https://github.com/hungpham10/codegraph-rs diff --git a/packaging/homebrew/codegraph.rb.template b/packaging/homebrew/codegraph.rb.template index bfa4e50002..8afeaf9b9b 100644 --- a/packaging/homebrew/codegraph.rb.template +++ b/packaging/homebrew/codegraph.rb.template @@ -9,9 +9,14 @@ class Codegraph < Formula url "https://github.com/hungpham10/codegraph-rs/archive/refs/tags/@@TAG@@.tar.gz" sha256 "@@SRC_SHA@@" + depends_on "node" => :build depends_on "rust" => :build def install + # The Svelte UI is embedded at compile time by rust-embed but its build + # output is not committed, so build it first from web/. + system "npm", "--prefix", "web", "ci", "--no-audit", "--no-fund" + system "npm", "--prefix", "web", "run", "build" system "cargo", "build", "--release", "--locked", "-p", "codegraph" bin.install "target/release/codegraph" end diff --git a/packaging/winget/codegraph.yaml b/packaging/winget/codegraph.yaml index 5df680b725..0b5ef8f092 100644 --- a/packaging/winget/codegraph.yaml +++ b/packaging/winget/codegraph.yaml @@ -6,7 +6,7 @@ # release time (or automate it in the release pipeline before submitting to # microsoft/winget-pkgs). PackageIdentifier: hungpham10.codegraph -PackageVersion: 2.2.6 +PackageVersion: 2.2.7 PackageName: codegraph Publisher: Hung Pham PublisherUrl: https://github.com/hungpham10/codegraph-rs @@ -18,7 +18,7 @@ PackageUrl: https://github.com/hungpham10/codegraph-rs InstallerType: zip Installers: - Architecture: x64 - InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.6/codegraph-x86_64-pc-windows-msvc.zip + InstallerUrl: https://github.com/hungpham10/codegraph-rs/releases/download/v2.2.7/codegraph-x86_64-pc-windows-msvc.zip InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000 InstallerType: zip ManifestType: singleton diff --git a/scripts/build-web.sh b/scripts/build-web.sh new file mode 100755 index 0000000000..4a59c5afb4 --- /dev/null +++ b/scripts/build-web.sh @@ -0,0 +1,24 @@ +#!/bin/sh +# Build web UI (SvelteKit SPA) và ghi asset vào crate `codegraph-web` để +# rust-embed nhúng vào binary. Chạy trước `cargo build`/release. +set -eu + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WEB="$ROOT/web" +ASSETS="$ROOT/crates/codegraph-web/assets" + +if ! command -v npm >/dev/null 2>&1; then + echo "npm not found — install Node.js (https://nodejs.org) first." >&2 + exit 1 +fi + +echo "==> npm ci (web)" +npm --prefix "$WEB" ci --no-audit --no-fund + +echo "==> svelte-check" +npm --prefix "$WEB" run check + +echo "==> vite build → $ASSETS" +npm --prefix "$WEB" run build + +echo "==> done. Assets in $ASSETS" diff --git a/scripts/install.ps1 b/scripts/install.ps1 index 04b90f1898..c410c820ee 100644 --- a/scripts/install.ps1 +++ b/scripts/install.ps1 @@ -5,11 +5,11 @@ # # Usage (pin a version / download the script first): # irm https://raw.githubusercontent.com/hungpham10/codegraph-rs/main/scripts/install.ps1 -OutFile install.ps1 -# .\install.ps1 -Version 2.2.6 +# .\install.ps1 -Version 2.2.7 [CmdletBinding()] param( - # Pin a specific version, e.g. "2.2.6". Empty = latest release. + # Pin a specific version, e.g. "2.2.7". Empty = latest release. [string]$Version ) diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000000..3b462cb0c4 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/web/.npmrc b/web/.npmrc new file mode 100644 index 0000000000..b6f27f1359 --- /dev/null +++ b/web/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000000..68450d738d --- /dev/null +++ b/web/README.md @@ -0,0 +1,45 @@ +# codegraph web UI + +Giao diện **research / review code** cho codegraph — SvelteKit SPA (client-only), +Tailwind v4, mermaid. Backend là GraphQL của `codegraph serve --graphql`. + +## Modules + +| Route | Module | Mô tả | +|---|---|---| +| `/` | **Explore** | search symbol, xem flow (mermaid), callers/callees/impact, context markdown, files/status | +| `/review` | **Review** | dán unified diff → phân tích symbol/flow bị chạm + trace delta | +| `/documents` | **Documents** | duyệt/search/hydrate structured docs (YAML/JSON/TOML/HCL), ingest, mine patterns | + +## Develop + +```sh +npm install +npm run dev # http://localhost:5173 — proxy /graphql → 127.0.0.1:8123 +``` + +Backend (1 terminal khác): + +```sh +codegraph serve --graphql --mermaid --addr 127.0.0.1:8123 --path +``` + +Dev proxy đọc biến `CODEGRAPH_BACKEND` (mặc định `http://127.0.0.1:8123`). + +## Build & nhúng vào binary + +```sh +scripts/build-web.sh +``` + +Script chạy `npm ci` + `svelte-check` + `vite build`, ghi asset vào +`crates/codegraph-web/assets/` (gitignored). `rust-embed` nhúng folder đó lúc +`cargo build` → UI đi kèm binary, phục vụ tại `/` (same-origin, không CORS). + +Tắt UI khi chỉ cần API: `codegraph serve --graphql --no-web`. + +## Check + +```sh +npm run check # svelte-check +``` diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000000..54c856bf07 --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,3226 @@ +{ + "name": "web", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "web", + "version": "0.0.1", + "dependencies": { + "graphql-request": "^7.4.0", + "marked": "^18.1.0", + "mermaid": "^12.1.0" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^8.0.0", + "@sveltejs/adapter-static": "^4.0.0", + "@sveltejs/kit": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.6.4", + "svelte": "^5.57.1", + "svelte-check": "^4.6.0", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "vite": "^8.3.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-2.1.0.tgz", + "integrity": "sha512-sdg9NxU3zR4Mnawfbc/x6GB5Wf17WYud5qOuEuxXjaKpYpMkISSJEjItGebXJ2bQ4DIcly4NYH23mtkGJjvKUw==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-13.2.0.tgz", + "integrity": "sha512-bA9dvWlhAUcHkNwU+8w9yiUxJPlj/Lm3VqS7MD+NbIR/7LmpNLuK/AWNFdCo/T3SK5ND0/UHf3IJvl/Jn/i6yg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "13.2.0", + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-13.2.0.tgz", + "integrity": "sha512-bCARdAsG+eEbOveSXcOAC7pVxA04I1ionzeMOwIYf5NX7SaS7zgn4Dgug2BSqoL+iyKeMNSorQjsFSHJHsCNwg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "13.2.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-13.2.0.tgz", + "integrity": "sha512-kxpUkLtiaK09ph+dib+Yvm+3sWSnuwlo25gqfKRgTPNmF6H7uW4gVkMjaRdtnN783S9sM6vUNWh9CsxYX4QwKw==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-13.2.0.tgz", + "integrity": "sha512-7qrYHRlKfWf2AvBfJi5eVOrKAkbxvMFsy/iIHTGIiApNcEecCaVIdR+mIQhyCBsTDJxBz4JWXHMo6krbhR9hVg==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-13.2.0.tgz", + "integrity": "sha512-SzKYY68rseOrb9KILVzPsl6SjkQtdWw01SRGB5aCVI5kFcHltQsOYV3JB0Ffst72DgkvHCyDQ0NLwxtmCCf97Q==", + "license": "Apache-2.0" + }, + "node_modules/@graphql-typed-document-node/core": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@graphql-typed-document-node/core/-/core-3.2.0.tgz", + "integrity": "sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==", + "license": "MIT", + "peerDependencies": { + "graphql": "^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.7.tgz", + "integrity": "sha512-JZHlwdID+dy+lTgbYC8NEC4zeugqeYsc6jewvzb4c58kHauJn+X7rNwQjxz5p2qSjqaEeQoLkCIQ9v/H4PK0/w==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^2.0.1", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-2.0.1.tgz", + "integrity": "sha512-j37dj/h6p6pots1L7pLPPNuABEEu9WP4OYg9pW1g+nYG331j0+3canE9OagtYdytYvdSAMG+2fKU5cxep6LgjA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~13.2.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.152.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", + "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", + "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", + "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", + "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", + "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", + "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", + "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", + "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", + "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", + "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", + "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", + "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", + "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", + "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", + "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", + "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/adapter-auto": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-8.0.0.tgz", + "integrity": "sha512-CKOwUS865xfoZn3CpM868slPfitsQ4vmxAR/yOfTIguAwStgkhjhzV0zEhmkzPalH338LJ9XnQX9vfQeY7220A==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^3.0.0-next.0" + } + }, + "node_modules/@sveltejs/adapter-static": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-4.0.0.tgz", + "integrity": "sha512-aF1J23R1ebKqUxQ4/qLr/Lxu63NFmLAtg8UZl3HFLxaoA1D/nhTVGcD9lTqx65iUbjNR2iOL0TgOxSX82x6Paw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@sveltejs/kit": "^3.0.0-next.0" + } + }, + "node_modules/@sveltejs/kit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-3.0.1.tgz", + "integrity": "sha512-8JMCCq7ZUJELi3hbhb0+c9Rs8qnbHW90vhOfM+t1FSF4fhIpG9/FOsH4IYgcITMhbdkSrrn+Kvd9G+q8A+UO6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1", + "@standard-schema/spec": "^1.1.0", + "cookie": "^2.0.1", + "devalue": "^5.9.4", + "esm-env": "^1.2.2", + "magic-string": "^1.2.3", + "mrmime": "^2.0.1", + "sirv": "^3.0.2" + }, + "bin": { + "svelte-kit": "svelte-kit.js" + }, + "engines": { + "node": ">=22.17" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.0.0", + "@sveltejs/vite-plugin-svelte": "^7.0.0", + "svelte": "^5.57.1", + "typescript": "^6.0.0", + "vite": "^8.0.12" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tailwindcss/node": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.3.tgz", + "integrity": "sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.24.1", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.3" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@tailwindcss/node/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.3.tgz", + "integrity": "sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-arm64": "4.3.3", + "@tailwindcss/oxide-darwin-x64": "4.3.3", + "@tailwindcss/oxide-freebsd-x64": "4.3.3", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.3", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.3", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.3", + "@tailwindcss/oxide-linux-x64-musl": "4.3.3", + "@tailwindcss/oxide-wasm32-wasi": "4.3.3", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.3", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.3" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.3.tgz", + "integrity": "sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.3.tgz", + "integrity": "sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.3.tgz", + "integrity": "sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.3.tgz", + "integrity": "sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.3.tgz", + "integrity": "sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.3.tgz", + "integrity": "sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.3.tgz", + "integrity": "sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.3.tgz", + "integrity": "sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.3.tgz", + "integrity": "sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.3.tgz", + "integrity": "sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.3.tgz", + "integrity": "sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.3.3.tgz", + "integrity": "sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 20" + } + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.20", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.20.tgz", + "integrity": "sha512-hwbzQuNUfcPvbegQFatVPl/MY/tcM9KLl963hQ5laJKPh81TEZ1+dNG9PirGvcaDBkp+BCshExAyKVPW91dozw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || >=4.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.3.tgz", + "integrity": "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.3.3", + "@tailwindcss/oxide": "4.3.3", + "tailwindcss": "4.3.3" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7 || ^8" + } + }, + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w==", + "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==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "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==", + "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==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.12", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.12.tgz", + "integrity": "sha512-Qe/KWYhEiIIxGs7HrAAjMfShxKldx19SJtr5zu53f3afPsdZNz7HHtdTLXo/kqeiWNXVycI24kSnfzBYkTzpgw==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==", + "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==", + "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==", + "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==", + "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==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.9.tgz", + "integrity": "sha512-0sE1406XBYJGiqD3AusTl9ZqC//2mIXix51tbom25gDCA8ri4xnSZg28CaSE8Srl6FClqABUKfsn0qghgdepMA==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "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==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.6.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.4.tgz", + "integrity": "sha512-ldVPDCzj7fsaGZrLB0NuHuTvJcsNasysBAqMolr/cgxrLd1xbqxIr3XJiPnHHJUCxj5sNF1vnRj9aWnrVh5Jcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chevrotain": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-13.2.0.tgz", + "integrity": "sha512-GTouk5YzgBxlTPCaDn5q0q2EJKkrYmnPF+HmF8v0Qo4r3iyPG7HJivkN1wthCXZcm3sIcZFpGTRovLPNdJNRCA==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "13.2.0", + "@chevrotain/gast": "13.2.0", + "@chevrotain/regexp-to-ast": "13.2.0", + "@chevrotain/types": "13.2.0", + "@chevrotain/utils": "13.2.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cytoscape": { + "version": "3.34.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.3.tgz", + "integrity": "sha512-yfYGhRcGAntq6YBD583j4n0Eg3jIxvWmZtz/5uz9UYkeIStSlMxuUja+ec5j3iBD8nv1rwaOAYMW09tBdkSeaQ==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "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-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-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "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", + "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/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dompurify": { + "version": "3.4.16", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.16.tgz", + "integrity": "sha512-sqo+pNp3qRhCIpbgRi1y8Tgk27Bo2Ry7w0dC1NBeNTdZChWjz9Xb/KOoZbRP/R6pQZ80Qw8YhXw13hWWBbMRnQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/elkjs": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/elkjs/-/elkjs-0.9.3.tgz", + "integrity": "sha512-f/ZeWvW/BCXbhGEf1Ujp29EASo/lk1FDnETgNKwJrsVvGZhUWCZyg3xLJjAsxfOmt8KjswHmI5EwCQcPMpOYhQ==", + "license": "EPL-2.0" + }, + "node_modules/enhanced-resolve": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.26.0.tgz", + "integrity": "sha512-9vhedylFonb2YGogzUKX6+Ja72gOJbN1QHAqdrvqLwhdl/QWbKopzoUC9EbQNVsAns/bx/4uyqalrQAoy1IByw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "license": "MIT", + "peer": true, + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/graphql-request": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-7.4.0.tgz", + "integrity": "sha512-xfr+zFb/QYbs4l4ty0dltqiXIp07U6sl+tOKAb0t50/EnQek6CVVBLjETXi+FghElytvgaAWtIOt3EV7zLzIAQ==", + "license": "MIT", + "dependencies": { + "@graphql-typed-document-node/core": "^3.2.0" + }, + "peerDependencies": { + "graphql": "14 - 16" + } + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "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/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "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-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/marked": { + "version": "18.1.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.1.0.tgz", + "integrity": "sha512-PamYXWWWg2nboG3oX5Ffzpy3EFZROqZJK9hSlMmUnekp7TxPs5DcT95vwd46m73/exfKCRVMtgXoPMI4FEJL6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mermaid": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-12.1.0.tgz", + "integrity": "sha512-wlVCp+8eTupfCeeFvoZNNiTuHrvag0P2jz/ILgb/f/6jkVokUefOcujefi8qUe/j2asHiSePncVsz/xzzA80LQ==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^2.0.1", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "chevrotain": "~13.2.0", + "cytoscape": "^3.34.0", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.21", + "dompurify": "^3.4.12", + "elkjs": "^0.9.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.47", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/mermaid/node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/package-manager-detector": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", + "license": "MIT" + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, + "node_modules/rolldown": { + "version": "1.2.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", + "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.152.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.12", + "@rolldown/binding-android-arm64": "1.2.12", + "@rolldown/binding-darwin-arm64": "1.2.12", + "@rolldown/binding-darwin-x64": "1.2.12", + "@rolldown/binding-freebsd-x64": "1.2.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", + "@rolldown/binding-linux-arm64-gnu": "1.2.12", + "@rolldown/binding-linux-arm64-musl": "1.2.12", + "@rolldown/binding-linux-ppc64-gnu": "1.2.12", + "@rolldown/binding-linux-s390x-gnu": "1.2.12", + "@rolldown/binding-linux-x64-gnu": "1.2.12", + "@rolldown/binding-linux-x64-musl": "1.2.12", + "@rolldown/binding-openharmony-arm64": "1.2.12", + "@rolldown/binding-win32-arm64-msvc": "1.2.12", + "@rolldown/binding-win32-x64-msvc": "1.2.12" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "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/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "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/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/svelte": { + "version": "5.57.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.2.tgz", + "integrity": "sha512-1YiPCAXT0Ct6g1vOMmSLa0Nsc3QRMvtDF32ihgglGVrugIuIK38RyDiFNXVDhtQ81TJjSO57NdMw6DyYLqSGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz", + "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/tailwindcss": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.3.tgz", + "integrity": "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", + "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/vite": { + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.3.tgz", + "integrity": "sha512-cTAldKPImjg6c+gk48U19POPn3GCBzZwpdsN8ZMEEcbpes+6/wvfqUd0C2y3qYY4wsj8PwgFwrZ/1jBPVttMSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.11", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000000..50efe630bf --- /dev/null +++ b/web/package.json @@ -0,0 +1,37 @@ +{ + "name": "web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^8.0.0", + "@sveltejs/adapter-static": "^4.0.0", + "@sveltejs/kit": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^7.2.0", + "@tailwindcss/typography": "^0.5.20", + "@tailwindcss/vite": "^4.3.3", + "@types/node": "^26.6.4", + "svelte": "^5.57.1", + "svelte-check": "^4.6.0", + "tailwindcss": "^4.3.3", + "typescript": "^6.0.3", + "vite": "^8.3.0" + }, + "imports": { + "#lib": "./src/lib/index.js", + "#lib/*": "./src/lib/*" + }, + "dependencies": { + "graphql-request": "^7.4.0", + "marked": "^18.1.0", + "mermaid": "^12.1.0" + } +} diff --git a/web/src/app.css b/web/src/app.css new file mode 100644 index 0000000000..21cbf8f76d --- /dev/null +++ b/web/src/app.css @@ -0,0 +1,34 @@ +@import 'tailwindcss'; +@plugin '@tailwindcss/typography'; + +@theme { + --font-sans: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; + --font-mono: ui-monospace, 'SF Mono', 'JetBrains Mono', Menlo, Consolas, monospace; +} + +html { + color-scheme: dark; +} + +body { + @apply bg-neutral-950 text-neutral-200 font-sans antialiased; +} + +/* Scrollbar gọn cho panel code/diagram. */ +.thin-scroll::-webkit-scrollbar { + width: 8px; + height: 8px; +} +.thin-scroll::-webkit-scrollbar-thumb { + background: rgb(64 64 64 / 0.8); + border-radius: 9999px; +} +.thin-scroll::-webkit-scrollbar-track { + background: transparent; +} + +/* Mermaid render ra SVG — cho nền tối, canh giữa. */ +.mermaid-host svg { + max-width: 100%; + height: auto; +} diff --git a/web/src/app.d.ts b/web/src/app.d.ts new file mode 100644 index 0000000000..da08e6da59 --- /dev/null +++ b/web/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/web/src/app.html b/web/src/app.html new file mode 100644 index 0000000000..6a2bb58226 --- /dev/null +++ b/web/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/web/src/lib/assets/favicon.svg b/web/src/lib/assets/favicon.svg new file mode 100644 index 0000000000..cc5dc66a3a --- /dev/null +++ b/web/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/web/src/lib/components/ConnectPanel.svelte b/web/src/lib/components/ConnectPanel.svelte new file mode 100644 index 0000000000..bc158b4a05 --- /dev/null +++ b/web/src/lib/components/ConnectPanel.svelte @@ -0,0 +1,53 @@ + + +
+
+

Connect a workspace

+

+ Server đã sẵn sàng nhưng chưa bind workspace. Nhập đường dẫn tuyệt đối tới repo. +

+
+ + + {#if workspace.error} +
+ {workspace.error} +
+ {/if} + +
+
+
diff --git a/web/src/lib/components/DiffView.svelte b/web/src/lib/components/DiffView.svelte new file mode 100644 index 0000000000..445cb7644b --- /dev/null +++ b/web/src/lib/components/DiffView.svelte @@ -0,0 +1,24 @@ + + +
+ {#each lines as l, i (i)} +
{l.text || ' '}
+ {/each} +
diff --git a/web/src/lib/components/FileBrowser.svelte b/web/src/lib/components/FileBrowser.svelte new file mode 100644 index 0000000000..b8e347ff0a --- /dev/null +++ b/web/src/lib/components/FileBrowser.svelte @@ -0,0 +1,89 @@ + + +
+
+

Files

+ +
+ + {#if status} +
+ symbols{status.symbols} + chains{status.chains} + edges{status.edges} + files{status.files} +
+ {/if} + +
+ +
+ +
+ {#if loading && files.length === 0} +
Loading…
+ {:else} + {#each filtered as f (f.path)} +
+ {shortPath(f.path, 44)} + {f.lines}L · {formatBytes(f.bytes)} +
+ {/each} + {#if filtered.length === 0} +
No files.
+ {/if} + {/if} +
+
diff --git a/web/src/lib/components/MermaidView.svelte b/web/src/lib/components/MermaidView.svelte new file mode 100644 index 0000000000..0b53e60995 --- /dev/null +++ b/web/src/lib/components/MermaidView.svelte @@ -0,0 +1,63 @@ + + +
+
+ {title} + +
+
+ {#if error} +
+ Mermaid error: {error} +
{code}
+
+ {/if} +
+ {#if !rendered && !error} +
Rendering diagram…
+ {/if} +
+
diff --git a/web/src/lib/components/RelationshipList.svelte b/web/src/lib/components/RelationshipList.svelte new file mode 100644 index 0000000000..eb564d046d --- /dev/null +++ b/web/src/lib/components/RelationshipList.svelte @@ -0,0 +1,37 @@ + + +{#if title} +

+ {title} + ({items.length}) +

+{/if} +{#if items.length === 0} +
none
+{:else} +
+ {#each items as s (s.id)} + + {/each} +
+{/if} diff --git a/web/src/lib/components/Sidebar.svelte b/web/src/lib/components/Sidebar.svelte new file mode 100644 index 0000000000..bb6367bb45 --- /dev/null +++ b/web/src/lib/components/Sidebar.svelte @@ -0,0 +1,59 @@ + + +{#if items.length > 0} + +{/if} diff --git a/web/src/lib/components/SymbolDetail.svelte b/web/src/lib/components/SymbolDetail.svelte new file mode 100644 index 0000000000..de825bc566 --- /dev/null +++ b/web/src/lib/components/SymbolDetail.svelte @@ -0,0 +1,354 @@ + + +
+ +
+
+ {symbol.kind} +

{symbol.name}

+ {#if symbol.language} + {symbol.language} + {/if} +
+
+ {shortPath(symbol.file, 90)}:{symbol.line}{symbol.endLine + ? `–${symbol.endLine}` + : ''} +
+ {#if symbol.signature} +
{symbol.signature}
+ {/if} + {#if symbol.annotations?.length} +
+ {#each symbol.annotations as a (a.name + a.line)} + @{a.name} + {/each} +
+ {/if} + {#if symbol.doc} +

{symbol.doc}

+ {/if} +
+ + +
+ {#each [['graph', 'Graph'], ['flow', 'Flow'], ['structure', 'Structure'], ['context', 'Context']] as [id, label] (id)} + + {/each} +
+ {#each ['FLOW', 'CALLERS', 'CALLEES', 'IMPACT'] as k (k)} + + {/each} + {#if kind !== 'FLOW'} + + {/if} +
+
+ + +
+ {#if tab === 'graph'} +
+
+ {#if diagramLoading} +
Rendering…
+ {:else if diagramErr} +
+ {diagramErr} +
+ {:else if diagram} + + {/if} +
+ + {#if relCollapsed} + + {:else} +
+
+ +
+
+ + + +
+
+ {/if} +
+ {:else if tab === 'flow'} +
+ {#if !flow} +
Loading flow…
+ {:else} +
{flow.chainDesc.length} steps · {flow.calls.length} calls
+
    + {#each flow.chainDesc as d, i (i)} +
  1. + {i} + {d || '·'} +
  2. + {/each} +
+ {#if flow.calls.length} +

+ Calls +

+
+ {#each flow.calls as c (c.position)} +
+ {#if c.toId} + + {:else} + {c.toName} + ext + {/if} + L{c.line} + {#if c.condition}if {c.condition}{/if} + {#if EFFECT_LABELS[c.effect]} + {EFFECT_LABELS[c.effect]} + {/if} +
+ {/each} +
+ {/if} + {/if} +
+ {:else if tab === 'structure'} +
+ {#if classInfo} +

+ Methods +

+ +

+ Fields +

+ + {/if} + {#if scope} +

+ Parameters +

+ +

+ Locals +

+ + {/if} + {#if !classInfo && !scope} +
No structure info for this symbol.
+ {/if} +
+ {:else if tab === 'context'} +
+
+ +
+
+ {#if context} +
+ {@html renderMarkdown(context)} +
+ {:else} +
Loading context…
+ {/if} +
+
+ {/if} +
+
diff --git a/web/src/lib/components/SymbolList.svelte b/web/src/lib/components/SymbolList.svelte new file mode 100644 index 0000000000..03b9c9bafd --- /dev/null +++ b/web/src/lib/components/SymbolList.svelte @@ -0,0 +1,132 @@ + + +
+
+ +
+ {total} {kind.toLowerCase()}s +
+ + +
+
+
+ +
+ {#if error} +
{error}
+ {:else if loading && items.length === 0} +
Loading…
+ {:else if items.length === 0} +
No {kind.toLowerCase()}s.
+ {:else} +
+ {#each items as s (s.id)} + + {/each} +
+ {/if} +
+
diff --git a/web/src/lib/components/SymbolSearch.svelte b/web/src/lib/components/SymbolSearch.svelte new file mode 100644 index 0000000000..83647d22b4 --- /dev/null +++ b/web/src/lib/components/SymbolSearch.svelte @@ -0,0 +1,125 @@ + + +
+
+ e.key === 'Enter' && run()} + /> +
+ + + +
+
+ +
+ {#if error} +
{error}
+ {:else if searched && results.length === 0 && !loading} +
No symbols matched.
+ {:else} +
+ {#each results as s (s.id)} + + {/each} +
+ {#if total > results.length} +
+ Showing {results.length} of {total}. Narrow the query for more. +
+ {/if} + {/if} +
+
diff --git a/web/src/lib/components/TopBar.svelte b/web/src/lib/components/TopBar.svelte new file mode 100644 index 0000000000..ad0f8f34cb --- /dev/null +++ b/web/src/lib/components/TopBar.svelte @@ -0,0 +1,89 @@ + + +
+
+ codegraph + +
+ +
+ {#if workspace.status} + + {/if} + + + + {workspace.connected ? 'connected' : 'offline'} + + + + +
+
+ +{#if showSettings} +
+
+ + +
+ + +
+
+
+{/if} diff --git a/web/src/lib/format.ts b/web/src/lib/format.ts new file mode 100644 index 0000000000..4559e50211 --- /dev/null +++ b/web/src/lib/format.ts @@ -0,0 +1,45 @@ +/** Helpers hiển thị dùng chung. */ + +const KIND_COLORS: Record = { + FUNCTION: 'bg-sky-900/50 text-sky-300 border-sky-800', + METHOD: 'bg-sky-900/40 text-sky-300 border-sky-800', + CLASS: 'bg-violet-900/50 text-violet-300 border-violet-800', + INTERFACE: 'bg-violet-900/40 text-violet-300 border-violet-800', + ENUM: 'bg-fuchsia-900/40 text-fuchsia-300 border-fuchsia-800', + MODULE: 'bg-amber-900/40 text-amber-300 border-amber-800', + VARIABLE: 'bg-neutral-800 text-neutral-300 border-neutral-700', + CONSTANT: 'bg-neutral-800 text-neutral-300 border-neutral-700', + FIELD: 'bg-emerald-900/40 text-emerald-300 border-emerald-800', + PARAMETER: 'bg-emerald-900/30 text-emerald-300 border-emerald-800', + FILE: 'bg-neutral-800 text-neutral-400 border-neutral-700', + CONFIG: 'bg-neutral-800 text-neutral-400 border-neutral-700' +}; + +export function kindColor(kind: string): string { + return KIND_COLORS[kind] ?? 'bg-neutral-800 text-neutral-300 border-neutral-700'; +} + +export function formatBytes(n: number): string { + if (n < 1024) return `${n} B`; + if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`; + return `${(n / 1024 / 1024).toFixed(1)} MB`; +} + +export function shortPath(p: string, max = 48): string { + if (p.length <= max) return p; + return '…' + p.slice(p.length - max + 1); +} + +/** Effect → nhãn ngắn gọn. */ +export const EFFECT_LABELS: Record = { + NONE: '', + SQL_QUERY: 'sql', + SQL_WRITE: 'sql-write', + CACHE_READ: 'cache', + CACHE_WRITE: 'cache-write', + HTTP_CALL: 'http', + EVENT_EMIT: 'emit', + FILE_READ: 'read', + FILE_WRITE: 'write', + LOG: 'log' +}; diff --git a/web/src/lib/graphql/client.ts b/web/src/lib/graphql/client.ts new file mode 100644 index 0000000000..e1d7f74fe9 --- /dev/null +++ b/web/src/lib/graphql/client.ts @@ -0,0 +1,36 @@ +import { GraphQLClient } from 'graphql-request'; +import { get } from 'svelte/store'; +import { settings } from '#lib/settings'; + +/** + * Endpoint GraphQL. Mặc định = same-origin (`/graphql`) — đúng khi UI được + * nhúng trong binary (`codegraph serve --graphql`) và khi chạy dev qua Vite + * proxy. Có thể override bằng settings (localStorage). + */ +export function graphqlEndpoint(): string { + const { endpoint } = get(settings); + if (endpoint) return endpoint.replace(/\/+$/, ''); + if (typeof location !== 'undefined') return `${location.origin}/graphql`; + return 'http://127.0.0.1:8123/graphql'; +} + +function client(): GraphQLClient { + const { apiKey } = get(settings); + const headers: Record = {}; + if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`; + return new GraphQLClient(graphqlEndpoint(), { headers }); +} + +/** Chạy một GraphQL query/mutation, trả data đã typed (ném lỗi nếu có). */ +export async function gql( + document: string, + variables?: Record +): Promise { + return client().request(document, variables); +} + +/** Rút message lỗi từ exception graphql-request cho UI. */ +export function gqlError(e: unknown): string { + if (e instanceof Error) return e.message; + return String(e); +} diff --git a/web/src/lib/graphql/queries.ts b/web/src/lib/graphql/queries.ts new file mode 100644 index 0000000000..85b94a90db --- /dev/null +++ b/web/src/lib/graphql/queries.ts @@ -0,0 +1,370 @@ +// GraphQL documents + kiểu dữ liệu trả về của codegraph API. +// Enum async-graphql dùng SCREAMING_SNAKE_CASE (SymbolKind, ScopeLevel, …). + +export interface Annotation { + name: string; + line: number; + args: Record | null; +} + +export interface Symbol { + id: string; + name: string; + kind: string; + scope: string; + scopeId: string; + typeRef: string; + typeName: string | null; + file: string; + line: number; + endLine: number; + signature: string | null; + doc: string | null; + annotations: Annotation[]; + language: string; +} + +export interface SearchSymbolResult { + symbols: Symbol[]; + total: number; + timedOut: boolean; + resume: string | null; + indexVersion: number; +} + +export interface FileInfo { + path: string; + language: string; + bytes: number; + lines: number; +} + +export interface Status { + symbols: number; + chains: number; + edges: number; + files: number; + nextId: number; +} + +export interface MemberInfo { + id: string; + name: string; + kind: string; + line: number; + signature: string | null; +} + +export interface ClassInfo { + class: Symbol; + fields: MemberInfo[]; + methods: MemberInfo[]; +} + +export interface FunctionScope { + function: Symbol; + parameters: Symbol[]; + locals: Symbol[]; +} + +export interface Dependency { + name: string; + count: number; +} + +export interface DependenciesReport { + internal: Dependency[]; + external: Dependency[]; + total: number; +} + +export interface FlowCall { + position: number; + toName: string; + toId: string | null; + line: number; + condition: string | null; + effect: string; + effectDesc: string | null; + args: string[]; +} + +export interface FlowResult { + symbol: Symbol; + chain: string[]; + chainDesc: string[]; + calls: FlowCall[]; +} + +export interface SearchFlowResult { + functionId: string; + functionName: string; + chain: string[]; + matchCount: number; +} + +export interface CallSite { + callerId: string; + callName: string; + line: number; + condition: string | null; + isLoopBody: boolean; + argExprs: string[]; +} + +export interface CallSiteResult { + funcId: string; + funcName: string; + file: string; + callSites: CallSite[]; +} + +export interface ListResult { + items: Symbol[]; + total: number; + hasMore: boolean; +} + +export interface DocInfo { + docId: string; + path: string; + format: string; + rootNodeId: string; + nodes: number; +} + +export interface DocNode { + id: string; + path: string[]; + kind: string; + value: string | null; + key: string | null; + doc: string; + children: DocNode[]; +} + +export interface DocStats { + docs: number; + nodes: number; +} + +export interface DocPattern { + patternId: string; + tokens: string[]; + nodeCount: number; + docCount: number; + docFreq: number; +} + +const SYMBOL_FIELDS = ` + id name kind scope scopeId typeRef typeName file line endLine + signature doc language + annotations { name line args } +`; + +// ── Query lookups ── + +export const QS = { + status: `query { status { symbols chains edges files nextId } }`, + + uiConfig: `query { uiConfig }`, + + searchSymbol: ` + query Search($input: SearchSymbolInput!) { + searchSymbol(input: $input) { + symbols { ${SYMBOL_FIELDS} } + total timedOut resume indexVersion + } + }`, + + symbol: ` + query Symbol($id: ID) { + symbol(id: $id) { ${SYMBOL_FIELDS} } + }`, + + callers: ` + query Callers($id: ID!, $depth: Int) { + callers(id: $id, depth: $depth) { ${SYMBOL_FIELDS} } + }`, + + callees: ` + query Callees($id: ID!) { + callees(id: $id) { ${SYMBOL_FIELDS} } + }`, + + impact: ` + query Impact($id: ID!, $maxDepth: Int) { + impact(id: $id, maxDepth: $maxDepth) { ${SYMBOL_FIELDS} } + }`, + + flow: ` + query Flow($id: ID!) { + flow(id: $id) { + symbol { ${SYMBOL_FIELDS} } + chain chainDesc + calls { position toName toId line condition effect effectDesc args } + } + }`, + + mermaid: ` + query Mermaid($id: ID!, $kind: MermaidKind!, $depth: Int) { + mermaid(id: $id, kind: $kind, depth: $depth) + }`, + + context: ` + query Context($req: ContextRequestInput!) { + context(req: $req) + }`, + + files: ` + query Files($prefix: String) { + graphcodeFiles(prefix: $prefix) { path language bytes lines } + }`, + + classInfo: ` + query Class($id: ID!) { + graphcodeClass(id: $id) { + class { ${SYMBOL_FIELDS} } + fields { id name kind line signature } + methods { id name kind line signature } + } + }`, + + functionScope: ` + query Scope($id: ID!) { + graphcodeFunctionScope(id: $id) { + function { ${SYMBOL_FIELDS} } + parameters { ${SYMBOL_FIELDS} } + locals { ${SYMBOL_FIELDS} } + } + }`, + + listTypes: ` + query Types($kind: TypeKind!, $limit: Int, $offset: Int) { + graphcodeListTypes(kind: $kind, limit: $limit, offset: $offset) { + items { ${SYMBOL_FIELDS} } + total hasMore + } + }`, + + listSymbols: ` + query ListSymbols($kind: SymbolKind!, $limit: Int, $offset: Int) { + graphcodeListSymbols(kind: $kind, limit: $limit, offset: $offset) { + items { ${SYMBOL_FIELDS} } + total hasMore + } + }`, + + searchFlow: ` + query SearchFlow($pattern: String!, $limit: Int, $offset: Int) { + searchFlow(pattern: $pattern, limit: $limit, offset: $offset) { + results { functionId functionName chain matchCount } + total hasMore + } + }`, + + references: ` + query References($query: String!, $limit: Int) { + references(query: $query, limit: $limit) { + results { + funcId funcName file + callSites { callerId callName line condition isLoopBody argExprs } + } + total hasMore + } + }`, + + dependencies: ` + query Deps { + graphcodeDependencies { + internal { name count } + external { name count } + total + } + }`, + + gitBranches: `query { gitBranches }`, + + // ── Documents ── + docStats: `query { graphdocStats { docs nodes } }`, + + docList: ` + query { + graphdocList { docId path format rootNodeId nodes } + }`, + + docSearch: ` + query DocSearch($pattern: String!, $depth: Int) { + graphdocSearch(pattern: $pattern, depth: $depth) { + id path kind value key doc children { id path kind } + } + }`, + + docHydrate: ` + query DocHydrate($id: ID!, $depth: Int) { + graphdocHydrate(id: $id, maxDepth: $depth) { + id path kind value key doc + children { + id path kind value key + children { id path kind value key } + } + } + }`, + + docSearchValue: ` + query DocValue($query: String!, $limit: Int) { + graphdocSearchValue(query: $query, limit: $limit) { + id path kind value key doc + } + }`, + + docPatterns: ` + query { + graphdocListPatterns { patternId tokens nodeCount docCount docFreq } + }` +}; + +// ── Mutations ── + +export const MS = { + init: ` + mutation Init($path: String!, $index: Boolean) { + init(path: $path, index: $index) + }`, + deinit: `mutation { deinit }`, + index: `mutation { index }`, + diff: ` + mutation Diff($args: JSON!) { + graphcodeDiff(args: $args) + }`, + diffSimulate: ` + mutation DiffSim($args: JSON!) { + graphcodeDiffSimulate(args: $args) + }`, + originSimulate: ` + mutation OriginSim($args: JSON!) { + graphcodeOriginSimulate(args: $args) + }`, + branchCompare: ` + mutation BranchCompare($args: JSON!) { + graphcodeBranchCompare(args: $args) + }`, + + // ── Documents ── + docIngest: ` + mutation DocIngest($path: String!, $format: String) { + graphdocIngest(path: $path, format: $format) + }`, + docIngestDir: ` + mutation DocIngestDir($path: String!, $limit: Int) { + graphdocIngestDir(path: $path, limit: $limit) + }`, + docRemove: ` + mutation DocRemove($id: ID!) { + graphdocRemove(id: $id) + }`, + docMinePatterns: ` + mutation DocMine($topK: Int, $minCount: Int, $maxDepth: Int) { + graphdocMinePatterns(topK: $topK, minCount: $minCount, maxDepth: $maxDepth) + }` +}; diff --git a/web/src/lib/index.ts b/web/src/lib/index.ts new file mode 100644 index 0000000000..73c55efb4d --- /dev/null +++ b/web/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `#lib` alias in this folder. diff --git a/web/src/lib/markdown.ts b/web/src/lib/markdown.ts new file mode 100644 index 0000000000..f02c628e69 --- /dev/null +++ b/web/src/lib/markdown.ts @@ -0,0 +1,8 @@ +import { marked } from 'marked'; + +marked.setOptions({ gfm: true, breaks: false }); + +/** Render markdown (context output) thành HTML. */ +export function renderMarkdown(md: string): string { + return marked.parse(md ?? '') as string; +} diff --git a/web/src/lib/settings.ts b/web/src/lib/settings.ts new file mode 100644 index 0000000000..78ebe7ea25 --- /dev/null +++ b/web/src/lib/settings.ts @@ -0,0 +1,31 @@ +import { writable } from 'svelte/store'; + +const STORAGE_KEY = 'codegraph.settings'; + +export interface Settings { + /** Override endpoint GraphQL (rỗng = same-origin `/graphql`). */ + endpoint: string; + /** API key (`Authorization: Bearer …`) nếu server bật `--api-key`. */ + apiKey: string; +} + +function load(): Settings { + if (typeof localStorage === 'undefined') return { endpoint: '', apiKey: '' }; + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { endpoint: '', apiKey: '' }; + const parsed = JSON.parse(raw) as Partial; + return { endpoint: parsed.endpoint ?? '', apiKey: parsed.apiKey ?? '' }; + } catch { + return { endpoint: '', apiKey: '' }; + } +} + +/** Settings bền vững (localStorage) — endpoint + api key. */ +export const settings = writable(load()); + +if (typeof localStorage !== 'undefined') { + settings.subscribe((value) => { + localStorage.setItem(STORAGE_KEY, JSON.stringify(value)); + }); +} diff --git a/web/src/lib/workspace.svelte.ts b/web/src/lib/workspace.svelte.ts new file mode 100644 index 0000000000..8c5a83825e --- /dev/null +++ b/web/src/lib/workspace.svelte.ts @@ -0,0 +1,66 @@ +import { QS, MS } from '#lib/graphql/queries'; +import { gql, gqlError } from '#lib/graphql/client'; +import type { Status } from '#lib/graphql/queries'; + +/** + * Trạng thái workspace toàn cục (runes trong `.svelte.ts`). + * UI hỏi `status` để biết đã có index chưa; nếu chưa → panel Connect. + */ +class Workspace { + status = $state(null); + connected = $state(false); + loading = $state(false); + error = $state(null); + /** Root đã bind (suy ra từ lần connect gần nhất). */ + root = $state(null); + /** Module UI đang bật (từ config server). */ + uiModules = $state(['explore', 'review', 'documents']); + + /** Module này bật không? */ + hasModule(id: string): boolean { + return this.uiModules.includes(id); + } + + async loadUiConfig() { + try { + const data = await gql<{ uiConfig: string[] }>(QS.uiConfig); + if (data.uiConfig?.length) this.uiModules = data.uiConfig; + } catch { + // giữ mặc định + } + } + + async refresh() { + this.loading = true; + this.error = null; + try { + const data = await gql<{ status: Status }>(QS.status); + this.status = data.status; + this.connected = true; + } catch (e) { + this.connected = false; + this.status = null; + this.error = gqlError(e); + } finally { + this.loading = false; + } + } + + async init(path: string, index: boolean) { + this.loading = true; + this.error = null; + try { + await gql(MS.init, { path, index }); + this.root = path; + await this.refresh(); + } catch (e) { + this.connected = false; + this.error = gqlError(e); + throw e; + } finally { + this.loading = false; + } + } +} + +export const workspace = new Workspace(); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte new file mode 100644 index 0000000000..d1a8885a3b --- /dev/null +++ b/web/src/routes/+layout.svelte @@ -0,0 +1,24 @@ + + + + + codegraph · research cockpit + + +
+ +
+ +
+ {@render children()} +
+
+
diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts new file mode 100644 index 0000000000..87448c3d0c --- /dev/null +++ b/web/src/routes/+layout.ts @@ -0,0 +1,4 @@ +// SPA thuần client — không SSR/prerender (UI gọi GraphQL runtime). +export const ssr = false; +export const prerender = false; +export const csr = true; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte new file mode 100644 index 0000000000..b11d848f77 --- /dev/null +++ b/web/src/routes/+page.svelte @@ -0,0 +1,101 @@ + + +{#if !workspace.hasModule('explore')} +
+ Explore module is disabled. Enable it with --ui-modules explore. +
+{:else if !workspace.connected && !workspace.loading} + +{:else} +
+ + {#if !leftCollapsed} +
+
+ + + +
+
+ {#if tab === 'browse'} + (selected = s)} /> + {:else} + (selected = s)} selectedId={selected?.id ?? null} /> + {/if} +
+
+ {:else} + + {/if} + + +
+ {#if selected} + (selected = s)} /> + {:else} +
+

Chọn một symbol để xem flow, call graph và context.

+ +
+ {/if} +
+ + + {#if showFiles} +
+
+ +
+
+ +
+
+ {/if} +
+{/if} diff --git a/web/src/routes/documents/+page.svelte b/web/src/routes/documents/+page.svelte new file mode 100644 index 0000000000..2032017bb7 --- /dev/null +++ b/web/src/routes/documents/+page.svelte @@ -0,0 +1,293 @@ + + +{#if !workspace.hasModule('documents')} +
+ Documents module is disabled. Enable it with --ui-modules documents. +
+{:else} +
+ +
+
+

Documents

+ {#if stats} + + {stats.docs} docs · {stats.nodes} nodes + + {/if} + + +
+
+ + + + +
+ {#if notice} +
+ {notice} +
+ {/if} + {#if error} +
+ {error} +
+ {/if} +
+ +
+ +
+

+ Documents +

+ {#each docs as d (d.docId)} +
+ + +
+ {/each} + {#if docs.length === 0} +
Chưa có document. Ingest một file.
+ {/if} + + {#if patterns.length} +

+ Patterns +

+ {#each patterns as p (p.patternId)} +
+ {p.tokens.join(' › ')} + P{p.patternId} · {p.docCount} docs · freq {p.docFreq.toFixed(2)} +
+ {/each} + {/if} +
+ + +
+
+ + e.key === 'Enter' && search()} + /> + +
+ +
+ {#if hydrated} +
+

doc {hydrated.doc}

+ +
+
+ {@render node(hydrated, 0)} +
+ {:else if results.length} + {#each results as r (r.id)} + + {/each} + {:else} +
+ Chọn document bên trái hoặc search để xem cây giá trị. +
+ {/if} +
+
+
+
+{/if} + +{#snippet node(n: DocNode, depth: number)} + {@const pad = depth * 14} +
+ {n.key ?? (n.path.length ? n.path[n.path.length - 1] : '•')} + {#if n.value} + = + {n.value} + {:else} + ({n.kind}) + {/if} + {#each n.children as c (c.id)} + {@render node(c, depth + 1)} + {/each} +
+{/snippet} diff --git a/web/src/routes/review/+page.svelte b/web/src/routes/review/+page.svelte new file mode 100644 index 0000000000..e02614b1f6 --- /dev/null +++ b/web/src/routes/review/+page.svelte @@ -0,0 +1,308 @@ + + +{#if !workspace.hasModule('review')} +
+ Review module is disabled. Enable it with --ui-modules review. +
+{:else} +
+ +
+ + +
+ + {#if error} +
{error}
+ {/if} + + {#if mode === 'branches'} + +
+ + → + + + +
+ + {#if compareResult} + + {#if compareResult.report} + {@const s = compareResult.report.summary} +
+ files {s.filesMatched}/{s.filesInDiff} + symbols {s.symbolsAffected} + flows {s.flowsAffected} + {#if s.newFiles.length} + +{s.newFiles.length} new files + {/if} +
+
+ + +
+
+
+ {#if compareResult.baseNote} +
+ base index: {compareResult.baseNote} +
+ {/if} + {/if} + +
+ {#if view === 'graph' && compareResult.diagram} +
+
+ ▢ added + ▢ removed + mermaid · {base} → {head || 'working tree'} +
+
+ +
+
+ {:else} + + {/if} +
+ {:else} +
+ Chọn 2 branch rồi Compare. Điền entry để xem graph 2 màu. +
+ {/if} + {:else} + +
+
+
+ {#each [['diff', 'Analyze'], ['simulate', 'Simulate'], ['origin', 'Ref vs worktree']] as [m, label] (m)} + + {/each} +
+
+ + + +
+ +
+
+ {#if pasteResult} +
{prettyPaste}
+ {:else} +
Kết quả phân tích hiện ở đây.
+ {/if} +
+
+ {/if} +
+{/if} diff --git a/web/static/robots.txt b/web/static/robots.txt new file mode 100644 index 0000000000..b6dd6670cb --- /dev/null +++ b/web/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000000..eaa622679f --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "$app/tsconfig", + "compilerOptions": { + "strict": true, + "types": ["node", "$app/types"], + "paths": { + "#lib": ["./src/lib/index.ts"], + "#lib/*": ["./src/lib/*"] + } + }, + "include": ["src", "vite.config.ts"] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000000..20e4180f93 --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,47 @@ +import adapter from '@sveltejs/adapter-static'; +import { sveltekit } from '@sveltejs/kit/vite'; +import tailwindcss from '@tailwindcss/vite'; +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; + +// Ứng dụng là SPA client-only: adapter-static với fallback `200.html`. +// Khi serve trong binary Rust (crate `codegraph-web`) fallback này được phục +// vụ cho mọi route không khớp asset. `assets` ghi thẳng vào crate để +// rust-embed nhúng lúc `cargo build`. +// Backend GraphQL khi `codegraph serve --graphql` bind mặc định 0.0.0.0:8123. +// Dev proxy giữ UI same-origin (endpoint = location.origin) như lúc nhúng +// vào binary — tránh CORS và mô phỏng đúng môi trường production. +const backend = process.env.CODEGRAPH_BACKEND ?? 'http://127.0.0.1:8123'; + +export default defineConfig({ + resolve: { + alias: { + '#lib': fileURLToPath(new URL('./src/lib', import.meta.url)) + } + }, + server: { + proxy: { + '/graphql': { target: backend, changeOrigin: true }, + '/health': { target: backend, changeOrigin: true } + } + }, + plugins: [ + tailwindcss(), + sveltekit({ + compilerOptions: { + // Force runes mode for the project, except for libraries. Can be removed in svelte 6. + runes: ({ filename }) => + filename.split(/[/\\]/).includes('node_modules') ? undefined : true + }, + + // SPA: build ra static, không cần Node server lúc chạy. + adapter: adapter({ + pages: '../crates/codegraph-web/assets', + assets: '../crates/codegraph-web/assets', + fallback: '200.html', + precompress: false, + strict: false + }) + }) + ] +}); From 2315f0bd9b203ad202f72aacee39126aae3c9b03 Mon Sep 17 00:00:00 2001 From: hungpham10 <136320753+hungpham10@users.noreply.github.com> Date: Wed, 7 Oct 2026 06:47:10 +0000 Subject: [PATCH 2/3] style: apply rustfmt --- crates/codegraph-api/src/mermaid.rs | 16 +-- crates/codegraph-graph/src/lib.rs | 4 +- crates/codegraph-mcp/src/docgraph.rs | 1 - crates/codegraph-mcp/src/lib.rs | 9 +- crates/codegraph-source/src/git.rs | 6 +- crates/codegraph-web/src/lib.rs | 151 ++++++++++++++------------- 6 files changed, 100 insertions(+), 87 deletions(-) diff --git a/crates/codegraph-api/src/mermaid.rs b/crates/codegraph-api/src/mermaid.rs index 4a1e529275..083e201138 100644 --- a/crates/codegraph-api/src/mermaid.rs +++ b/crates/codegraph-api/src/mermaid.rs @@ -608,7 +608,15 @@ mod tests { let f = flow_labelled( chain, vec![ - "root", "IF_TRUE", "a", "BRANCH_END", "SWITCH_CASE", "b", "SWITCH_END", "LOOP", "c", + "root", + "IF_TRUE", + "a", + "BRANCH_END", + "SWITCH_CASE", + "b", + "SWITCH_END", + "LOOP", + "c", "LOOP_BACK", ], vec![ @@ -616,11 +624,7 @@ mod tests { call(5, "b", Some(2), 5), call(8, "c", Some(3), 8), ], - vec![ - (1, "x > 0"), - (4, "Cmd::Init"), - (7, "i < n"), - ], + vec![(1, "x > 0"), (4, "Cmd::Init"), (7, "i < n")], ); let m = control_flow(&f); assert!(m.contains("c1{\"IF_TRUE: x > 0\"}"), "{m}"); diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index 04632c6d16..d7b7829cc5 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -787,9 +787,7 @@ impl GraphIndex { // Đo bằng sink `codegraph_core::prof` — stderr bị MCP host nuốt mất // khi `serve --mcp`, nên log chỉ ra được khi bật qua env (xem prof.rs). macro_rules! cg_t { - ($n:expr) => {{ - codegraph_core::prof::stage($n) - }}; + ($n:expr) => {{ codegraph_core::prof::stage($n) }}; } macro_rules! cg_d { ($t:expr, $n:expr) => {{ diff --git a/crates/codegraph-mcp/src/docgraph.rs b/crates/codegraph-mcp/src/docgraph.rs index 991b315a6b..48fecfa4ed 100644 --- a/crates/codegraph-mcp/src/docgraph.rs +++ b/crates/codegraph-mcp/src/docgraph.rs @@ -2,4 +2,3 @@ //! và GraphQL dùng lại. Re-export để giữ API nội bộ ổn định. pub use codegraph_extract::SharedDocGraph; - diff --git a/crates/codegraph-mcp/src/lib.rs b/crates/codegraph-mcp/src/lib.rs index 27c2836c47..fb45a87a4d 100644 --- a/crates/codegraph-mcp/src/lib.rs +++ b/crates/codegraph-mcp/src/lib.rs @@ -504,8 +504,13 @@ impl CodegraphServer { } "codegraph_graphcode_origin_simulate" => { let git = codegraph_source::DiskGit::new(root.clone()); - codegraph_api::tools::dispatch_origin_simulate(&root, sgi.clone(), &git, args.clone()) - .await + codegraph_api::tools::dispatch_origin_simulate( + &root, + sgi.clone(), + &git, + args.clone(), + ) + .await } _ => { tools::dispatch_with_api( diff --git a/crates/codegraph-source/src/git.rs b/crates/codegraph-source/src/git.rs index 4a145a3981..f37447359e 100644 --- a/crates/codegraph-source/src/git.rs +++ b/crates/codegraph-source/src/git.rs @@ -112,7 +112,11 @@ impl GitRepo for DiskGit { } async fn branches(&self) -> Result> { - let text = self.run(&["branch", "--format=%(refname:short)", "--sort=-committerdate"])?; + let text = self.run(&[ + "branch", + "--format=%(refname:short)", + "--sort=-committerdate", + ])?; Ok(text .lines() .map(|l| l.trim().to_string()) diff --git a/crates/codegraph-web/src/lib.rs b/crates/codegraph-web/src/lib.rs index c67475785d..a911df7fdc 100644 --- a/crates/codegraph-web/src/lib.rs +++ b/crates/codegraph-web/src/lib.rs @@ -27,105 +27,108 @@ struct WebAssets; /// phía client xử lý. Asset dưới `_app/immutable/` được cache vĩnh viễn /// (tên có content-hash). pub fn web_router() -> Router { - Router::new().fallback(serve) + Router::new().fallback(serve) } /// Có asset UI được nhúng không (folder build đã tồn tại lúc compile). pub fn has_assets() -> bool { - WebAssets::get("index.html").is_some() || WebAssets::get("200.html").is_some() + WebAssets::get("index.html").is_some() || WebAssets::get("200.html").is_some() } /// Chuẩn hoá đường dẫn → tên file trong `assets/`. fn normalize(path: &str) -> String { - let p = path.trim_start_matches('/'); - if p.is_empty() { - "index.html".to_string() - } else { - p.to_string() - } + let p = path.trim_start_matches('/'); + if p.is_empty() { + "index.html".to_string() + } else { + p.to_string() + } } /// Đọc một file asset, set content-type + cache-control. fn asset_response(name: &str) -> Option { - let asset = WebAssets::get(name)?; - let mime = mime_guess::from_path(name).first_or_octet_stream(); - let cache = if name.starts_with("_app/immutable/") { - // content-hash trong tên → immutable. - "public, max-age=31536000, immutable" - } else { - "no-cache" - }; - Some( - Response::builder() - .status(StatusCode::OK) - .header(header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap()) - .header(header::CACHE_CONTROL, HeaderValue::from_static(cache)) - .body(Body::from(asset.data.into_owned())) - .unwrap(), - ) + let asset = WebAssets::get(name)?; + let mime = mime_guess::from_path(name).first_or_octet_stream(); + let cache = if name.starts_with("_app/immutable/") { + // content-hash trong tên → immutable. + "public, max-age=31536000, immutable" + } else { + "no-cache" + }; + Some( + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + HeaderValue::from_str(mime.as_ref()).unwrap(), + ) + .header(header::CACHE_CONTROL, HeaderValue::from_static(cache)) + .body(Body::from(asset.data.into_owned())) + .unwrap(), + ) } async fn serve(uri: Uri) -> Response { - let name = normalize(uri.path()); + let name = normalize(uri.path()); - if let Some(res) = asset_response(&name) { - return res; - } + if let Some(res) = asset_response(&name) { + return res; + } - // SPA fallback: shell cho client-side routing. - if let Some(res) = asset_response("200.html").or_else(|| asset_response("index.html")) { - return res; - } + // SPA fallback: shell cho client-side routing. + if let Some(res) = asset_response("200.html").or_else(|| asset_response("index.html")) { + return res; + } - // Chưa build web — hướng dẫn. - ( - StatusCode::NOT_FOUND, - [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], - "Web UI chưa được build vào binary này.\n\ + // Chưa build web — hướng dẫn. + ( + StatusCode::NOT_FOUND, + [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], + "Web UI chưa được build vào binary này.\n\ Chạy `scripts/build-web.sh` rồi build lại codegraph.\n", - ) - .into_response() + ) + .into_response() } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[test] - fn normalize_root_to_index() { - assert_eq!(normalize("/"), "index.html"); - assert_eq!(normalize(""), "index.html"); - assert_eq!(normalize("/_app/immutable/x.js"), "_app/immutable/x.js"); - } + #[test] + fn normalize_root_to_index() { + assert_eq!(normalize("/"), "index.html"); + assert_eq!(normalize(""), "index.html"); + assert_eq!(normalize("/_app/immutable/x.js"), "_app/immutable/x.js"); + } - #[test] - fn has_assets_matches_built_folder() { - // Sau `scripts/build-web.sh` folder có index/200.html → true; clone sạch - // (chỉ .gitkeep) → false. Không assert cứng, chỉ đảm bảo không panic. - let _ = has_assets(); - } + #[test] + fn has_assets_matches_built_folder() { + // Sau `scripts/build-web.sh` folder có index/200.html → true; clone sạch + // (chỉ .gitkeep) → false. Không assert cứng, chỉ đảm bảo không panic. + let _ = has_assets(); + } - #[tokio::test] - async fn fallback_serves_shell_or_guidance() { - use axum::body::Body; - use axum::http::Request; - use tower::ServiceExt; + #[tokio::test] + async fn fallback_serves_shell_or_guidance() { + use axum::body::Body; + use axum::http::Request; + use tower::ServiceExt; - let app = web_router(); - let res = app - .oneshot( - Request::builder() - .uri("/some/spa/route") - .body(Body::empty()) - .unwrap(), - ) - .await - .unwrap(); - // Có asset → 200 (shell); chưa build → 404 (hướng dẫn). Cả hai đều hợp lệ. - assert!( - res.status() == StatusCode::OK || res.status() == StatusCode::NOT_FOUND, - "unexpected status: {}", - res.status() - ); - } + let app = web_router(); + let res = app + .oneshot( + Request::builder() + .uri("/some/spa/route") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + // Có asset → 200 (shell); chưa build → 404 (hướng dẫn). Cả hai đều hợp lệ. + assert!( + res.status() == StatusCode::OK || res.status() == StatusCode::NOT_FOUND, + "unexpected status: {}", + res.status() + ); + } } From 6bb556cf097ad9981ca610f461b3ade840cbdd77 Mon Sep 17 00:00:00 2001 From: Hung Pham Date: Wed, 7 Oct 2026 14:16:23 +0700 Subject: [PATCH 3/3] =?UTF-8?q?fix(graph):=20sibling=5Ffile=20gi=E1=BB=AF?= =?UTF-8?q?=20th=C6=B0=20m=E1=BB=A5c=20cha=20tr=C3=AAn=20Windows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Path Windows dùng '\\' nên name_dsn tạo 'names.sqlite' ở cwd thay vì cạnh db.sqlite → name_engine_stamp_survives_reopen fail trên CI Windows. Xử lý cả '/' lẫn '\\' + thêm unit test hồi quy. --- crates/codegraph-graph/src/lib.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/codegraph-graph/src/lib.rs b/crates/codegraph-graph/src/lib.rs index d7b7829cc5..d138a380f9 100644 --- a/crates/codegraph-graph/src/lib.rs +++ b/crates/codegraph-graph/src/lib.rs @@ -169,11 +169,17 @@ fn name_dsn(main_dsn: &str) -> Option { } /// Thay tên file cuối của `path` (giữ nguyên thư mục cha) — `names.lmdb` -/// đặt cạnh `db.lmdb` dù `a/b/db.lmdb` → `a/b/names.lmdb`. +/// đặt cạnh `db.lmdb` dù `a/b/db.lmdb` → `a/b/names.lmdb`. Xử lý **cả** dấu +/// phân cách `/` (Unix/DSN) lẫn `\` (Windows) để path `:\...\db.sqlite` +/// giữ nguyên thư mục cha thay vì rơi về cwd. #[cfg(any(feature = "sqlite", feature = "lmdb", feature = "redis"))] fn sibling_file(path: &str, file: &str) -> String { - match path.rfind('/') { - Some(i) => format!("{}/{}", &path[..i], file), + let sep = match (path.rfind('/'), path.rfind('\\')) { + (Some(a), Some(b)) => Some(a.max(b)), + (a, b) => a.or(b), + }; + match sep { + Some(i) => format!("{}{}", &path[..=i], file), None => file.to_string(), } } @@ -2907,6 +2913,20 @@ mod tests { Annotation, MARKER_BRANCH_END, MARKER_IF_TRUE, MARKER_LOOP, MARKER_LOOP_BACK, ScopeLevel, }; + #[test] + #[cfg(any(feature = "sqlite", feature = "lmdb", feature = "redis"))] + fn sibling_file_handles_both_separators() { + // Unix / DSN style. + assert_eq!(sibling_file("db.sqlite", "names.sqlite"), "names.sqlite"); + assert_eq!(sibling_file("a/b/db.lmdb", "names.lmdb"), "a/b/names.lmdb"); + // Windows backslash must keep the parent dir (regression: + // name_engine_stamp_survives_reopen failed on windows). + assert_eq!( + sibling_file(r"C:\repo\.codegraph\db.sqlite", "names.sqlite"), + r"C:\repo\.codegraph\names.sqlite" + ); + } + fn sym(file: &str, name: &str, id: u64) -> Symbol { Symbol { id,