diff --git a/.github/actions/setup-mux/action.yml b/.github/actions/setup-mux/action.yml deleted file mode 100644 index 284be53..0000000 --- a/.github/actions/setup-mux/action.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: "Setup Mux" -description: "Setup Bun and install dependencies with caching" -runs: - using: "composite" - steps: - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Get Bun version - id: bun-version - shell: bash - run: echo "version=$(bun --version)" >> $GITHUB_OUTPUT - - - name: Cache node_modules - id: cache-node-modules - uses: actions/cache@v4 - with: - path: node_modules - key: ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.bun-version.outputs.version }}-node-modules-${{ hashFiles('**/bun.lock') }} - restore-keys: | - ${{ runner.os }}-${{ runner.arch }}-bun-${{ steps.bun-version.outputs.version }}-node-modules- - - - name: Cache bun install cache - if: steps.cache-node-modules.outputs.cache-hit != 'true' - id: cache-bun-install - uses: actions/cache@v4 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-cache-${{ hashFiles('**/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-cache- - - - name: Install dependencies - if: steps.cache-node-modules.outputs.cache-hit != 'true' - shell: bash - run: bun install --frozen-lockfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c30cc53..fdb9b1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,21 +6,24 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: ci: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2 with: - bun-version: latest + bun-version: 1.4.0 - name: Install dependencies - run: bun install + run: bun install --frozen-lockfile - name: Check formatting run: bun run fmt:check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 4e31e4b..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*" - workflow_dispatch: - inputs: - version: - description: "Version to release (e.g., 0.1.0)" - required: true - -permissions: - contents: write - -jobs: - release: - strategy: - matrix: - include: - - os: macos-latest - platform: mac - - os: ubuntu-latest - platform: linux - - os: windows-latest - platform: win - - runs-on: ${{ matrix.os }} - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Bun - uses: oven-sh/setup-bun@v2 - with: - bun-version: latest - - - name: Install dependencies - run: bun install - - - name: Build browser bundle - run: bun run build:browser - - - name: Generate icons - run: bun run icons - - # macOS: Import certificates for code signing - - name: Import macOS certificates - if: matrix.platform == 'mac' - env: - MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} - MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - MACOS_KEYCHAIN_PWD: ${{ secrets.MACOS_KEYCHAIN_PWD }} - run: | - if [ -n "$MACOS_CERTIFICATE" ]; then - echo "Importing macOS certificate..." - echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12 - security create-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain - security default-keychain -s build.keychain - security unlock-keychain -p "$MACOS_KEYCHAIN_PWD" build.keychain - security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign - security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_KEYCHAIN_PWD" build.keychain - rm certificate.p12 - else - echo "No macOS certificate provided, skipping code signing" - fi - - # Build and publish - - name: Build Electron app - run: bun run scripts/build-electron.ts - - - name: Publish to GitHub Releases - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - CSC_LINK: ${{ secrets.MACOS_CERTIFICATE }} - CSC_KEY_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }} - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - WIN_CSC_LINK: ${{ secrets.WIN_CERTIFICATE }} - WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CERTIFICATE_PWD }} - run: | - # Unset empty signing vars so electron-builder ignores them - [ -z "$CSC_LINK" ] && unset CSC_LINK CSC_KEY_PASSWORD && export CSC_IDENTITY_AUTO_DISCOVERY=false - [ -z "$APPLE_ID" ] && unset APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID - [ -z "$WIN_CSC_LINK" ] && unset WIN_CSC_LINK WIN_CSC_KEY_PASSWORD - - npx electron-builder --config electron-builder.json --${{ matrix.platform }} --publish always - - # Upload artifacts for debugging - - name: Upload artifacts - uses: actions/upload-artifact@v4 - with: - name: release-${{ matrix.platform }} - path: | - release/*.dmg - release/*.zip - release/*.exe - release/*.AppImage - release/*.tar.gz - release/*.yml - if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index 299a0b9..e2a2dfb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,10 +13,6 @@ # testing /coverage -# next.js -/.next/ -/out/ - # production /build @@ -33,12 +29,8 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* -# vercel -.vercel - # typescript *.tsbuildinfo -next-env.d.ts dist/ release/ diff --git a/AGENTS.md b/AGENTS.md index 8020428..fb79585 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,6 +1,6 @@ -# pulldash +# better pr -Pulldash is the fastest way to review pull requests. +better pr is the fastest way to review pull requests. ## Development @@ -10,9 +10,13 @@ _NEVER_ use browser tools - they will not work with this project. ## Principles -- Performance is P1. If things feel laggy or are not smooth, it is of the utmost importance to fix. Pulldash exists because GitHub's PR review is slow. +- Performance is P1. If things feel laggy or are not smooth, it is of the utmost importance to fix. better pr exists because GitHub's PR review is slow. - Tests should primarily occur at the data-layer, and the frontend should mostly dummily render the data. +## Changelog + +Any user-visible feature or change must add an entry (newest first) to `CHANGELOG` in `src/browser/lib/changelog.ts`. It powers the "What's new" dialog; users see a dot until they open it. Use a unique `id` like `YYYY-MM-DD-slug`. + ## Testing Tests should be minimal, not conflict with each other, and not be race-prone. diff --git a/README.md b/README.md index 1d90a3d..043e1b6 100644 --- a/README.md +++ b/README.md @@ -1,62 +1,36 @@ -

- pulldash logo - Pulldash -

+# better pr -![GitHub Release](https://img.shields.io/github/v/release/coder/pulldash) ![GitHub License](https://img.shields.io/github/license/coder/pulldash) ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/coder/pulldash/ci.yml) +A fast, keyboard-driven PR review UI. -Fast, filterable PR review. Entirely client-side. +## Setup -> [!WARNING] -> Pulldash is WIP. Expect bugs. +Requires [Bun](https://bun.sh). -## Try It - -**Browser**: [pulldash.com](https://pulldash.com). Replace `github.com` with `pulldash.com` in any PR URL. - -**Desktop**: [Latest release](https://github.com/coder/pulldash/releases) for Linux, macOS, Windows. - -[![Example](./docs/screenshots/overview.png)](https://pulldash.com) - -## Features - -- **Custom filters**: Add repos and filter by review requests, authored PRs, or all activity. - - ![Filtering PRs](./docs/screenshots/filtering.png) - -- **Keyboard-driven**: `j`/`k` to navigate files, arrows for lines, `c` to comment, `s` to submit. - - ![Keybinds](./docs/screenshots/keybind-driven.png) - -- **Fast file search**: `Ctrl+K` to fuzzy-find across hundreds of changed files. - - ![Search](./docs/screenshots/search.png) - -## Why +```bash +git clone git@github.com:tskimmett/pulldash.git better-pr +cd better-pr +bun install +bun start +``` -- GitHub's review UI is slow (especially for large diffs) -- No central view to filter PRs you care about -- AI tooling has produced more PRs than ever before—making a snappy review UI essential +Open http://localhost:3002 (set `PORT` to change it). To update: `git pull && bun install && bun start`. -## How It Works +## Sign in -GitHub's API supports [CORS](https://docs.github.com/en/rest/using-the-rest-api/using-cors-and-jsonp-to-make-cross-origin-requests), so Pulldash runs entirely client-side. No backend proxying your requests. +Use a **personal access token**: GitHub → Settings → Developer settings. It needs `repo` scope. The token stays in your browser's `localStorage`. -- **Web Worker pool**: Diff parsing and syntax highlighting run in workers sized to `navigator.hardwareConcurrency`. The main thread stays free for scrolling. +For Cognito repositories, the PAT must also have SSO configured and authorized for the Cognito organization. -- **Pre-computed navigation**: When a diff loads, we index all navigable lines. Arrow keys are O(1)—no DOM queries. +## Semantic review (optional, experimental) -- **External store**: State lives outside React ([`useSyncExternalStore`](https://react.dev/reference/react/useSyncExternalStore)). Focusing line 5000 doesn't re-render the file tree. +This feature is experimental and the results are hit-or-miss. Treat the walkthrough as a rough starting point, not a reliable summary of the PR. -- **Virtualized rendering**: Diffs, file lists, and the command palette only render visible rows. +The AI walkthrough appears only if an agent is available on the machine running the server: Claude Code logged in (or `ANTHROPIC_API_KEY`), or Codex logged in (or `OPENAI_API_KEY`). Diffs are sent to the provider you pick. -## Development +## Deploy -```bash -bun install -bun dev -``` +`bun run deploy` builds the app and uploads `dist/browser` to an Azure Static Web App. It needs `az login` plus `AZURE_SUBSCRIPTION`, `SWA_RESOURCE_GROUP` and `SWA_APP_NAME` (Bun reads them from `.env`). The hosted build has no server, so semantic review is unavailable there. ## License -[AGPL](./LICENSE) +[AGPL-3.0](./LICENSE) diff --git a/bun.lock b/bun.lock index b21a627..c4fe444 100644 --- a/bun.lock +++ b/bun.lock @@ -5,19 +5,22 @@ "": { "name": "pulldash", "dependencies": { + "@openai/codex-sdk": "^0.156.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-tabs": "^1.1.13", + "mermaid": "^11.17.2", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", "remark-gemoji": "^8.0.0", + "vscode-diff": "^3.0.1", }, "devDependencies": { - "@electron/asar": "^4.0.1", + "@anthropic-ai/claude-agent-sdk": "^0.3.282", "@hono/node-server": "^1.19.6", "@octokit/core": "^7.0.6", "@radix-ui/react-collapsible": "^1.1.12", @@ -36,15 +39,9 @@ "cmdk": "^1.1.1", "concurrently": "^9.2.1", "diff": "^8.0.2", - "electron": "^39.2.4", - "electron-builder": "^26.0.12", - "electron-updater": "^6.6.2", - "geist": "^1.5.1", "gitdiff-parser": "^0.3.1", "hono": "^4.10.7", "lucide-react": "^0.555.0", - "png2icons": "^2.0.1", - "posthog-js": "^1.300.0", "prettier": "^3.7.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -52,91 +49,41 @@ "react-router-dom": "^7.9.6", "refractor": "^5.0.0", "remark-gfm": "^4.0.1", - "sharp": "^0.34.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^4", - "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", "typescript": "^5", }, }, }, "packages": { - "7zip-bin": ["7zip-bin@5.2.0", "", {}, "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A=="], + "@antfu/install-pkg": ["@antfu/install-pkg@1.1.0", "", { "dependencies": { "package-manager-detector": "^1.3.0", "tinyexec": "^1.0.1" } }, "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ=="], - "@develar/schema-utils": ["@develar/schema-utils@2.6.5", "", { "dependencies": { "ajv": "^6.12.0", "ajv-keywords": "^3.4.1" } }, "sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.282", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.282", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.282", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.282", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.282", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.282", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.282", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.282", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.282" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-6UAerS1udzndLEx+0XW3gQWiICgfu/a+2fx/aLY3gUy+1JUQESbwYkhR40+D6d+yjueCslkLmvkPlZQFZDph6A=="], - "@electron/asar": ["@electron/asar@4.0.1", "", { "dependencies": { "commander": "^13.1.0", "glob": "^11.0.1", "minimatch": "^10.0.1" }, "bin": { "asar": "bin/asar.mjs" } }, "sha512-F4Ykm1jiBGY1WV/o8Q8oFW8Nq0u+S2/vPujzNJtdSJ6C4LHC4CiGLn7c17s7SolZ23gcvCebMncmZtNc+MkxPQ=="], + "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.282", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NwvIIpEOORp16K7NgjWE2lt2Y+VmddvkkRoOpyKXz2H2bRixa5ToV+byp2gUaKVsCixBuAiQ2lL9e79gy5lbNQ=="], - "@electron/fuses": ["@electron/fuses@1.8.0", "", { "dependencies": { "chalk": "^4.1.1", "fs-extra": "^9.0.1", "minimist": "^1.2.5" }, "bin": { "electron-fuses": "dist/bin.js" } }, "sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw=="], + "@anthropic-ai/claude-agent-sdk-darwin-x64": ["@anthropic-ai/claude-agent-sdk-darwin-x64@0.3.282", "", { "os": "darwin", "cpu": "x64" }, "sha512-MjVmeojCOIAdqt90j1B9ENfCwLIzMhSyzQJd6+rBn8KDKklduPLbs//rVhvIwR+MJBF67PDkczOb4VXiYPJRxA=="], - "@electron/get": ["@electron/get@2.0.3", "", { "dependencies": { "debug": "^4.1.1", "env-paths": "^2.2.0", "fs-extra": "^8.1.0", "got": "^11.8.5", "progress": "^2.0.3", "semver": "^6.2.0", "sumchecker": "^3.0.1" }, "optionalDependencies": { "global-agent": "^3.0.0" } }, "sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ=="], + "@anthropic-ai/claude-agent-sdk-linux-arm64": ["@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.282", "", { "os": "linux", "cpu": "arm64" }, "sha512-hL/G7pCQnS9m8RIqxCpljaQsvITKRYdqoNeDsJGIqaFpd79redTwlovRUBtFMeDAtASwtha1wLyh24v+gUb58A=="], - "@electron/node-gyp": ["@electron/node-gyp@github:electron/node-gyp#06b29aa", { "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", "glob": "^8.1.0", "graceful-fs": "^4.2.6", "make-fetch-happen": "^10.2.1", "nopt": "^6.0.0", "proc-log": "^2.0.1", "semver": "^7.3.5", "tar": "^6.2.1", "which": "^2.0.2" }, "bin": "./bin/node-gyp.js" }, "electron-node-gyp-06b29aa"], + "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": ["@anthropic-ai/claude-agent-sdk-linux-arm64-musl@0.3.282", "", { "os": "linux", "cpu": "arm64" }, "sha512-2/3EtJKfNQfjn1MtTRh7EpcqlAI/UCEaiHSdlPH+AZPDnQXiHLW6JITJNdsc3MMuwjnbsiL4NJBnDr35qkIb9A=="], - "@electron/notarize": ["@electron/notarize@2.5.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.1", "promise-retry": "^2.0.1" } }, "sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A=="], + "@anthropic-ai/claude-agent-sdk-linux-x64": ["@anthropic-ai/claude-agent-sdk-linux-x64@0.3.282", "", { "os": "linux", "cpu": "x64" }, "sha512-NxinW4k1Xh3xTCPKpsw+XXV+O3zwLtLQeJnFucc/LKQQ3p4pXJ7/hW/DIiYdmhgIIaMFkP+2xP8G5+Y8swxB2w=="], - "@electron/osx-sign": ["@electron/osx-sign@1.3.1", "", { "dependencies": { "compare-version": "^0.1.2", "debug": "^4.3.4", "fs-extra": "^10.0.0", "isbinaryfile": "^4.0.8", "minimist": "^1.2.6", "plist": "^3.0.5" }, "bin": { "electron-osx-flat": "bin/electron-osx-flat.js", "electron-osx-sign": "bin/electron-osx-sign.js" } }, "sha512-BAfviURMHpmb1Yb50YbCxnOY0wfwaLXH5KJ4+80zS0gUkzDX3ec23naTlEqKsN+PwYn+a1cCzM7BJ4Wcd3sGzw=="], + "@anthropic-ai/claude-agent-sdk-linux-x64-musl": ["@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.282", "", { "os": "linux", "cpu": "x64" }, "sha512-Q96hallUSHCCvmsMC08aO3YtivTbRIzzrgfdVjJAcOrQ+6tSwN0Ni4yMEwhWRnq3DVZlNDYIssd1/k7H4lS7Og=="], - "@electron/rebuild": ["@electron/rebuild@3.7.0", "", { "dependencies": { "@electron/node-gyp": "git+https://github.com/electron/node-gyp.git#06b29aafb7708acef8b3669835c8a7857ebc92d2", "@malept/cross-spawn-promise": "^2.0.0", "chalk": "^4.0.0", "debug": "^4.1.1", "detect-libc": "^2.0.1", "fs-extra": "^10.0.0", "got": "^11.7.0", "node-abi": "^3.45.0", "node-api-version": "^0.2.0", "ora": "^5.1.0", "read-binary-file-arch": "^1.0.6", "semver": "^7.3.5", "tar": "^6.0.5", "yargs": "^17.0.1" }, "bin": { "electron-rebuild": "lib/cli.js" } }, "sha512-VW++CNSlZwMYP7MyXEbrKjpzEwhB5kDNbzGtiPEjwYysqyTCF+YbNJ210Dj3AjWsGSV4iEEwNkmJN9yGZmVvmw=="], + "@anthropic-ai/claude-agent-sdk-win32-arm64": ["@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.282", "", { "os": "win32", "cpu": "arm64" }, "sha512-4v9guW9NSDJj+cTP+aND3Wz6gAFV5OU/VP4CFjc79KgdwPpAtwm2fw5BKhneAuoUc21KU9gHWBtLLOdBvONoZw=="], - "@electron/universal": ["@electron/universal@2.0.1", "", { "dependencies": { "@electron/asar": "^3.2.7", "@malept/cross-spawn-promise": "^2.0.0", "debug": "^4.3.1", "dir-compare": "^4.2.0", "fs-extra": "^11.1.1", "minimatch": "^9.0.3", "plist": "^3.1.0" } }, "sha512-fKpv9kg4SPmt+hY7SVBnIYULE9QJl8L3sCfcBsnqbJwwBwAeTLokJ9TRt9y7bK0JAzIW2y78TVVjvnQEms/yyA=="], + "@anthropic-ai/claude-agent-sdk-win32-x64": ["@anthropic-ai/claude-agent-sdk-win32-x64@0.3.282", "", { "os": "win32", "cpu": "x64" }, "sha512-oOMNLTcLGqPBHvrTVRXNL6q8LK7DL3blkuBBrAgsmkgr+pxi0E74eQEk7XQ5dYwhETa2/ZfNIwNevcYB7LAVtg=="], - "@electron/windows-sign": ["@electron/windows-sign@1.2.2", "", { "dependencies": { "cross-dirname": "^0.1.0", "debug": "^4.3.4", "fs-extra": "^11.1.1", "minimist": "^1.2.8", "postject": "^1.0.0-alpha.6" }, "bin": { "electron-windows-sign": "bin/electron-windows-sign.js" } }, "sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.120.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-ZlvmNFT/iIF6JD13rxbbMWD8nvGR0RaUp6yMQnoc+4Af0YjVVe/bIdW1XSQQsoxXAtg1NaT6Vak0LKFlJ4d37Q=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A=="], + "@braintree/sanitize-url": ["@braintree/sanitize-url@7.1.2", "", {}, "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.27.0", "", { "os": "android", "cpu": "arm" }, "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ=="], - - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.0", "", { "os": "android", "cpu": "arm64" }, "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ=="], - - "@esbuild/android-x64": ["@esbuild/android-x64@0.27.0", "", { "os": "android", "cpu": "x64" }, "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q=="], - - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg=="], - - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g=="], - - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw=="], - - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g=="], - - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.0", "", { "os": "linux", "cpu": "arm" }, "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ=="], - - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.0", "", { "os": "linux", "cpu": "none" }, "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.0", "", { "os": "linux", "cpu": "x64" }, "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.0", "", { "os": "none", "cpu": "x64" }, "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.0", "", { "os": "none", "cpu": "arm64" }, "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.0", "", { "os": "win32", "cpu": "x64" }, "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg=="], + "@chevrotain/types": ["@chevrotain/types@11.1.2", "", {}, "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw=="], "@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="], @@ -146,91 +93,15 @@ "@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="], - "@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="], - "@hono/node-server": ["@hono/node-server@1.19.6", "", { "peerDependencies": { "hono": "^4" } }, "sha512-Shz/KjlIeAhfiuE93NDKVdZ7HdBVLQAfdbaXEaoAVO3ic9ibRSLGIQGkcBbFyuLr+7/1D5ZCINM8B+6IvXeMtw=="], - "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], - - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], - - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], - - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], - - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], - - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], - - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], - - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], - - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], - - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], - - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], - - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], - - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], - - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], - - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], - - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], - - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], - - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], - - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], - - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], - - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@iconify/utils": ["@iconify/utils@3.1.4", "", { "dependencies": { "@antfu/install-pkg": "^1.1.0", "@iconify/types": "^2.0.0", "import-meta-resolve": "^4.2.0" } }, "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@mermaid-js/parser": ["@mermaid-js/parser@1.2.1", "", { "dependencies": { "@chevrotain/types": "~11.1.2" } }, "sha512-n12NohV3mrUyUL2o93IgG/ifeW9FTyeJn3zDxkhwa8MJ9Fxg3HQMlA3RiGmD/3UnJvheztkjjQAjA2T4LmUcpw=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], - - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], - - "@isaacs/balanced-match": ["@isaacs/balanced-match@4.0.1", "", {}, "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ=="], - - "@isaacs/brace-expansion": ["@isaacs/brace-expansion@5.0.0", "", { "dependencies": { "@isaacs/balanced-match": "^4.0.1" } }, "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA=="], - - "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], - - "@malept/cross-spawn-promise": ["@malept/cross-spawn-promise@2.0.0", "", { "dependencies": { "cross-spawn": "^7.0.1" } }, "sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg=="], - - "@malept/flatpak-bundler": ["@malept/flatpak-bundler@0.4.0", "", { "dependencies": { "debug": "^4.1.1", "fs-extra": "^9.0.0", "lodash": "^4.17.15", "tmp-promise": "^3.0.2" } }, "sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q=="], - - "@next/env": ["@next/env@16.0.6", "", {}, "sha512-PFTK/G/vM3UJwK5XDYMFOqt8QW42mmhSgdKDapOlCqBUAOfJN2dyOnASR/xUR/JRrro0pLohh/zOJ77xUQWQAg=="], - - "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.0.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-AGzKiPlDiui+9JcPRHLI4V9WFTTcKukhJTfK9qu3e0tz+Y/88B7vo5yZoO7UaikplJEHORzG3QaBFQfkjhnL0Q=="], - - "@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.0.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-LlLLNrK9WCIUkq2GciWDcquXYIf7vLxX8XE49gz7EncssZGL1vlHwgmURiJsUZAvk0HM1a8qb1ABDezsjAE/jw=="], - - "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-r04NzmLSGGfG8EPXKVK72N5zDNnq9pa9el78LhdtqIC3zqKh74QfKHnk24DoK4PEs6eY7sIK/CnNpt30oc59kg=="], - - "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.0.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-hfB/QV0hA7lbD1OJxp52wVDlpffUMfyxUB5ysZbb/pBC5iuhyLcEKSVQo56PFUUmUQzbMsAtUu6k2Gh9bBtWXA=="], - - "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-PZJushBgfvKhJBy01yXMdgL+l5XKr7uSn5jhOQXQXiH3iPT2M9iG64yHpPNGIKitKrHJInwmhPVGogZBAJOCPw=="], - - "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.0.6", "", { "os": "linux", "cpu": "x64" }, "sha512-LqY76IojrH9yS5fyATjLzlOIOgwyzBuNRqXwVxcGfZ58DWNQSyfnLGlfF6shAEqjwlDNLh4Z+P0rnOI87Y9jEw=="], - - "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.0.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-eIfSNNqAkj0tqKRf0u7BVjqylJCuabSrxnpSENY3YKApqwDMeAqYPmnOwmVe6DDl3Lvkbe7cJAyP6i9hQ5PmmQ=="], - - "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-QGs18P4OKdK9y2F3Th42+KGnwsc2iaThOe6jxQgP62kslUU4W+g6AzI6bdIn/pslhSfxjAMU5SjakfT5Fyo/xA=="], - - "@npmcli/fs": ["@npmcli/fs@2.1.2", "", { "dependencies": { "@gar/promisify": "^1.1.3", "semver": "^7.3.5" } }, "sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ=="], - - "@npmcli/move-file": ["@npmcli/move-file@2.0.1", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], "@octokit/auth-token": ["@octokit/auth-token@6.0.0", "", {}, "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w=="], @@ -248,6 +119,22 @@ "@octokit/types": ["@octokit/types@16.0.0", "", { "dependencies": { "@octokit/openapi-types": "^27.0.0" } }, "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg=="], + "@openai/codex": ["@openai/codex@0.156.1", "", { "optionalDependencies": { "@openai/codex-darwin-arm64": "npm:@openai/codex@0.156.1-darwin-arm64", "@openai/codex-darwin-x64": "npm:@openai/codex@0.156.1-darwin-x64", "@openai/codex-linux-arm64": "npm:@openai/codex@0.156.1-linux-arm64", "@openai/codex-linux-x64": "npm:@openai/codex@0.156.1-linux-x64", "@openai/codex-win32-arm64": "npm:@openai/codex@0.156.1-win32-arm64", "@openai/codex-win32-x64": "npm:@openai/codex@0.156.1-win32-x64" }, "bin": { "codex": "bin/codex.js" } }, "sha512-nI1iVl/n2SO2lSvlwEsJx63zdSI4C4Me2gR7AG0OWMJiGSakz2tY2hx43E39Zq5aEoeB5bZjJXzp5Sqhog6vyA=="], + + "@openai/codex-darwin-arm64": ["@openai/codex@0.156.1-darwin-arm64", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Jg6wbdV+wmMZczhwE74GSxOYEZlViKXn6KyCw/yfrz3PAKFD14xljuPopmdhWC1+8IKU2WdN5fdmXNPt2q4HPA=="], + + "@openai/codex-darwin-x64": ["@openai/codex@0.156.1-darwin-x64", "", { "os": "darwin", "cpu": "x64" }, "sha512-BVjqNOoltWrnNUrgMRepvDIIBmd4XY+ikAE4pYVHlrwiizNlEWQuCQOTrFMJKp54LFRG8jDTjLZyYQ1QrliuOg=="], + + "@openai/codex-linux-arm64": ["@openai/codex@0.156.1-linux-arm64", "", { "os": "linux", "cpu": "arm64" }, "sha512-sTEHk31C12dqzJdjZ2u9KLFCXKL04KyCM+QheSjdQI8uzmVJS7aEF2/qAqFxXwk/jwMzC9u7IihfoIlAGR6YsA=="], + + "@openai/codex-linux-x64": ["@openai/codex@0.156.1-linux-x64", "", { "os": "linux", "cpu": "x64" }, "sha512-2ePo0wgOcnONKsuzp8vBjOmNY+IdsKaouaDDIdiKq9HOWNuV/GI22OeXft2A/1GaoL71ictO0/pLAsCEnQ6wew=="], + + "@openai/codex-sdk": ["@openai/codex-sdk@0.156.1", "", { "dependencies": { "@openai/codex": "0.156.1" } }, "sha512-t1GNPYrSIjHGfBKZ+9pbjOS/by0tMwVe+27fHsMO3RHl9QLIc7dB8XYR954iZ6HpF4EAjZs0melT0mPXF03jVg=="], + + "@openai/codex-win32-arm64": ["@openai/codex@0.156.1-win32-arm64", "", { "os": "win32", "cpu": "arm64" }, "sha512-e5DsyOHlKWulTBz/neIczpcYLQDqtaLtBj2lgncOgeQw5mlAQVZHlGvKsoKVcEPUKLb8opIMJBxXi6cz3JJ4hg=="], + + "@openai/codex-win32-x64": ["@openai/codex@0.156.1-win32-x64", "", { "os": "win32", "cpu": "x64" }, "sha512-MJyLxbBs2zzp5kbaR/99Zwe7SmbrwUkveTcT+ayYlO48V0nYh0eU+h2lalBwvC7VJ/ya/bXnUtISJfJKhGCD/g=="], + "@oven/bun-darwin-aarch64": ["@oven/bun-darwin-aarch64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-eJopQrUk0WR7jViYDC29+Rp50xGvs4GtWOXBeqCoFMzutkkO3CZvHehA4JqnjfWMTSS8toqvRhCSOpOz62Wf9w=="], "@oven/bun-darwin-x64": ["@oven/bun-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-xGDePueVFrNgkS+iN0QdEFeRrx2MQ5hQ9ipRFu7N73rgoSSJsFlOKKt2uGZzunczedViIfjYl0ii0K4E9aZ0Ow=="], @@ -270,10 +157,6 @@ "@oven/bun-windows-x64-baseline": ["@oven/bun-windows-x64-baseline@1.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-u5eZHKq6TPJSE282KyBOicGQ2trkFml0RoUfqkPOJVo7TXGrsGYYzdsugZRnVQY/WEmnxGtBy4T3PAaPqgQViA=="], - "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], - - "@posthog/core": ["@posthog/core@1.7.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-d6ZV4grpzeH/6/LP8quMVpSjY1puRkrqfwcPvGRKUAX7tb7YHyp/zMiTDuJmOFbpUxAMBXH5nDwcPiyCY2WGzA=="], - "@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="], "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], @@ -348,137 +231,139 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@sindresorhus/is": ["@sindresorhus/is@4.6.0", "", {}, "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw=="], - - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], - - "@szmarczak/http-timer": ["@szmarczak/http-timer@4.0.6", "", { "dependencies": { "defer-to-connect": "^2.0.0" } }, "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], "@tanstack/react-virtual": ["@tanstack/react-virtual@3.13.12", "", { "dependencies": { "@tanstack/virtual-core": "3.13.12" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA=="], "@tanstack/virtual-core": ["@tanstack/virtual-core@3.13.12", "", {}, "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA=="], - "@tootallnate/once": ["@tootallnate/once@2.0.0", "", {}, "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A=="], - "@types/bun": ["@types/bun@1.3.3", "", { "dependencies": { "bun-types": "1.3.3" } }, "sha512-ogrKbJ2X5N0kWLLFKeytG0eHDleBYtngtlbu9cyBKFtNL3cnpDZkNdQj8flVf6WTZUX5ulI9AY1oa7ljhSrp+g=="], - "@types/cacheable-request": ["@types/cacheable-request@6.0.3", "", { "dependencies": { "@types/http-cache-semantics": "*", "@types/keyv": "^3.1.4", "@types/node": "*", "@types/responselike": "^1.0.0" } }, "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw=="], + "@types/d3": ["@types/d3@7.4.3", "", { "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": "*" } }, "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + "@types/d3-axis": ["@types/d3-axis@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw=="], - "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], + "@types/d3-brush": ["@types/d3-brush@3.0.6", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A=="], - "@types/fs-extra": ["@types/fs-extra@9.0.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA=="], + "@types/d3-chord": ["@types/d3-chord@3.0.6", "", {}, "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - "@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="], + "@types/d3-contour": ["@types/d3-contour@3.0.6", "", { "dependencies": { "@types/d3-array": "*", "@types/geojson": "*" } }, "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg=="], - "@types/keyv": ["@types/keyv@3.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg=="], + "@types/d3-delaunay": ["@types/d3-delaunay@6.0.4", "", {}, "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], + "@types/d3-dispatch": ["@types/d3-dispatch@3.0.7", "", {}, "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA=="], - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], + "@types/d3-drag": ["@types/d3-drag@3.0.7", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ=="], - "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], + "@types/d3-dsv": ["@types/d3-dsv@3.0.7", "", {}, "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g=="], - "@types/plist": ["@types/plist@3.0.5", "", { "dependencies": { "@types/node": "*", "xmlbuilder": ">=11.0.1" } }, "sha512-E6OCaRmAe4WDmWNsL/9RMqdkkzDCY1etutkflWk4c+AcjDU07Pcz1fQwTX0TQz+Pxqn9i4L1TU3UFpjnrcDgxA=="], + "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], + "@types/d3-fetch": ["@types/d3-fetch@3.0.7", "", { "dependencies": { "@types/d3-dsv": "*" } }, "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA=="], - "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], + "@types/d3-force": ["@types/d3-force@3.0.10", "", {}, "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw=="], - "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/d3-format": ["@types/d3-format@3.0.4", "", {}, "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g=="], - "@types/responselike": ["@types/responselike@1.0.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw=="], + "@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/d3-hierarchy": ["@types/d3-hierarchy@3.1.7", "", {}, "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg=="], - "@types/verror": ["@types/verror@1.10.11", "", {}, "sha512-RlDm9K7+o5stv0Co8i8ZRGxDbrTxhJtgjqjFyVh/tXQyl/rYtTKlnTvZ88oSTeYREWurwx20Js4kTuKCsFkUtg=="], + "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], + "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251201.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251201.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251201.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-EiPEgGwNa2uHyyKgeoWTL6wWHKUBmF3xsfZ3OHofk7TxUuxb2mpLG5igEuaBe8iUwkCUl9uZgJvOu6o0wE5NSA=="], + "@types/d3-polygon": ["@types/d3-polygon@3.0.2", "", {}, "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA=="], - "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251201.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PY0BrlRF3YCZEMxzuk79IFSgpGqUErkdrW7Aq+/mF8DEET5uaDypTMb8Vz4CLYJ7Xvvxz8eZsLimPbv6hYDIvA=="], + "@types/d3-quadtree": ["@types/d3-quadtree@3.0.6", "", {}, "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg=="], - "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251201.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YeDrjnsvXwm/MNG8aURT3J+cmHQIhpiElBKOVOy/H6ky4S2Ro9ufG+Bj9CqS3etbTCLhV5btk+QNh86DZ4VDkQ=="], + "@types/d3-random": ["@types/d3-random@3.0.4", "", {}, "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA=="], - "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "arm" }, "sha512-gr2EQYK888YdGROMc7l3N3MeKY1V3QVImKIQZNgqprV+N2rXaFnxGAZ+gql3LqZgRGel4a12vCUJeP7Pjl2gww=="], + "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-HbEn+SBTDZEtwN/VUxA2To+6vEr7x++SCRc6yGp5y4onpBL2xnH17UoxWiqN9J4Bu1DbQ9jZv3D5CzwBlofPQA=="], + "@types/d3-scale-chromatic": ["@types/d3-scale-chromatic@3.1.0", "", {}, "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ=="], - "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "x64" }, "sha512-q94K/LZ3Ab/SbUBMBsf37VdsumeZ1dZmymJYlhGBqk/fdXBayL0diLR3RdzyeQWbCXAxWL5KFKLIiIc3cI/fcA=="], + "@types/d3-selection": ["@types/d3-selection@3.0.11", "", {}, "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w=="], - "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251201.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-/AFwpsX/G05bBsfVURfg4+/JC6gfvqj9jfFe/7oe1Y1J42koN5C8TH+eSmMOOEcPYpFjR1e+NWckqBJKaCXJ4A=="], + "@types/d3-shape": ["@types/d3-shape@3.2.0", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw=="], - "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251201.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vTUCDEuSP4ifLHqb8aljuj44v6+M1HDKo1WLnboTDpwU7IIrTux/0jzkPfEHd9xd5FU4EhSA8ZrYDwKI0BcRcg=="], + "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + "@types/d3-time-format": ["@types/d3-time-format@4.0.3", "", {}, "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg=="], - "@xmldom/xmldom": ["@xmldom/xmldom@0.8.11", "", {}, "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw=="], + "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="], + "@types/d3-transition": ["@types/d3-transition@3.0.9", "", { "dependencies": { "@types/d3-selection": "*" } }, "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "@types/d3-zoom": ["@types/d3-zoom@3.0.8", "", { "dependencies": { "@types/d3-interpolate": "*", "@types/d3-selection": "*" } }, "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw=="], - "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], + "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="], + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], - "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], - "ajv-keywords": ["ajv-keywords@3.5.2", "", { "peerDependencies": { "ajv": "^6.9.1" } }, "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="], + "@types/geojson": ["@types/geojson@7946.0.16", "", {}, "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - "app-builder-bin": ["app-builder-bin@5.0.0-alpha.12", "", {}, "sha512-j87o0j6LqPL3QRr8yid6c+Tt5gC7xNfYo6uQIQkorAC6MpeayVMZrEDzKmJJ/Hlv7EnOQpaRm53k6ktDYZyB6w=="], + "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - "app-builder-lib": ["app-builder-lib@26.0.12", "", { "dependencies": { "@develar/schema-utils": "~2.6.5", "@electron/asar": "3.2.18", "@electron/fuses": "^1.8.0", "@electron/notarize": "2.5.0", "@electron/osx-sign": "1.3.1", "@electron/rebuild": "3.7.0", "@electron/universal": "2.0.1", "@malept/flatpak-bundler": "^0.4.0", "@types/fs-extra": "9.0.13", "async-exit-hook": "^2.0.1", "builder-util": "26.0.11", "builder-util-runtime": "9.3.1", "chromium-pickle-js": "^0.2.0", "config-file-ts": "0.2.8-rc1", "debug": "^4.3.4", "dotenv": "^16.4.5", "dotenv-expand": "^11.0.6", "ejs": "^3.1.8", "electron-publish": "26.0.11", "fs-extra": "^10.1.0", "hosted-git-info": "^4.1.0", "is-ci": "^3.0.0", "isbinaryfile": "^5.0.0", "js-yaml": "^4.1.0", "json5": "^2.2.3", "lazy-val": "^1.0.5", "minimatch": "^10.0.0", "plist": "3.1.0", "resedit": "^1.7.0", "semver": "^7.3.8", "tar": "^6.1.12", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" }, "peerDependencies": { "dmg-builder": "26.0.12", "electron-builder-squirrel-windows": "26.0.12" } }, "sha512-+/CEPH1fVKf6HowBUs6LcAIoRcjeqgvAeoSE+cl7Y7LndyQ9ViGPYibNk7wmhMHzNgHIuIbw4nWADPO+4mjgWw=="], + "@types/node": ["@types/node@22.19.1", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + "@types/prismjs": ["@types/prismjs@1.26.5", "", {}, "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], + "@types/react": ["@types/react@19.2.7", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg=="], - "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], - "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - "async-exit-hook": ["async-exit-hook@2.0.1", "", {}, "sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw=="], + "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251201.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251201.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251201.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251201.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-EiPEgGwNa2uHyyKgeoWTL6wWHKUBmF3xsfZ3OHofk7TxUuxb2mpLG5igEuaBe8iUwkCUl9uZgJvOu6o0wE5NSA=="], - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251201.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-PY0BrlRF3YCZEMxzuk79IFSgpGqUErkdrW7Aq+/mF8DEET5uaDypTMb8Vz4CLYJ7Xvvxz8eZsLimPbv6hYDIvA=="], - "at-least-node": ["at-least-node@1.0.0", "", {}, "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg=="], + "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251201.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-YeDrjnsvXwm/MNG8aURT3J+cmHQIhpiElBKOVOy/H6ky4S2Ro9ufG+Bj9CqS3etbTCLhV5btk+QNh86DZ4VDkQ=="], - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], + "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "arm" }, "sha512-gr2EQYK888YdGROMc7l3N3MeKY1V3QVImKIQZNgqprV+N2rXaFnxGAZ+gql3LqZgRGel4a12vCUJeP7Pjl2gww=="], + + "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-HbEn+SBTDZEtwN/VUxA2To+6vEr7x++SCRc6yGp5y4onpBL2xnH17UoxWiqN9J4Bu1DbQ9jZv3D5CzwBlofPQA=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251201.1", "", { "os": "linux", "cpu": "x64" }, "sha512-q94K/LZ3Ab/SbUBMBsf37VdsumeZ1dZmymJYlhGBqk/fdXBayL0diLR3RdzyeQWbCXAxWL5KFKLIiIc3cI/fcA=="], - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251201.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-/AFwpsX/G05bBsfVURfg4+/JC6gfvqj9jfFe/7oe1Y1J42koN5C8TH+eSmMOOEcPYpFjR1e+NWckqBJKaCXJ4A=="], - "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], + "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251201.1", "", { "os": "win32", "cpu": "x64" }, "sha512-vTUCDEuSP4ifLHqb8aljuj44v6+M1HDKo1WLnboTDpwU7IIrTux/0jzkPfEHd9xd5FU4EhSA8ZrYDwKI0BcRcg=="], + + "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], + + "@upsetjs/venn.js": ["@upsetjs/venn.js@2.0.0", "", { "optionalDependencies": { "d3-selection": "^3.0.0", "d3-transition": "^3.0.1" } }, "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - "boolean": ["boolean@3.2.0", "", {}, "sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw=="], + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], + "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "builder-util": ["builder-util@26.0.11", "", { "dependencies": { "7zip-bin": "~5.2.0", "@types/debug": "^4.1.6", "app-builder-bin": "5.0.0-alpha.12", "builder-util-runtime": "9.3.1", "chalk": "^4.1.2", "cross-spawn": "^7.0.6", "debug": "^4.3.4", "fs-extra": "^10.1.0", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "is-ci": "^3.0.0", "js-yaml": "^4.1.0", "sanitize-filename": "^1.6.3", "source-map-support": "^0.5.19", "stat-mode": "^1.0.0", "temp-file": "^3.4.0", "tiny-async-pool": "1.3.0" } }, "sha512-xNjXfsldUEe153h1DraD0XvDOpqGR0L5eKFkdReB7eFW5HqysDZFfly4rckda6y9dF39N3pkPlOblcfHKGw+uA=="], + "before-after-hook": ["before-after-hook@4.0.0", "", {}, "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ=="], - "builder-util-runtime": ["builder-util-runtime@9.3.1", "", { "dependencies": { "debug": "^4.3.4", "sax": "^1.2.4" } }, "sha512-2/egrNDDnRaxVwK3A+cJq6UOlqOdedGA7JPqCeJjN2Zjk1/QB/6QUi3b714ScIGS7HafFXTyzJEOr5b44I3kvQ=="], + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], "bun": ["bun@1.3.3", "", { "optionalDependencies": { "@oven/bun-darwin-aarch64": "1.3.3", "@oven/bun-darwin-x64": "1.3.3", "@oven/bun-darwin-x64-baseline": "1.3.3", "@oven/bun-linux-aarch64": "1.3.3", "@oven/bun-linux-aarch64-musl": "1.3.3", "@oven/bun-linux-x64": "1.3.3", "@oven/bun-linux-x64-baseline": "1.3.3", "@oven/bun-linux-x64-musl": "1.3.3", "@oven/bun-linux-x64-musl-baseline": "1.3.3", "@oven/bun-windows-x64": "1.3.3", "@oven/bun-windows-x64-baseline": "1.3.3" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "x64", "arm64", ], "bin": { "bun": "bin/bun.exe", "bunx": "bin/bunx.exe" } }, "sha512-2hJ4ocTZ634/Ptph4lysvO+LbbRZq8fzRvMwX0/CqaLBxrF2UB5D1LdMB8qGcdtCer4/VR9Bx5ORub0yn+yzmw=="], @@ -486,15 +371,11 @@ "bun-types": ["bun-types@1.3.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-z3Xwlg7j2l9JY27x5Qn3Wlyos8YAp0kKRlrePAOjgjMGS5IG6E7Jnlx736vH9UVI4wUICwwhC9anYL++XeOgTQ=="], - "cacache": ["cacache@16.1.3", "", { "dependencies": { "@npmcli/fs": "^2.1.0", "@npmcli/move-file": "^2.0.0", "chownr": "^2.0.0", "fs-minipass": "^2.1.0", "glob": "^8.0.1", "infer-owner": "^1.0.4", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "mkdirp": "^1.0.4", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^9.0.0", "tar": "^6.1.11", "unique-filename": "^2.0.0" } }, "sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ=="], - - "cacheable-lookup": ["cacheable-lookup@5.0.4", "", {}, "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA=="], - - "cacheable-request": ["cacheable-request@7.0.4", "", { "dependencies": { "clone-response": "^1.0.2", "get-stream": "^5.1.0", "http-cache-semantics": "^4.0.0", "keyv": "^4.0.0", "lowercase-keys": "^2.0.0", "normalize-url": "^6.0.1", "responselike": "^2.0.0" } }, "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg=="], + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], - "caniuse-lite": ["caniuse-lite@1.0.30001757", "", {}, "sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ=="], + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], @@ -508,133 +389,141 @@ "character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="], - "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], + "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], - "chromium-pickle-js": ["chromium-pickle-js@0.2.0", "", {}, "sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], - "clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], + "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], - "cli-truncate": ["cli-truncate@2.1.0", "", { "dependencies": { "slice-ansi": "^3.0.0", "string-width": "^4.2.0" } }, "sha512-n8fOixwDD6b/ObinzTrp1ZKFzbgvKZvuz/TvejnLn1aQfC6r52XEx85FmuC+3HI+JM7coBRXUvNqEU2PHVrHpg=="], + "commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], - "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], + "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], - "clone-response": ["clone-response@1.0.3", "", { "dependencies": { "mimic-response": "^1.0.0" } }, "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], - "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], - "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + "cose-base": ["cose-base@1.0.3", "", { "dependencies": { "layout-base": "^1.0.0" } }, "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg=="], - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "cytoscape": ["cytoscape@3.34.2", "", {}, "sha512-Cm2jaj1X/PBNlzV9yH8zcfGOxO7U+CJ/+mxSBVPSchLaugdp4jtlGx5qaHtPRZ6tgiZ5P+o1XoRfJA+ba6KM3g=="], - "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], + "cytoscape-cose-bilkent": ["cytoscape-cose-bilkent@4.1.0", "", { "dependencies": { "cose-base": "^1.0.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ=="], - "compare-version": ["compare-version@0.1.2", "", {}, "sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A=="], + "cytoscape-fcose": ["cytoscape-fcose@2.2.0", "", { "dependencies": { "cose-base": "^2.2.0" }, "peerDependencies": { "cytoscape": "^3.2.0" } }, "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "d3": ["d3@7.9.0", "", { "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" } }, "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA=="], - "concurrently": ["concurrently@9.2.1", "", { "dependencies": { "chalk": "4.1.2", "rxjs": "7.8.2", "shell-quote": "1.8.3", "supports-color": "8.1.1", "tree-kill": "1.2.2", "yargs": "17.7.2" }, "bin": { "conc": "dist/bin/concurrently.js", "concurrently": "dist/bin/concurrently.js" } }, "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng=="], + "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - "config-file-ts": ["config-file-ts@0.2.8-rc1", "", { "dependencies": { "glob": "^10.3.12", "typescript": "^5.4.3" } }, "sha512-GtNECbVI82bT4RiDIzBSVuTKoSHufnU7Ce7/42bkWZJZFLjmDF2WBpVsvRkhKCfKBnTBb3qZrBwPpFBU/Myvhg=="], + "d3-axis": ["d3-axis@3.0.0", "", {}, "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw=="], - "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "d3-brush": ["d3-brush@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "3", "d3-transition": "3" } }, "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ=="], - "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], + "d3-chord": ["d3-chord@3.0.1", "", { "dependencies": { "d3-path": "1 - 3" } }, "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g=="], - "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - "crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="], + "d3-contour": ["d3-contour@4.0.2", "", { "dependencies": { "d3-array": "^3.2.0" } }, "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA=="], - "cross-dirname": ["cross-dirname@0.1.0", "", {}, "sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q=="], + "d3-delaunay": ["d3-delaunay@6.0.4", "", { "dependencies": { "delaunator": "5" } }, "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "d3-dispatch": ["d3-dispatch@3.0.1", "", {}, "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg=="], - "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "d3-drag": ["d3-drag@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-selection": "3" } }, "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "d3-dsv": ["d3-dsv@3.0.1", "", { "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" } }, "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q=="], - "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], + "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], + "d3-fetch": ["d3-fetch@3.0.1", "", { "dependencies": { "d3-dsv": "1 - 3" } }, "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw=="], - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], + "d3-force": ["d3-force@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-quadtree": "1 - 3", "d3-timer": "1 - 3" } }, "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg=="], - "defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="], + "d3-format": ["d3-format@3.1.2", "", {}, "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg=="], - "define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + "d3-geo": ["d3-geo@3.1.1", "", { "dependencies": { "d3-array": "2.5.0 - 3" } }, "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q=="], - "define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="], + "d3-hierarchy": ["d3-hierarchy@3.1.2", "", {}, "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "d3-polygon": ["d3-polygon@3.0.1", "", {}, "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg=="], - "detect-node": ["detect-node@2.1.0", "", {}, "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g=="], + "d3-quadtree": ["d3-quadtree@3.0.1", "", {}, "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw=="], - "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], + "d3-random": ["d3-random@3.0.1", "", {}, "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ=="], - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], + "d3-sankey": ["d3-sankey@0.12.3", "", { "dependencies": { "d3-array": "1 - 2", "d3-shape": "^1.2.0" } }, "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ=="], - "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "d3-scale": ["d3-scale@4.0.2", "", { "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" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - "dir-compare": ["dir-compare@4.2.0", "", { "dependencies": { "minimatch": "^3.0.5", "p-limit": "^3.1.0 " } }, "sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ=="], + "d3-scale-chromatic": ["d3-scale-chromatic@3.1.0", "", { "dependencies": { "d3-color": "1 - 3", "d3-interpolate": "1 - 3" } }, "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ=="], - "dmg-builder": ["dmg-builder@26.0.12", "", { "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", "builder-util-runtime": "9.3.1", "fs-extra": "^10.1.0", "iconv-lite": "^0.6.2", "js-yaml": "^4.1.0" }, "optionalDependencies": { "dmg-license": "^1.0.11" } }, "sha512-59CAAjAhTaIMCN8y9kD573vDkxbs1uhDcrFLHSgutYdPcGOU35Rf95725snvzEOy4BFB7+eLJ8djCNPmGwG67w=="], + "d3-selection": ["d3-selection@3.0.0", "", {}, "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ=="], - "dmg-license": ["dmg-license@1.0.11", "", { "dependencies": { "@types/plist": "^3.0.1", "@types/verror": "^1.10.3", "ajv": "^6.10.0", "crc": "^3.8.0", "iconv-corefoundation": "^1.1.7", "plist": "^3.0.4", "smart-buffer": "^4.0.2", "verror": "^1.10.0" }, "os": "darwin", "bin": { "dmg-license": "bin/dmg-license.js" } }, "sha512-ZdzmqwKmECOWJpqefloC5OJy1+WZBBse5+MR88z9g9Zn4VY+WYUkAyojmhzJckH5YbbZGcYIuGAkY5/Ys5OM2Q=="], + "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - "dotenv-expand": ["dotenv-expand@11.0.7", "", { "dependencies": { "dotenv": "^16.4.5" } }, "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA=="], + "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "d3-transition": ["d3-transition@3.0.1", "", { "dependencies": { "d3-color": "1 - 3", "d3-dispatch": "1 - 3", "d3-ease": "1 - 3", "d3-interpolate": "1 - 3", "d3-timer": "1 - 3" }, "peerDependencies": { "d3-selection": "2 - 3" } }, "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w=="], - "ejs": ["ejs@3.1.10", "", { "dependencies": { "jake": "^10.8.5" }, "bin": { "ejs": "bin/cli.js" } }, "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA=="], + "d3-zoom": ["d3-zoom@3.0.0", "", { "dependencies": { "d3-dispatch": "1 - 3", "d3-drag": "2 - 3", "d3-interpolate": "1 - 3", "d3-selection": "2 - 3", "d3-transition": "2 - 3" } }, "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw=="], - "electron": ["electron@39.2.4", "", { "dependencies": { "@electron/get": "^2.0.0", "@types/node": "^22.7.7", "extract-zip": "^2.0.1" }, "bin": { "electron": "cli.js" } }, "sha512-KxPtwpFceQKSxRtUY39piHLYhJMMyHfOhc70e6zRnKGrbRdK6hzEqssth8IGjlKOdkeT4KCvIEngnNraYk39+g=="], + "dagre-d3-es": ["dagre-d3-es@7.0.14", "", { "dependencies": { "d3": "^7.9.0", "lodash-es": "^4.17.21" } }, "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg=="], - "electron-builder": ["electron-builder@26.0.12", "", { "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", "builder-util-runtime": "9.3.1", "chalk": "^4.1.2", "dmg-builder": "26.0.12", "fs-extra": "^10.1.0", "is-ci": "^3.0.0", "lazy-val": "^1.0.5", "simple-update-notifier": "2.0.0", "yargs": "^17.6.2" }, "bin": { "electron-builder": "cli.js", "install-app-deps": "install-app-deps.js" } }, "sha512-cD1kz5g2sgPTMFHjLxfMjUK5JABq3//J4jPswi93tOPFz6btzXYtK5NrDt717NRbukCUDOrrvmYVOWERlqoiXA=="], + "dayjs": ["dayjs@1.11.23", "", {}, "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ=="], - "electron-builder-squirrel-windows": ["electron-builder-squirrel-windows@26.0.12", "", { "dependencies": { "app-builder-lib": "26.0.12", "builder-util": "26.0.11", "electron-winstaller": "5.4.0" } }, "sha512-kpwXM7c/ayRUbYVErQbsZ0nQZX4aLHQrPEG9C4h9vuJCXylwFH8a7Jgi2VpKIObzCXO7LKHiCw4KdioFLFOgqA=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "electron-publish": ["electron-publish@26.0.11", "", { "dependencies": { "@types/fs-extra": "^9.0.11", "builder-util": "26.0.11", "builder-util-runtime": "9.3.1", "chalk": "^4.1.2", "form-data": "^4.0.0", "fs-extra": "^10.1.0", "lazy-val": "^1.0.5", "mime": "^2.5.2" } }, "sha512-a8QRH0rAPIWH9WyyS5LbNvW9Ark6qe63/LqDB7vu2JXYpi0Gma5Q60Dh4tmTqhOBQt0xsrzD8qE7C+D7j+B24A=="], + "decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="], - "electron-updater": ["electron-updater@6.6.2", "", { "dependencies": { "builder-util-runtime": "9.3.1", "fs-extra": "^10.1.0", "js-yaml": "^4.1.0", "lazy-val": "^1.0.5", "lodash.escaperegexp": "^4.1.2", "lodash.isequal": "^4.5.0", "semver": "^7.6.3", "tiny-typed-emitter": "^2.1.0" } }, "sha512-Cr4GDOkbAUqRHP5/oeOmH/L2Bn6+FQPxVLZtPbcmKZC63a1F3uu5EefYOssgZXG3u/zBlubbJ5PJdITdMVggbw=="], + "delaunator": ["delaunator@5.1.0", "", { "dependencies": { "robust-predicates": "^3.0.2" } }, "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ=="], - "electron-winstaller": ["electron-winstaller@5.4.0", "", { "dependencies": { "@electron/asar": "^3.2.1", "debug": "^4.1.1", "fs-extra": "^7.0.1", "lodash": "^4.17.21", "temp": "^0.9.0" }, "optionalDependencies": { "@electron/windows-sign": "^1.1.2" } }, "sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], + "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], - "env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="], + "dompurify": ["dompurify@3.4.14", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg=="], - "err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -642,54 +531,46 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - - "es6-error": ["es6-error@4.1.1", "", {}, "sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg=="], - - "esbuild": ["esbuild@0.27.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.0", "@esbuild/android-arm": "0.27.0", "@esbuild/android-arm64": "0.27.0", "@esbuild/android-x64": "0.27.0", "@esbuild/darwin-arm64": "0.27.0", "@esbuild/darwin-x64": "0.27.0", "@esbuild/freebsd-arm64": "0.27.0", "@esbuild/freebsd-x64": "0.27.0", "@esbuild/linux-arm": "0.27.0", "@esbuild/linux-arm64": "0.27.0", "@esbuild/linux-ia32": "0.27.0", "@esbuild/linux-loong64": "0.27.0", "@esbuild/linux-mips64el": "0.27.0", "@esbuild/linux-ppc64": "0.27.0", "@esbuild/linux-riscv64": "0.27.0", "@esbuild/linux-s390x": "0.27.0", "@esbuild/linux-x64": "0.27.0", "@esbuild/netbsd-arm64": "0.27.0", "@esbuild/netbsd-x64": "0.27.0", "@esbuild/openbsd-arm64": "0.27.0", "@esbuild/openbsd-x64": "0.27.0", "@esbuild/openharmony-arm64": "0.27.0", "@esbuild/sunos-x64": "0.27.0", "@esbuild/win32-arm64": "0.27.0", "@esbuild/win32-ia32": "0.27.0", "@esbuild/win32-x64": "0.27.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA=="], + "es-toolkit": ["es-toolkit@1.51.0", "", {}, "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw=="], "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="], - "exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], - "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], - "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], - "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + "express-rate-limit": ["express-rate-limit@8.6.2", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A=="], - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], + "fast-content-type-parse": ["fast-content-type-parse@3.0.0", "", {}, "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg=="], - "filelist": ["filelist@1.0.4", "", { "dependencies": { "minimatch": "^5.0.1" } }, "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "fast-uri": ["fast-uri@3.1.6", "", {}, "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q=="], - "fs-extra": ["fs-extra@10.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ=="], + "fastdom": ["fastdom@1.0.12", "", { "dependencies": { "strictdom": "^1.0.1" } }, "sha512-LB+xjSTEbjHE1cWsxu+tN2Xqr1kpi+V9aADI7sVM5ZMaXyYGPHULQMzpJMYqOTULK/73pUkWVzzObFRBkPr+hg=="], - "fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="], + "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "geist": ["geist@1.5.1", "", { "peerDependencies": { "next": ">=13.2.0" } }, "sha512-mAHZxIsL2o3ZITFaBVFBnwyDOw+zNLYum6A6nIjpzCGIO8QtC3V76XF2RnZTyLx1wlDTmMDy8jg3Ib52MIjGvQ=="], - "gemoji": ["gemoji@8.1.0", "", {}, "sha512-HA4Gx59dw2+tn+UAa7XEV4ufUKI4fH1KgcbenVA9YKSj1QJTT0xh5Mwv5HMFNN3l2OtUe3ZIfuRwSyZS5pLIWw=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -700,32 +581,16 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - - "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], - "gitdiff-parser": ["gitdiff-parser@0.3.1", "", {}, "sha512-YQJnY8aew65id8okGxKCksH3efDCJ9HzV7M9rsvd65habf39Pkh4cgYJ27AaoDMqo1X98pgNJhNMrm/kpV7UVQ=="], - "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], - - "global-agent": ["global-agent@3.0.0", "", { "dependencies": { "boolean": "^3.0.1", "es6-error": "^4.1.1", "matcher": "^3.0.0", "roarr": "^2.15.3", "semver": "^7.3.2", "serialize-error": "^7.0.1" } }, "sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q=="], - - "globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], - "got": ["got@11.8.6", "", { "dependencies": { "@sindresorhus/is": "^4.0.0", "@szmarczak/http-timer": "^4.0.5", "@types/cacheable-request": "^6.0.1", "@types/responselike": "^1.0.0", "cacheable-lookup": "^5.0.3", "cacheable-request": "^7.0.2", "decompress-response": "^6.0.0", "http2-wrapper": "^1.0.0-beta.5.2", "lowercase-keys": "^2.0.0", "p-cancelable": "^2.0.0", "responselike": "^2.0.0" } }, "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g=="], - - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "hachure-fill": ["hachure-fill@0.5.2", "", {}, "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], @@ -752,109 +617,67 @@ "hono": ["hono@4.10.7", "", {}, "sha512-icXIITfw/07Q88nLSkB9aiUrd8rYzSweK681Kjo/TSggaGbOX4RRyxxm71v+3PC8C/j+4rlxGeoTRxQDkaJkUw=="], - "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - "html-url-attributes": ["html-url-attributes@3.0.1", "", {}, "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - "http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="], - - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - - "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - - "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], - - "iconv-corefoundation": ["iconv-corefoundation@1.1.7", "", { "dependencies": { "cli-truncate": "^2.1.0", "node-addon-api": "^1.6.3" }, "os": "darwin" }, "sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ=="], - - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - - "infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "import-meta-resolve": ["import-meta-resolve@4.2.0", "", {}, "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "inline-style-parser": ["inline-style-parser@0.2.7", "", {}, "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA=="], - "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], + + "ip-address": ["ip-address@10.5.0", "", {}, "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], "is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="], "is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="], - "is-ci": ["is-ci@3.0.1", "", { "dependencies": { "ci-info": "^3.2.0" }, "bin": { "is-ci": "bin.js" } }, "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ=="], - "is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - - "is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="], - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "isbinaryfile": ["isbinaryfile@5.0.7", "", {}, "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ=="], + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "jackspeak": ["jackspeak@4.1.1", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" } }, "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ=="], - - "jake": ["jake@10.9.4", "", { "dependencies": { "async": "^3.2.6", "filelist": "^1.0.4", "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" } }, "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], + "jose": ["jose@6.2.10", "", {}, "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g=="], - "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], - "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "katex": ["katex@0.16.47", "", { "dependencies": { "commander": "^8.3.0" }, "bin": { "katex": "cli.js" } }, "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], + "khroma": ["khroma@2.1.0", "", {}, "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw=="], - "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + "layout-base": ["layout-base@1.0.2", "", {}, "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg=="], - "lazy-val": ["lazy-val@1.0.5", "", {}, "sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q=="], - - "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], - - "lodash.escaperegexp": ["lodash.escaperegexp@4.1.2", "", {}, "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw=="], - - "lodash.isequal": ["lodash.isequal@4.5.0", "", {}, "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ=="], - - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], + "lodash-es": ["lodash-es@4.18.1", "", {}, "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="], "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - "lowercase-keys": ["lowercase-keys@2.0.0", "", {}, "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA=="], - "lowlight": ["lowlight@3.3.0", "", { "dependencies": { "@types/hast": "^3.0.0", "devlop": "^1.0.0", "highlight.js": "~11.11.0" } }, "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ=="], - "lru-cache": ["lru-cache@11.2.4", "", {}, "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg=="], - "lucide-react": ["lucide-react@0.555.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-D8FvHUGbxWBRQM90NZeIyhAvkFfsh3u9ekrMvJ30Z6gnpBHS6HC6ldLg7tL45hwiIz/u66eKDtdA23gwwGsAHA=="], - "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - "matcher": ["matcher@3.0.0", "", { "dependencies": { "escape-string-regexp": "^4.0.0" } }, "sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng=="], + "marked": ["marked@16.4.2", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], @@ -888,6 +711,12 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mermaid": ["mermaid@11.17.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.2", "@iconify/utils": "^3.0.2", "@mermaid-js/parser": "^1.2.1", "@types/d3": "^7.4.3", "@upsetjs/venn.js": "^2.0.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.3.3", "es-toolkit": "^1.45.1", "fastdom": "1.0.12", "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" } }, "sha512-V6K3C8EBdEsPFZXSKMJe6ppQOENxuHARr9GvHX4hh47lAbhMRD9qf4oEK7LoaRQxULMa80/qt5gHO73aCleBBg=="], + "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], @@ -944,115 +773,53 @@ "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], - - "minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="], - - "minipass-fetch": ["minipass-fetch@2.1.2", "", { "dependencies": { "minipass": "^3.1.6", "minipass-sized": "^1.0.3", "minizlib": "^2.1.2" }, "optionalDependencies": { "encoding": "^0.1.13" } }, "sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA=="], - - "minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="], - - "minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="], - - "minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="], - - "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], - - "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], - "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "next": ["next@16.0.6", "", { "dependencies": { "@next/env": "16.0.6", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.0.6", "@next/swc-darwin-x64": "16.0.6", "@next/swc-linux-arm64-gnu": "16.0.6", "@next/swc-linux-arm64-musl": "16.0.6", "@next/swc-linux-x64-gnu": "16.0.6", "@next/swc-linux-x64-musl": "16.0.6", "@next/swc-win32-arm64-msvc": "16.0.6", "@next/swc-win32-x64-msvc": "16.0.6", "sharp": "^0.34.4" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-2zOZ/4FdaAp5hfCU/RnzARlZzBsjaTZ/XjNQmuyYLluAPM7kcrbIkdeO2SL0Ysd1vnrSgU+GwugfeWX1cUCgCg=="], + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "node-abi": ["node-abi@3.85.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg=="], - - "node-addon-api": ["node-addon-api@1.7.2", "", {}, "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg=="], - - "node-api-version": ["node-api-version@0.2.1", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q=="], - - "nopt": ["nopt@6.0.0", "", { "dependencies": { "abbrev": "^1.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g=="], - - "normalize-url": ["normalize-url@6.1.0", "", {}, "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A=="], - - "object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "p-cancelable": ["p-cancelable@2.1.1", "", {}, "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg=="], - - "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], - - "p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "package-manager-detector": ["package-manager-detector@1.8.0", "", {}, "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-data-parser": ["path-data-parser@0.1.0", "", {}, "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w=="], - "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], - - "pe-library": ["pe-library@0.4.1", "", {}, "sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw=="], - - "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="], - - "png2icons": ["png2icons@2.0.1", "", { "bin": { "png2icons": "./png2icons-cli.js" } }, "sha512-GDEQJr8OG4e6JMp7mABtXFSEpgJa1CCpbQiAR+EjhkHJHnUL9zPPtbOrjsMD8gUbikgv3j7x404b0YJsV3aVFA=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], + "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "posthog-js": ["posthog-js@1.300.0", "", { "dependencies": { "@posthog/core": "1.7.0", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", "web-vitals": "^4.2.4" } }, "sha512-FpeD4pkpg9i/3F5xpeX+CZeq3R3BaLyFDkzb4sOjpUIW1AAVuOEqPTxnWIess6C3pOw1u3mowq0WSeh1PmrqfA=="], + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], - "postject": ["postject@1.0.0-alpha.6", "", { "dependencies": { "commander": "^9.4.0" }, "bin": { "postject": "dist/cli.js" } }, "sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A=="], + "points-on-curve": ["points-on-curve@0.2.0", "", {}, "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A=="], - "preact": ["preact@10.28.0", "", {}, "sha512-rytDAoiXr3+t6OIP3WGlDd0ouCUG1iCWzkcY3++Nreuoi17y6T5i/zRhe6uYfoVcxq6YU+sBtJouuRDsq8vvqA=="], + "points-on-path": ["points-on-path@0.2.1", "", { "dependencies": { "path-data-parser": "0.1.0", "points-on-curve": "0.2.0" } }, "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g=="], "prettier": ["prettier@3.7.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-QgODejq9K3OzoBbuyobZlUhznP5SKwPqp+6Q6xw6o8gnhr4O85L2U915iM2IDcfF2NPXVaM9zlo9tdwipnYwzg=="], - "proc-log": ["proc-log@2.0.1", "", {}, "sha512-Kcmo2FhfDTXdcbfDH76N7uBYHINxc/8GW7UAVuVP9I+Va3uHSerrnKV6dLooga/gh7GlgzuCCr/eoldnL1muGw=="], - - "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - - "promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="], - - "promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="], - "property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="], - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "qs": ["qs@6.15.3", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], - "quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="], + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], "react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], @@ -1070,10 +837,6 @@ "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "read-binary-file-arch": ["read-binary-file-arch@1.0.6", "", { "dependencies": { "debug": "^4.3.4" }, "bin": { "read-binary-file-arch": "cli.js" } }, "sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - "refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="], "rehype-highlight": ["rehype-highlight@7.0.2", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-text": "^4.0.0", "lowlight": "^3.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA=="], @@ -1094,43 +857,29 @@ "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "resedit": ["resedit@1.7.2", "", { "dependencies": { "pe-library": "^0.4.1" } }, "sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA=="], - - "resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "robust-predicates": ["robust-predicates@3.0.3", "", {}, "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA=="], - "responselike": ["responselike@2.0.1", "", { "dependencies": { "lowercase-keys": "^2.0.0" } }, "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw=="], + "roughjs": ["roughjs@4.6.6", "", { "dependencies": { "hachure-fill": "^0.5.2", "path-data-parser": "^0.1.0", "points-on-curve": "^0.2.0", "points-on-path": "^0.2.1" } }, "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ=="], - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], - "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], - - "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - - "roarr": ["roarr@2.15.4", "", { "dependencies": { "boolean": "^3.0.1", "detect-node": "^2.0.4", "globalthis": "^1.0.1", "json-stringify-safe": "^5.0.1", "semver-compare": "^1.0.0", "sprintf-js": "^1.1.2" } }, "sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A=="], + "rw": ["rw@1.3.3", "", {}, "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ=="], "rxjs": ["rxjs@7.8.2", "", { "dependencies": { "tslib": "^2.1.0" } }, "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sanitize-filename": ["sanitize-filename@1.6.3", "", { "dependencies": { "truncate-utf8-bytes": "^1.0.0" } }, "sha512-y/52Mcy7aw3gRm7IrcGDFx/bCk4AhRh2eI9luHOQM86nZsqwiRkkq2GekHXBBD+SmPidc8i2PqtYZl+pWJ8Oeg=="], - - "sax": ["sax@1.4.3", "", {}, "sha512-yqYn1JhPczigF94DMS+shiDMjDowYO6y9+wB/4WgO0Y19jWYk0lQ4tuG5KI7kj4FTp1wxPj5IFfcrz/s1c3jjQ=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "semver-compare": ["semver-compare@1.0.0", "", {}, "sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow=="], + "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], - "serialize-error": ["serialize-error@7.0.1", "", { "dependencies": { "type-fest": "^0.13.1" } }, "sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw=="], + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1138,51 +887,33 @@ "shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="], - - "slice-ansi": ["slice-ansi@3.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-pSyv7bSTC7ig9Dcgbw9AuRNUb5k5V6oDudjZoMBSr13qpLBG7tB+zgCkARjq7xIUgdz5P1Qe8u+rSGdouOOIyQ=="], + "side-channel": ["side-channel@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4", "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], - "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], - "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], - "socks-proxy-agent": ["socks-proxy-agent@7.0.0", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww=="], - - "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], - - "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="], + "standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="], - "ssri": ["ssri@9.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "stat-mode": ["stat-mode@1.0.0", "", {}, "sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg=="], + "strictdom": ["strictdom@1.0.1", "", {}, "sha512-cEmp9QeXXRmjj/rVp9oyiqcvyocWab/HaoN4+bwFeZ7QzykJD6L3yD4v12K1x0tHpqRqVpJevN3gW7kyM39Bqg=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], - "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], - - "sumchecker": ["sumchecker@3.0.1", "", { "dependencies": { "debug": "^4.1.0" } }, "sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg=="], + "stylis": ["stylis@4.4.0", "", {}, "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -1190,19 +921,9 @@ "tailwindcss": ["tailwindcss@4.1.17", "", {}, "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q=="], - "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], - - "temp": ["temp@0.9.4", "", { "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" } }, "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA=="], - - "temp-file": ["temp-file@3.4.0", "", { "dependencies": { "async-exit-hook": "^2.0.1", "fs-extra": "^10.0.0" } }, "sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg=="], - - "tiny-async-pool": ["tiny-async-pool@1.3.0", "", { "dependencies": { "semver": "^5.5.0" } }, "sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA=="], - - "tiny-typed-emitter": ["tiny-typed-emitter@2.1.0", "", {}, "sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA=="], - - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], + "tinyexec": ["tinyexec@1.3.0", "", {}, "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ=="], - "tmp-promise": ["tmp-promise@3.0.3", "", { "dependencies": { "tmp": "^0.2.0" } }, "sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], @@ -1210,15 +931,15 @@ "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], - "truncate-utf8-bytes": ["truncate-utf8-bytes@1.0.2", "", { "dependencies": { "utf8-byte-length": "^1.0.1" } }, "sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "ts-dedent": ["ts-dedent@2.3.0", "", {}, "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg=="], - "tsx": ["tsx@4.21.0", "", { "dependencies": { "esbuild": "~0.27.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tw-animate-css": ["tw-animate-css@1.4.0", "", {}, "sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ=="], - "type-fest": ["type-fest@0.13.1", "", {}, "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg=="], + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -1226,10 +947,6 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], - "unique-filename": ["unique-filename@2.0.1", "", { "dependencies": { "unique-slug": "^3.0.0" } }, "sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A=="], - - "unique-slug": ["unique-slug@3.0.0", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w=="], - "unist-util-find-after": ["unist-util-find-after@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], @@ -1244,19 +961,15 @@ "universal-user-agent": ["universal-user-agent@7.0.3", "", {}, "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - - "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], "use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="], - "utf8-byte-length": ["utf8-byte-length@1.0.5", "", {}, "sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA=="], - - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@14.0.2", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ=="], - "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], @@ -1264,65 +977,31 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], + "vscode-diff": ["vscode-diff@3.0.1", "", {}, "sha512-0rRDmIuGsuQMIIhbWsILSMxG4k5x8unNvkD7P+yn5cA7O6bKg28P/dhvgABRvP5ksYmGE9UKHonk/Z4PBMdojA=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - "web-vitals": ["web-vitals@4.2.4", "", {}, "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="], - "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + "zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@electron/fuses/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], + "@modelcontextprotocol/sdk/@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], - "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - - "@electron/node-gyp/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - - "@electron/notarize/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@electron/osx-sign/isbinaryfile": ["isbinaryfile@4.0.10", "", {}, "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw=="], - - "@electron/universal/@electron/asar": ["@electron/asar@3.2.18", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg=="], - - "@electron/universal/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - - "@electron/universal/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "@electron/windows-sign/fs-extra": ["fs-extra@11.3.2", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A=="], - - "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], - - "@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], - - "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], - - "@malept/flatpak-bundler/fs-extra": ["fs-extra@9.1.0", "", { "dependencies": { "at-least-node": "^1.0.0", "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ=="], - - "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "@modelcontextprotocol/sdk/hono": ["hono@4.13.4", "", {}, "sha512-AGEwKIyRMHRv1t8Wjwa3LHxQ61X5CqrdFT+4BRNTpqS5aJNnpl5WLjADb7vFlJzI/8uK7T5QLVApCMQKNa3LgQ=="], "@radix-ui/react-collection/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], @@ -1336,142 +1015,34 @@ "@radix-ui/react-tooltip/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="], - "app-builder-lib/@electron/asar": ["@electron/asar@3.2.18", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg=="], - - "cacache/glob": ["glob@8.1.0", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^5.0.1", "once": "^1.3.0" } }, "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ=="], - - "cacache/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "cacache/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], - - "config-file-ts/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "cytoscape-fcose/cose-base": ["cose-base@2.2.0", "", { "dependencies": { "layout-base": "^2.0.0" } }, "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g=="], - "dir-compare/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "d3-dsv/commander": ["commander@7.2.0", "", {}, "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw=="], - "electron-winstaller/@electron/asar": ["@electron/asar@3.2.18", "", { "dependencies": { "commander": "^5.0.0", "glob": "^7.1.6", "minimatch": "^3.0.4" }, "bin": { "asar": "bin/asar.js" } }, "sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg=="], + "d3-dsv/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "electron-winstaller/fs-extra": ["fs-extra@7.0.1", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw=="], + "d3-sankey/d3-array": ["d3-array@2.12.1", "", { "dependencies": { "internmap": "^1.0.0" } }, "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ=="], - "filelist/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], + "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - "hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], - - "make-fetch-happen/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], - - "make-fetch-happen/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - - "make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "matcher/escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - - "minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - "postject/commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - - "restore-cursor/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "socks-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - - "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], - - "temp/mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - - "tiny-async-pool/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "@electron/get/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "@electron/get/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "@electron/node-gyp/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "@electron/universal/@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "@electron/universal/@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "@electron/universal/@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@electron/universal/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - - "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - - "@npmcli/move-file/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "app-builder-lib/@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "app-builder-lib/@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "app-builder-lib/@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "cacache/glob/minimatch": ["minimatch@5.1.6", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g=="], - - "cacache/rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "config-file-ts/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], - - "config-file-ts/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], - - "config-file-ts/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], - - "electron-winstaller/@electron/asar/commander": ["commander@5.1.0", "", {}, "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg=="], - - "electron-winstaller/@electron/asar/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - - "electron-winstaller/@electron/asar/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "electron-winstaller/fs-extra/jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "electron-winstaller/fs-extra/universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - - "filelist/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "make-fetch-happen/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "make-fetch-happen/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - - "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "@electron/node-gyp/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], - - "@npmcli/move-file/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "cacache/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "cacache/rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "cytoscape-fcose/cose-base/layout-base": ["layout-base@2.0.1", "", {}, "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg=="], - "config-file-ts/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + "d3-sankey/d3-array/internmap": ["internmap@1.0.1", "", {}, "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw=="], - "config-file-ts/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + "d3-sankey/d3-shape/d3-path": ["d3-path@1.0.9", "", {}, "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg=="], } } diff --git a/docs/screenshots/filtering.png b/docs/screenshots/filtering.png deleted file mode 100644 index b7b50a7..0000000 Binary files a/docs/screenshots/filtering.png and /dev/null differ diff --git a/docs/screenshots/keybind-driven.png b/docs/screenshots/keybind-driven.png deleted file mode 100644 index a454376..0000000 Binary files a/docs/screenshots/keybind-driven.png and /dev/null differ diff --git a/docs/screenshots/overview.png b/docs/screenshots/overview.png deleted file mode 100644 index f16aee4..0000000 Binary files a/docs/screenshots/overview.png and /dev/null differ diff --git a/docs/screenshots/search.png b/docs/screenshots/search.png deleted file mode 100644 index fab06e4..0000000 Binary files a/docs/screenshots/search.png and /dev/null differ diff --git a/docs/screenshots/viewed-files.png b/docs/screenshots/viewed-files.png deleted file mode 100644 index dba4a2e..0000000 Binary files a/docs/screenshots/viewed-files.png and /dev/null differ diff --git a/docs/semantic-review.md b/docs/semantic-review.md new file mode 100644 index 0000000..dc07a1d --- /dev/null +++ b/docs/semantic-review.md @@ -0,0 +1,220 @@ +# Semantic Review + +On-demand, AI-assisted reorganization of a PR from a flat file list into a guided, +dependency-ordered walkthrough — analogous to CodeRabbit's "Change Stack" — plus a +zero-AI **hide test files** toggle to cut initial cognitive load on large PRs. + +Status: **spec / not implemented** + +## Motivation + +Reviewers experience a PR as an alphabetical file list and must mentally reconstruct +the author's narrative before evaluating correctness. On large (often AI-generated) +PRs, that reconstruction is the bottleneck. better pr already has the fast, +keyboard-driven review shell; this adds the semantic layer on top. + +## Concepts + +- **Cohort** — an independent, logically related group of change _hunks_ + ("the auth refactor", "the new endpoint", "config plumbing"). Grouping is + **hunk-level**: one file's hunks can belong to different cohorts. +- **Layer** — an ordered reading step within a cohort. Layers are dependency-ordered: + foundational changes (types, data shapes, contracts) before consumers/call sites, + then tests. Each layer anchors to one or more **ranges**. +- **Range** — a contiguous span of changed lines in one file (`file`, `side`, + `startLine`, `endLine`) with a plain-language summary. +- **Diagram** — an optional Mermaid diagram (sequence / state / ER) attached to a + layer, generated only when the change warrants a visual. + +## User experience + +### Hide test files (independent of AI) + +- Toggle in the file tree header (near the existing "hide viewed" control): + **Hide tests**. +- Classification is heuristic path matching (`*_test.*`, `*.test.*`, `*.spec.*`, + `__tests__/`, `__mocks__/`, `testdata/`, `test/`, `tests/`, `spec/`, + `*_snapshot*`, `.snap`), implemented as a pure function with unit tests. +- Hidden test files are removed from the tree, `j`/`k` navigation, and search + results, with a count indicator ("12 test files hidden"); toggling back is one + click / keypress. Preference persists globally (localStorage), not per PR. + +### Semantic review mode + +- A **Semantic review** button appears in the PR header when at least one local + provider is available (see Availability). Clicking it: + 1. Checks the disk cache for `(repo, pr, headSha)`. Cache hit → open the mode + instantly. + 2. Otherwise starts an analysis job on the local server and streams progress + ("collecting diff", "provider running", elapsed time). Analysis of a large PR + can take minutes; the job continues if the user navigates away. +- Semantic mode is a **view mode of the existing PR review page**, not a separate + UI. It reuses the diff renderer, comment flows, virtualization, and keyboard model. + - **Left panel**: cohort → layer tree (replaces the file tree while in the mode; + a toggle switches back to Files view at any time). + - **Center**: the existing diff view scoped to the current layer — only hunks + intersecting the layer's ranges are shown, grouped under their file headers. + Commenting, line selection, and expansion work exactly as in file view. + - **Layer summary**: the layer title, summary, and optional diagram render above + the scoped diff. + - **Keyboard**: `j`/`k` move between layers (matching file navigation in file + view); arrows move between lines as today. +- **Reviewed tracking**: layers can be marked reviewed (`v`, same as files). + A file whose every range is inside reviewed layers is marked viewed in the + normal file view (and synced to GitHub's viewed state as today). +- **Staleness**: results are keyed to the head SHA. If the PR receives new commits, + the mode shows an "out of date — re-analyze" banner; the stale result remains + readable. + +### Failure behavior + +Invalid provider output, provider crash, or timeout → a clear error state with a +retry button. Never a broken half-view. Raw provider output is kept on disk next to +the cache entry for debugging. + +## Architecture + +``` +Browser (PRReviewStore) Local server (Hono, node) +──────────────────────── ───────────────────────────────────── +POST /api/semantic/analyze ────────► job runner ──► provider (pluggable) + { owner, repo, number, │ ├─ claude (Agent SDK) + headSha, prMeta, diff } │ └─ codex (codex exec) +GET /api/semantic/jobs/:id (SSE) ◄─── progress + result +GET /api/semantic/result/... ◄── disk cache ~/.better-pr/semantic/ +GET /api/semantic/providers ◄── availability detection +``` + +- The **browser supplies the input** (PR title/description + per-file patches it + already fetched from GitHub). The local server never holds GitHub credentials. +- Endpoints live in the shared Hono `api` app (`src/api/`). Static deployments + have no server, so `/api/semantic/providers` fails and the browser hides the + feature. + +### Provider interface + +```ts +interface SemanticProvider { + id: string; // "claude" | "codex" | ... + displayName: string; + available(): Promise; // auth/CLI detection, cheap + analyze( + input: AnalysisInput, + onProgress: (msg: string) => void, + signal: AbortSignal + ): Promise; +} + +interface AnalysisInput { + owner: string; + repo: string; + number: number; + headSha: string; + title: string; + body: string; + files: { filename: string; status: string; patch?: string }[]; +} +``` + +- **claude**: `@anthropic-ai/claude-agent-sdk` `query()` — rides the user's + Claude subscription auth (no API key handling in better pr). +- **codex**: spawns `codex exec` — rides the user's ChatGPT subscription. +- Both receive the same prompt contract and must emit JSON conforming to the + schema below; output is validated before acceptance and the provider is asked to + self-correct once on validation failure. +- `AnalysisInput` is deliberately abstract so a future **checkout-based** provider + (agent explores a local clone for deeper dependency inference) can slot in + without changing the schema or UI. +- If more than one provider is available, the analyze button offers a picker; + the last choice is remembered. + +### Output schema + +```ts +interface SemanticReview { + version: 1; + provider: string; + headSha: string; + generatedAt: string; // ISO 8601 + overview: string; // markdown, 2–5 sentences + cohorts: Cohort[]; +} +interface Cohort { + id: string; + title: string; + summary: string; + layers: Layer[]; +} +interface Layer { + id: string; + title: string; + summary: string; // markdown + diagram?: { kind: "sequence" | "state" | "er" | "flow"; mermaid: string }; + ranges: Range[]; +} +interface Range { + file: string; + side: "new" | "old"; // "old" only for pure deletions + startLine: number; + endLine: number; + summary?: string; +} +``` + +**Coverage invariant**: every hunk in the diff must intersect at least one range. +The validator computes uncovered hunks; any remainder is placed in an automatic +**"Uncovered changes"** cohort rather than silently dropped — the reviewer must be +able to trust that semantic mode shows the whole PR. Ranges that don't match any +actual hunk are pruned with a warning. + +### Caching + +- Disk: `~/.better-pr/semantic/----/.json` + (+ `.raw.txt` for the unvalidated provider output). +- Keyed by head SHA, so historical revisions accumulate naturally — enabling a + future "what changed since I last reviewed" snapshot feature. +- No TTL; entries are immutable per SHA. A `Re-analyze` action overwrites. + +### Prompt contract (summary) + +The provider prompt instructs the agent to: + +1. Read PR title/body and the full unified diff. +2. Partition all hunks into 2–7 independent cohorts by intent. +3. Order layers within each cohort foundation-first (contracts → consumers → tests). +4. Anchor every layer to exact file/line ranges from the diff (new side line + numbers), covering every hunk exactly once. +5. Write range/layer summaries in plain language explaining _why_, not restating + the diff. +6. Emit a Mermaid diagram only when the layer introduces an API interaction, + state machine, or schema relationship. +7. Output only JSON matching the schema. + +Lockfile/generated-file patch bodies are always elided (listed by name only). +Each provider declares a prompt budget matched to its context window. Diffs that +fit the budget run as a single prompt. Larger diffs are map-reduced: the diff is +partitioned into sections that each fit, a map pass annotates every section's +hunks with semantic fragments (range + label + summary), and a reduce pass +organizes all fragments into the final cohort/layer structure without re-reading +patches. If a provider still rejects a prompt as too long, the job runner halves +the budget and retries (eliding the largest patches, or shortening fragment +summaries) instead of failing. + +## Non-goals (v1) + +- Checkout/repo-context analysis (interface keeps the door open). +- Cross-revision diffs of semantic results ("what changed since snapshot N"). +- Sharing/exporting semantic reviews. +- Semantic review on the hosted deployment (requires a local agent by design). +- Author-defined manual grouping. + +## Performance notes + +Per AGENTS.md, performance is P1: + +- Analysis runs entirely out-of-process (local server + external agent); the main + thread only receives SSE progress strings and one JSON payload. +- Semantic mode reuses the existing parsed-diff cache, worker highlighting, and + virtualized rendering; scoping to ranges is a filter over already-parsed hunks. +- Mermaid rendering is lazy-loaded (dynamic import) only when a layer with a + diagram is opened, keeping it out of the main bundle. diff --git a/electron-builder.json b/electron-builder.json deleted file mode 100644 index df2d618..0000000 --- a/electron-builder.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", - "appId": "com.coder.pulldash", - "productName": "Pulldash", - "copyright": "Copyright © 2025 Coder", - - "directories": { - "output": "release", - "buildResources": "build", - "app": "dist" - }, - - "files": ["electron/**/*", "browser/**/*", "!browser/**/*.map"], - - "extraResources": [ - { - "from": "dist/browser", - "to": "browser", - "filter": ["**/*", "!**/*.map"] - } - ], - - "mac": { - "category": "public.app-category.developer-tools", - "icon": "build/icon.icns", - "darkModeSupport": true, - "hardenedRuntime": true, - "gatekeeperAssess": false, - "target": [ - { - "target": "dmg", - "arch": ["x64", "arm64"] - }, - { - "target": "zip", - "arch": ["x64", "arm64"] - } - ] - }, - - "dmg": { - "icon": "build/icon.icns", - "iconSize": 128, - "contents": [ - { - "x": 130, - "y": 220 - }, - { - "x": 410, - "y": 220, - "type": "link", - "path": "/Applications" - } - ], - "window": { - "width": 540, - "height": 380 - } - }, - - "win": { - "icon": "build/icon.ico", - "target": [ - { - "target": "nsis", - "arch": ["x64", "arm64"] - }, - { - "target": "portable", - "arch": ["x64"] - } - ] - }, - - "nsis": { - "oneClick": false, - "perMachine": false, - "allowToChangeInstallationDirectory": true, - "deleteAppDataOnUninstall": true, - "installerIcon": "build/icon.ico", - "uninstallerIcon": "build/icon.ico", - "installerHeaderIcon": "build/icon.ico", - "createDesktopShortcut": true, - "createStartMenuShortcut": true, - "shortcutName": "Pulldash" - }, - - "linux": { - "icon": "build/icon.png", - "category": "Development", - "target": [ - { - "target": "AppImage", - "arch": ["x64", "arm64"] - }, - { - "target": "tar.gz", - "arch": ["x64", "arm64"] - } - ], - "synopsis": "Fast GitHub PR Reviews", - "description": "A lightning-fast, local PR review dashboard with syntax highlighting." - }, - - "publish": { - "provider": "github", - "owner": "coder", - "repo": "pulldash", - "releaseType": "release" - } -} diff --git a/eslint.config.mjs b/eslint.config.mjs deleted file mode 100644 index 05e726d..0000000 --- a/eslint.config.mjs +++ /dev/null @@ -1,18 +0,0 @@ -import { defineConfig, globalIgnores } from "eslint/config"; -import nextVitals from "eslint-config-next/core-web-vitals"; -import nextTs from "eslint-config-next/typescript"; - -const eslintConfig = defineConfig([ - ...nextVitals, - ...nextTs, - // Override default ignores of eslint-config-next. - globalIgnores([ - // Default ignores of eslint-config-next: - ".next/**", - "out/**", - "build/**", - "next-env.d.ts", - ]), -]); - -export default eslintConfig; diff --git a/package.json b/package.json index 35b9630..33736dc 100644 --- a/package.json +++ b/package.json @@ -1,31 +1,15 @@ { - "name": "pulldash", + "name": "better-pr", "version": "0.0.5", - "description": "Fast, filterable PR review. Entirely client-side.", + "description": "Fast, filterable PR review.", "type": "module", - "bin": { - "pulldash": "./dist/pulldash" - }, - "files": [ - "dist" - ], - "main": "dist/electron/main.js", "scripts": { "dev": "concurrently \"bun run build:browser --watch\" \"bun run dev:node\"", "dev:node": "bun run src/node/main.ts", + "start": "bun run build:browser && bun run src/node/main.ts", "typecheck": "tsgo --noEmit", "build:browser": "bun run ./scripts/build-browser.ts", - "electron:dev": "concurrently \"bun run build:browser --watch\" \"bun run scripts/electron-dev.ts\"", - "electron:build": "bun run scripts/build-electron.ts", - "electron:package": "bun run electron:build && electron-builder --config electron-builder.json", - "electron:package:mac": "bun run electron:build && electron-builder --config electron-builder.json --mac", - "electron:package:win": "bun run electron:build && electron-builder --config electron-builder.json --win", - "electron:package:linux": "bun run electron:build && electron-builder --config electron-builder.json --linux", - "electron:publish": "bun run electron:build && electron-builder --config electron-builder.json --publish always", - "electron:publish:mac": "bun run electron:build && electron-builder --config electron-builder.json --mac --publish always", - "electron:publish:win": "bun run electron:build && electron-builder --config electron-builder.json --win --publish always", - "electron:publish:linux": "bun run electron:build && electron-builder --config electron-builder.json --linux --publish always", - "icons": "bun run scripts/generate-icons.ts", + "deploy": "bun run ./scripts/deploy-azure.ts", "fmt": "prettier --write --cache .", "fmt:check": "prettier --check .", "lint": "echo 'No linter configured yet'", @@ -35,17 +19,11 @@ "github", "pull-request", "code-review", - "diff", - "cli" + "diff" ], "license": "AGPL-3.0-only", - "author": { - "name": "Coder", - "email": "support@coder.com", - "url": "https://coder.com" - }, "devDependencies": { - "@electron/asar": "^4.0.1", + "@anthropic-ai/claude-agent-sdk": "^0.3.282", "@hono/node-server": "^1.19.6", "@octokit/core": "^7.0.6", "@radix-ui/react-collapsible": "^1.1.12", @@ -64,15 +42,9 @@ "cmdk": "^1.1.1", "concurrently": "^9.2.1", "diff": "^8.0.2", - "electron": "^39.2.4", - "electron-builder": "^26.0.12", - "electron-updater": "^6.6.2", - "geist": "^1.5.1", "gitdiff-parser": "^0.3.1", "hono": "^4.10.7", "lucide-react": "^0.555.0", - "png2icons": "^2.0.1", - "posthog-js": "^1.300.0", "prettier": "^3.7.3", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -80,23 +52,24 @@ "react-router-dom": "^7.9.6", "refractor": "^5.0.0", "remark-gfm": "^4.0.1", - "sharp": "^0.34.5", "tailwind-merge": "^3.4.0", "tailwindcss": "^4", - "tsx": "^4.21.0", "tw-animate-css": "^1.4.0", "typescript": "^5" }, "dependencies": { + "@openai/codex-sdk": "^0.156.1", "@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-hover-card": "^1.1.15", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-tabs": "^1.1.13", + "mermaid": "^11.17.2", "rehype-highlight": "^7.0.2", "rehype-raw": "^7.0.0", "rehype-sanitize": "^6.0.0", - "remark-gemoji": "^8.0.0" + "remark-gemoji": "^8.0.0", + "vscode-diff": "^3.0.1" } } diff --git a/scripts/build-browser.ts b/scripts/build-browser.ts index bce13fa..e9f93e3 100644 --- a/scripts/build-browser.ts +++ b/scripts/build-browser.ts @@ -1,11 +1,39 @@ import tailwind from "bun-plugin-tailwind"; +import { createHash } from "crypto"; import { watch } from "fs"; -import { cp } from "fs/promises"; +import { cp, rm } from "fs/promises"; import { resolve } from "path"; const isWatch = process.argv.includes("--watch"); +/** + * Copy the Static Web App config, allowing the inline + diff --git a/src/browser/index.tsx b/src/browser/index.tsx index 607b443..ccace23 100644 --- a/src/browser/index.tsx +++ b/src/browser/index.tsx @@ -2,35 +2,34 @@ import { createRoot } from "react-dom/client"; import { BrowserRouter, Routes, Route } from "react-router-dom"; import { AuthProvider } from "./contexts/auth"; import { GitHubProvider } from "./contexts/github"; -import { TelemetryProvider } from "./contexts/telemetry"; import { TabProvider } from "./contexts/tabs"; import { CommandPaletteProvider } from "./components/command-palette"; import { AppShell } from "./components/app-shell"; import { WelcomeDialog } from "./components/welcome-dialog"; +import { getTheme } from "./lib/theme"; import "./index.css"; +// Re-applies the persisted theme (index.html already set it pre-paint) and +// starts listening for system colour-scheme changes. +getTheme(); + createRoot(document.getElementById("app")!).render( - - - - - - {/* Home */} - } /> - {/* PR review - URL like /:owner/:repo/pull/:number */} - } - /> - - {/* Auth dialog - shown when not authenticated */} - - - - - + + + + + {/* Home */} + } /> + {/* PR review - URL like /:owner/:repo/pull/:number */} + } /> + + {/* Auth dialog - shown when not authenticated */} + + + + ); diff --git a/src/browser/lib/changelog.test.ts b/src/browser/lib/changelog.test.ts new file mode 100644 index 0000000..4e3ac09 --- /dev/null +++ b/src/browser/lib/changelog.test.ts @@ -0,0 +1,19 @@ +import { test, expect } from "bun:test"; +import { unseenEntries, type ChangelogEntry } from "./changelog"; + +const entries: ChangelogEntry[] = [ + { id: "c", date: "3", title: "C" }, + { id: "b", date: "2", title: "B" }, + { id: "a", date: "1", title: "A" }, +]; + +test("unseenEntries returns entries newer than the last seen id", () => { + expect(unseenEntries(entries, null).map((e) => e.id)).toEqual([ + "c", + "b", + "a", + ]); + expect(unseenEntries(entries, "a").map((e) => e.id)).toEqual(["c", "b"]); + expect(unseenEntries(entries, "c")).toEqual([]); + expect(unseenEntries(entries, "gone")).toEqual(entries); +}); diff --git a/src/browser/lib/changelog.ts b/src/browser/lib/changelog.ts new file mode 100644 index 0000000..c60b961 --- /dev/null +++ b/src/browser/lib/changelog.ts @@ -0,0 +1,78 @@ +// User-facing change log shown in the "What's new" dialog. Newest first. +// Add an entry here for every user-visible feature or change (see AGENTS.md). +// `id` must be unique and sortable; it is stored in localStorage as the last +// entry the user has seen. + +export type ChangelogEntry = { + id: string; + date: string; + title: string; + description?: string; +}; + +export const CHANGELOG: ChangelogEntry[] = [ + { + id: "2026-10-06-pr-stack-nav", + date: "2026-10-06", + title: "PR stack navigation", + description: + "Jump between PRs in a stack. Commits and checks tabs now work on mobile.", + }, + { + id: "2026-10-07-mobile", + date: "2026-10-07", + title: "Better mobile layout", + description: + "Responsive layout for narrow viewports, and long diff lines scroll horizontally instead of wrapping.", + }, + { + id: "2026-10-06-diff-stat", + date: "2026-10-06", + title: "Diff stat for visible files", + description: "Shown next to the hidden test files count.", + }, + { + id: "2026-10-02-auto-merge", + date: "2026-10-02", + title: "Schedule auto-merge", + description: "Enable auto-merge right from the PR merge box.", + }, + { + id: "2026-10-02-branch-search", + date: "2026-10-02", + title: "Search by branch name", + description: "Paste a branch name into the header search to find its PR.", + }, + { + id: "2026-10-02-pr-search", + date: "2026-10-02", + title: "Smarter PR search", + description: + "Two-stage search also finds PRs that aren't in your feed repos.", + }, + { + id: "2026-10-02-tree-expand", + date: "2026-10-02", + title: "Expand/collapse folders recursively", + description: "Available from the file tree folder context menu.", + }, + { + id: "2026-10-02-all-files", + date: "2026-10-02", + title: "All Files view polish", + description: + "File header repeats below each diff, the footer bar only shows for longer diffs, and viewed toggles no longer overlap files.", + }, +]; + +export const LAST_SEEN_KEY = "better_pr_last_seen_change"; + +/** Entries newer than `lastSeenId`. Unknown/missing id means all are unseen. */ +export function unseenEntries( + entries: ChangelogEntry[], + lastSeenId: string | null +): ChangelogEntry[] { + if (lastSeenId === null) return entries; + const idx = entries.findIndex((e) => e.id === lastSeenId); + return idx === -1 ? entries : entries.slice(0, idx); +} diff --git a/src/browser/lib/conditional-requests.test.ts b/src/browser/lib/conditional-requests.test.ts new file mode 100644 index 0000000..28d93dc --- /dev/null +++ b/src/browser/lib/conditional-requests.test.ts @@ -0,0 +1,29 @@ +import { expect, test } from "bun:test"; +import { Octokit } from "@octokit/core"; +import { addConditionalRequests } from "./conditional-requests"; +import { MemoryPersistentStore } from "./persistent-cache"; + +test("replays the stored body when GitHub answers 304", async () => { + const sent: Array = []; + const cacheModes: Array = []; + const fetch = async (_url: string, init: RequestInit) => { + const ifNoneMatch = new Headers(init.headers).get("if-none-match"); + sent.push(ifNoneMatch); + cacheModes.push(init.cache); + if (ifNoneMatch === '"v1"') return new Response(null, { status: 304 }); + return Response.json({ title: "cached" }, { headers: { etag: '"v1"' } }); + }; + const octokit = new Octokit({ request: { fetch } }); + addConditionalRequests(octokit, new MemoryPersistentStore()); + + const route = "GET /repos/{owner}/{repo}/pulls/{pull_number}"; + const params = { owner: "o", repo: "r", pull_number: 1 }; + const first = await octokit.request(route, params); + const second = await octokit.request(route, params); + + expect(sent).toEqual([null, '"v1"']); + // The browser's HTTP cache must never answer for GitHub + expect(cacheModes).toEqual(["no-store", "no-store"]); + expect(second.status).toBe(200); + expect(second.data).toEqual(first.data); +}); diff --git a/src/browser/lib/conditional-requests.ts b/src/browser/lib/conditional-requests.ts new file mode 100644 index 0000000..fb79cf2 --- /dev/null +++ b/src/browser/lib/conditional-requests.ts @@ -0,0 +1,79 @@ +import type { Octokit } from "@octokit/core"; +import type { PersistentStore } from "./persistent-cache"; + +interface StoredResponse { + etag: string; + status: number; + url: string; + data: unknown; +} + +const ETAG_PREFIX = "etag:"; + +export function hasStatus(error: unknown, status: number): boolean { + return ( + !!error && + typeof error === "object" && + "status" in error && + error.status === status + ); +} + +/** + * Revalidate GETs with the ETag from the last response. GitHub answers an + * unchanged resource with an empty 304 that doesn't count against the rate + * limit, and we replay the stored body. + */ +export function addConditionalRequests( + octokitInstance: Octokit, + persistent: PersistentStore +) { + octokitInstance.hook.wrap("request", async (request, options) => { + if (options.method !== "GET") return request(options); + const { url } = octokitInstance.request.endpoint.parse(options); + // File contents are fetched by commit SHA and cached separately. + if (url.includes("/contents/")) return request(options); + + const key = `${ETAG_PREFIX}${url} ${options.headers.accept ?? ""}`; + const stored = (await persistent.get(key))?.data; + if (stored) { + options.headers = { ...options.headers, "if-none-match": stored.etag }; + } + // GitHub marks responses `private, max-age=60`, so the browser would + // otherwise answer from its own cache for a minute, even right after we + // changed something. Revalidation above replaces that cache. + const baseFetch: typeof fetch = options.request?.fetch ?? fetch; + options.request = { + ...options.request, + fetch: (input: RequestInfo | URL, init?: RequestInit) => + baseFetch(input, { ...init, cache: "no-store" }), + }; + + try { + const response = await request(options); + const etag = response.headers.etag; + if (etag) { + persistent.set(key, { + data: { + etag, + status: response.status, + url: response.url, + data: response.data, + } satisfies StoredResponse, + timestamp: Date.now(), + }); + } + return response; + } catch (error) { + if (stored && hasStatus(error, 304)) { + return { + status: stored.status, + url: stored.url, + headers: { etag: stored.etag }, + data: stored.data, + }; + } + throw error; + } + }); +} diff --git a/src/browser/lib/diff-worker.ts b/src/browser/lib/diff-worker.ts index b0bae1a..8c2bdb5 100644 --- a/src/browser/lib/diff-worker.ts +++ b/src/browser/lib/diff-worker.ts @@ -4,13 +4,10 @@ * This worker processes diffs off the main thread to maintain UI responsiveness. */ -import gitDiffParser, { - Hunk as _Hunk, - Change as _Change, - DeleteChange, - InsertChange, -} from "gitdiff-parser"; -import { diffChars, diffWords } from "diff"; +import gitDiffParser, { Hunk as _Hunk } from "gitdiff-parser"; +import { computeHunkLines, type Line } from "./hunk-lines"; +import { generatePatch } from "./recover-patches"; +import { rebasedPatch, type RebaseResult } from "./rebase-range"; import { refractor } from "refractor/all"; // ============================================================================ @@ -23,11 +20,6 @@ export interface LineSegment { type: "insert" | "delete" | "normal"; } -interface RawLineSegment { - value: string; - type: "insert" | "delete" | "normal"; -} - export interface DiffLine { type: "insert" | "delete" | "normal"; lineNumber?: number; @@ -51,21 +43,16 @@ export interface DiffSkipBlock { export interface ParsedDiff { hunks: (DiffHunk | DiffSkipBlock)[]; + /** + * Default context revealed around each gap (keyed by skip index; the + * index one past the last skip block is the end-of-file gap). Present + * only when the new file's content was available at parse time. + */ + gapContext?: Record; + /** Total line count of the new file, when content was available. */ + totalNewLines?: number; } -interface ParseOptions { - maxDiffDistance: number; - maxChangeRatio: number; - mergeModifiedLines: boolean; - inlineMaxCharEdits: number; -} - -type ReplaceKey = T extends unknown - ? Omit & Record - : never; - -type Line = ReplaceKey<_Change, "content", RawLineSegment[]>; - interface Hunk extends Omit<_Hunk, "changes"> { type: "hunk"; lines: Line[]; @@ -82,6 +69,20 @@ interface SkipBlock { // ============================================================================ export type WorkerRequest = + | { + type: "generate-patch"; + id: string; + oldContent: string; + newContent: string; + } + | { + type: "rebase-patch"; + id: string; + baseOld: string; + ours: string; + baseNew: string; + head: string; + } | { type: "parse-diff"; id: string; @@ -103,6 +104,8 @@ export type WorkerRequest = }; export type WorkerResponse = + | { type: "generate-patch-result"; id: string; result: string | undefined } + | { type: "rebase-patch-result"; id: string; result: RebaseResult } | { type: "parse-diff-result"; id: string; @@ -330,274 +333,11 @@ function highlightFileByLines(content: string, lang: string): string[] { // Diff Parsing // ============================================================================ -const calculateChangeRatio = (a: string, b: string): number => { - const totalChars = a.length + b.length; - if (totalChars === 0) return 1; - const tokens = diffWords(a, b); - const changedChars = tokens - .filter((token) => token.added || token.removed) - .reduce((sum, token) => sum + token.value.length, 0); - return changedChars / totalChars; -}; - -const changeToLine = (change: _Change): Line => ({ - ...change, - content: [{ value: change.content, type: "normal" }], -}); - -function diffCharsIfWithinEditLimit(a: string, b: string, maxEdits = 4) { - const diffs = diffChars(a, b); - let edits = 0; - for (const part of diffs) { - if (part.added || part.removed) { - edits += part.value.length; - if (edits > maxEdits) return { exceededLimit: true }; - } - } - return { - exceededLimit: false, - diffs: diffs.map((d) => ({ - value: d.value, - type: d.added ? "insert" : d.removed ? "delete" : "normal", - })) as RawLineSegment[], - }; -} - -const buildInlineDiffSegments = ( - current: _Change, - next: _Change, - options: ParseOptions -): RawLineSegment[] => { - const segments: RawLineSegment[] = diffWords( - current.content, - next.content - ).map((token) => ({ - value: token.value, - type: token.added ? "insert" : token.removed ? "delete" : "normal", - })); - - const result: RawLineSegment[] = []; - const mergeIntoResult = (segment: RawLineSegment) => { - const last = result[result.length - 1]; - if (last && last.type === segment.type) { - last.value += segment.value; - } else { - result.push(segment); - } - }; - - for (let i = 0; i < segments.length; i++) { - const current = segments[i]; - const next = segments[i + 1]; - if (current.type === "delete" && next?.type === "insert") { - const charDiff = diffCharsIfWithinEditLimit( - current.value, - next.value, - options.inlineMaxCharEdits - ); - if (!charDiff.exceededLimit) { - charDiff.diffs!.forEach(mergeIntoResult); - i++; - } else { - result.push(current); - } - } else { - mergeIntoResult(current); - } - } - - return result; -}; - -const UNPAIRED = -1; - -function buildChangeIndices(changes: _Change[]) { - const insertIdxs: number[] = []; - const deleteIdxs: number[] = []; - for (let i = 0; i < changes.length; i++) { - const c = changes[i]; - if (c.type === "insert") insertIdxs.push(i); - else if (c.type === "delete") deleteIdxs.push(i); - } - return { insertIdxs, deleteIdxs }; -} - -function findBestInsertForDelete( - changes: _Change[], - delIdx: number, - insertIdxs: number[], - pairOfAdd: Int32Array, - options: ParseOptions -): number { - const del = changes[delIdx] as DeleteChange; - const lower = del.lineNumber - options.maxDiffDistance; - const upper = del.lineNumber + options.maxDiffDistance; - - let bestAddIdx = UNPAIRED; - let bestRatio = Infinity; - - for (const addIdx of insertIdxs) { - const add = changes[addIdx] as InsertChange; - if (pairOfAdd[addIdx] !== UNPAIRED) continue; - if (add.lineNumber < lower) continue; - if (add.lineNumber > upper) break; - - const ratio = calculateChangeRatio(del.content, add.content); - if (ratio > options.maxChangeRatio) continue; - if (ratio < bestRatio) { - bestRatio = ratio; - bestAddIdx = addIdx; - } - } - - return bestAddIdx; -} - -function buildInitialPairs( - changes: _Change[], - insertIdxs: number[], - deleteIdxs: number[], - options: ParseOptions -) { - const n = changes.length; - const pairOfDel = new Int32Array(n).fill(UNPAIRED); - const pairOfAdd = new Int32Array(n).fill(UNPAIRED); - - for (const di of deleteIdxs) { - const bestAddIdx = findBestInsertForDelete( - changes, - di, - insertIdxs, - pairOfAdd, - options - ); - if (bestAddIdx !== UNPAIRED) { - pairOfDel[di] = bestAddIdx; - pairOfAdd[bestAddIdx] = di; - } - } - - return { pairOfDel, pairOfAdd }; -} - -function buildUnpairedDeletePrefix(changes: _Change[], pairOfDel: Int32Array) { - const n = changes.length; - const prefix = new Int32Array(n + 1); - for (let i = 0; i < n; i++) { - const c = changes[i]; - const isInitiallyUnpairedDelete = - c.type === "delete" && pairOfDel[i] === UNPAIRED; - prefix[i + 1] = prefix[i] + (isInitiallyUnpairedDelete ? 1 : 0); - } - return prefix; -} - -function hasUnpairedDeleteBetween( - unpairedDelPrefix: Int32Array, - deleteIdx: number, - insertIdx: number -) { - const lower = Math.max(0, deleteIdx); - const upper = Math.max(lower, insertIdx); - return unpairedDelPrefix[upper] - unpairedDelPrefix[lower] > 0; -} - -function emitNormal(out: Line[], c: _Change) { - out.push(changeToLine(c)); -} - -function emitModified( - out: Line[], - del: DeleteChange, - add: InsertChange, - options: ParseOptions -) { - out.push({ - oldLineNumber: del.lineNumber, - newLineNumber: add.lineNumber, - type: "normal", - isNormal: true, - content: buildInlineDiffSegments(del, add, options), - }); -} - -function emitLines( - changes: _Change[], - pairOfDel: Int32Array, - pairOfAdd: Int32Array, - unpairedDelPrefix: Int32Array, - options: ParseOptions -): Line[] { - const out: Line[] = []; - const processed = new Uint8Array(changes.length); - - for (let i = 0; i < changes.length; i++) { - if (processed[i]) continue; - const c = changes[i]; - - if (c.type === "normal") { - processed[i] = 1; - emitNormal(out, c); - } else if (c.type === "delete") { - const pairedAddIdx = pairOfDel[i]; - if (pairedAddIdx === UNPAIRED) { - processed[i] = 1; - emitNormal(out, c); - } else if (pairedAddIdx > i) { - const shouldUnpair = hasUnpairedDeleteBetween( - unpairedDelPrefix, - i + 1, - pairedAddIdx - ); - if (shouldUnpair) { - pairOfAdd[pairedAddIdx] = UNPAIRED; - processed[i] = 1; - emitNormal(out, c); - } else { - processed[i] = 1; - } - } else { - const add = changes[pairedAddIdx] as InsertChange; - emitModified(out, c, add, options); - processed[i] = 1; - processed[pairedAddIdx] = 1; - } - } else { - const pairedDelIdx = pairOfAdd[i]; - if (pairedDelIdx === UNPAIRED) { - processed[i] = 1; - emitNormal(out, c); - } else { - const del = changes[pairedDelIdx] as DeleteChange; - emitModified(out, del, c, options); - processed[i] = 1; - processed[pairedDelIdx] = 1; - } - } - } - - return out; -} - -function mergeModifiedLines(changes: _Change[], options: ParseOptions): Line[] { - const { insertIdxs, deleteIdxs } = buildChangeIndices(changes); - const { pairOfDel, pairOfAdd } = buildInitialPairs( - changes, - insertIdxs, - deleteIdxs, - options - ); - const unpairedDelPrefix = buildUnpairedDeletePrefix(changes, pairOfDel); - return emitLines(changes, pairOfDel, pairOfAdd, unpairedDelPrefix, options); -} - -const parseHunk = (hunk: _Hunk, options: ParseOptions): Hunk => { +const parseHunk = (hunk: _Hunk): Hunk => { return { ...hunk, type: "hunk", - lines: options.mergeModifiedLines - ? mergeModifiedLines(hunk.changes, options) - : hunk.changes.map(changeToLine), + lines: computeHunkLines(hunk.changes), }; }; @@ -627,13 +367,6 @@ const insertSkipBlocks = (hunks: Hunk[]): (Hunk | SkipBlock)[] => { return result; }; -const defaultOptions: ParseOptions = { - maxDiffDistance: 30, - maxChangeRatio: 0.45, - mergeModifiedLines: true, - inlineMaxCharEdits: 4, -}; - // ============================================================================ // Main Functions // ============================================================================ @@ -650,7 +383,6 @@ function parseDiffWithHighlighting( +++ b/${filename} ${patch}`; - const opts = defaultOptions; const files = gitDiffParser.parse(diffHeader); const file = files[0]; @@ -672,9 +404,7 @@ ${patch}`; ? highlightFileByLines(newContent, language) : null; - const rawHunks = insertSkipBlocks( - file.hunks.map((hunk) => parseHunk(hunk, opts)) - ); + const rawHunks = insertSkipBlocks(file.hunks.map((hunk) => parseHunk(hunk))); const hunks: (DiffHunk | DiffSkipBlock)[] = rawHunks.map((hunk) => { if (hunk.type === "skip") { @@ -760,9 +490,105 @@ ${patch}`; }; }); + // Reveal extra context around each gap by default, GitHub-style, using + // the already-highlighted new file content (near-free at this point). + if (newContent && newHighlightedLines) { + const allNewLines = newContent.split("\n"); + const totalNewLines = + allNewLines[allNewLines.length - 1] === "" + ? allNewLines.length - 1 + : allNewLines.length; + + const makeLines = (start: number, count: number): DiffLine[] => { + const lines: DiffLine[] = []; + for (let i = 0; i < count; i++) { + const lineNum = start + i; + const value = allNewLines[lineNum - 1] ?? ""; + lines.push({ + type: "normal", + oldLineNumber: lineNum, + newLineNumber: lineNum, + content: [ + { + value, + html: + newHighlightedLines[lineNum - 1] ?? highlight(value, language), + type: "normal", + }, + ], + }); + } + return lines; + }; + + const gapContext: Record = + {}; + let expectedNextLine = 1; + let skipIdx = 0; + let seenHunk = false; + + for (const h of hunks) { + if (h.type === "skip") { + const start = expectedNextLine; + const count = h.count; + if (!seenHunk) { + // Top-of-file gap: only lines adjacent to the first change + gapContext[skipIdx] = + count <= DEFAULT_GAP_CONTEXT + ? { top: makeLines(start, count), bottom: [] } + : { + top: [], + bottom: makeLines( + start + count - DEFAULT_GAP_CONTEXT, + DEFAULT_GAP_CONTEXT + ), + }; + } else if (count <= DEFAULT_GAP_CONTEXT * 2) { + gapContext[skipIdx] = { top: makeLines(start, count), bottom: [] }; + } else { + gapContext[skipIdx] = { + top: makeLines(start, DEFAULT_GAP_CONTEXT), + bottom: makeLines( + start + count - DEFAULT_GAP_CONTEXT, + DEFAULT_GAP_CONTEXT + ), + }; + } + expectedNextLine += count; + skipIdx++; + } else { + seenHunk = true; + let maxNewLine = h.newStart; + for (const line of h.lines) { + if (line.newLineNumber && line.newLineNumber > maxNewLine) { + maxNewLine = line.newLineNumber; + } + } + expectedNextLine = maxNewLine + 1; + } + } + + // End-of-file gap (index one past the last skip block) + const trailingRemaining = totalNewLines - expectedNextLine + 1; + if (seenHunk && trailingRemaining > 0) { + gapContext[skipIdx] = { + top: makeLines( + expectedNextLine, + Math.min(DEFAULT_GAP_CONTEXT, trailingRemaining) + ), + bottom: [], + }; + } + + return { hunks, gapContext, totalNewLines }; + } + return { hunks }; } +/** Lines revealed around each gap by default. */ +const DEFAULT_GAP_CONTEXT = 20; + function highlightFileLines( content: string, filename: string, @@ -804,6 +630,27 @@ self.onmessage = (event: MessageEvent) => { try { switch (request.type) { + case "generate-patch": { + self.postMessage({ + type: "generate-patch-result", + id: request.id, + result: generatePatch(request.oldContent, request.newContent), + } as WorkerResponse); + break; + } + case "rebase-patch": { + self.postMessage({ + type: "rebase-patch-result", + id: request.id, + result: rebasedPatch( + request.baseOld, + request.ours, + request.baseNew, + request.head + ), + } as WorkerResponse); + break; + } case "parse-diff": { const result = parseDiffWithHighlighting( request.patch, diff --git a/src/browser/lib/diff.ts b/src/browser/lib/diff.ts index 36a82d6..6764cab 100644 --- a/src/browser/lib/diff.ts +++ b/src/browser/lib/diff.ts @@ -12,6 +12,7 @@ import type { ParsedDiff, DiffLine, } from "./diff-worker"; +import type { RebaseResult } from "./rebase-range"; // Re-export types for consumers export type { @@ -81,6 +82,45 @@ class DiffWorkerPool { return `${Date.now()}-${this.nextId++}`; } + async generatePatch( + oldContent: string, + newContent: string + ): Promise { + const id = this.generateId(); + const worker = this.getNextWorker(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + worker.postMessage({ + type: "generate-patch", + id, + oldContent, + newContent, + } as WorkerRequest); + }); + } + + /** Diff a start file, rebased onto the new merge base, against head. */ + async rebasePatch( + baseOld: string, + ours: string, + baseNew: string, + head: string + ): Promise { + const id = this.generateId(); + const worker = this.getNextWorker(); + return new Promise((resolve, reject) => { + this.pendingRequests.set(id, { resolve, reject }); + worker.postMessage({ + type: "rebase-patch", + id, + baseOld, + ours, + baseNew, + head, + } as WorkerRequest); + }); + } + async parseDiff( patch: string, filename: string, diff --git a/src/browser/lib/feed-filters.test.ts b/src/browser/lib/feed-filters.test.ts new file mode 100644 index 0000000..fa2dd1b --- /dev/null +++ b/src/browser/lib/feed-filters.test.ts @@ -0,0 +1,24 @@ +import { test, expect } from "bun:test"; +import { + ALL_REPOS_KEY, + buildSearchScopeQueries, + type FilterConfig, +} from "./feed-filters"; + +test("feed-filters: search scope drops mode for specific repos but keeps All Repos filters", () => { + const config: FilterConfig = { + state: "open", + repos: [ + { name: "a/b", mode: "review-requested" }, + { name: "c/d", mode: "authored" }, + { name: "a/b", mode: "reviewed" }, + { name: "off/repo", mode: "authored", enabled: false }, + { name: ALL_REPOS_KEY, mode: "involves" }, + ], + }; + expect(buildSearchScopeQueries(config)).toEqual([ + "is:pr archived:false is:open involves:@me", + "is:pr archived:false is:open repo:a/b repo:c/d", + ]); + expect(buildSearchScopeQueries({ state: "all", repos: [] })).toEqual([]); +}); diff --git a/src/browser/lib/feed-filters.ts b/src/browser/lib/feed-filters.ts new file mode 100644 index 0000000..eb8507c --- /dev/null +++ b/src/browser/lib/feed-filters.ts @@ -0,0 +1,199 @@ +// Filter mode type +export type FilterMode = + | "review-requested" + | "reviewed" + | "authored" + | "authored-by" + | "involves" + | "all"; + +// Special constant for "All Repos" global filter +export const ALL_REPOS_KEY = "__all_repos__"; + +// Repository with its filter mode +export interface RepoFilter { + name: string; + mode: FilterMode; + authoredBy?: string; // Username for "authored-by" filter mode + enabled?: boolean; // Whether the filter is active (defaults to true) +} + +// Filter configuration stored in localStorage +export interface FilterConfig { + repos: RepoFilter[]; + state: "open" | "closed" | "all"; +} + +// Check if a filter is the special "All Repos" filter +export function isAllReposFilter(filter: RepoFilter): boolean { + return filter.name === ALL_REPOS_KEY; +} + +// ============================================================================ +// Storage Helpers +// ============================================================================ + +const STORAGE_KEY = "pulldash_filter_config"; + +const DEFAULT_CONFIG: FilterConfig = { + // Default to showing review requests across all repos + repos: [{ name: ALL_REPOS_KEY, mode: "review-requested" }], + state: "open", +}; + +export function getFilterConfig(): FilterConfig { + try { + const stored = localStorage.getItem(STORAGE_KEY); + if (stored) { + const parsed = JSON.parse(stored); + // Migration: convert old string[] repos to RepoFilter[] + if ( + parsed.repos && + parsed.repos.length > 0 && + typeof parsed.repos[0] === "string" + ) { + parsed.repos = parsed.repos.map((name: string) => ({ + name, + mode: parsed.mode || "review-requested", + })); + delete parsed.mode; + } + // Migration: if user has empty repos, give them the new default (All Repos) + if (parsed.repos && parsed.repos.length === 0) { + parsed.repos = DEFAULT_CONFIG.repos; + } + return { ...DEFAULT_CONFIG, ...parsed }; + } + } catch { + // ignore + } + return DEFAULT_CONFIG; +} + +export function saveFilterConfig(config: FilterConfig): void { + localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); +} + +// ============================================================================ +// Query Builder +// ============================================================================ + +function getModeFilter(mode: FilterMode, authoredBy?: string): string { + switch (mode) { + case "review-requested": + return "review-requested:@me"; + case "reviewed": + return "reviewed-by:@me"; + case "authored": + return "author:@me"; + case "authored-by": + return authoredBy ? `author:${authoredBy}` : ""; + case "involves": + return "involves:@me"; + default: + return ""; + } +} + +// Build queries grouped by mode (GitHub doesn't support per-repo qualifiers with OR) +// Multiple repo: qualifiers act as OR, but user filters apply to all repos +export function buildSearchQueries(config: FilterConfig): string[] { + // Filter out disabled repos (enabled defaults to true if not specified) + const enabledRepos = config.repos.filter((r) => r.enabled !== false); + + if (enabledRepos.length === 0) { + return []; + } + + const stateFilter = + config.state === "open" + ? "is:open" + : config.state === "closed" + ? "is:closed" + : ""; + + const queries: string[] = []; + + // Separate "All Repos" filters from specific repo filters + const allReposFilters = enabledRepos.filter(isAllReposFilter); + const specificRepos = enabledRepos.filter((r) => !isAllReposFilter(r)); + + // Handle "All Repos" global filters (one query per mode) + for (const filter of allReposFilters) { + const parts = ["is:pr", "archived:false"]; + if (stateFilter) parts.push(stateFilter); + const modeFilter = getModeFilter(filter.mode, filter.authoredBy); + if (modeFilter) parts.push(modeFilter); + // Note: "all" mode on All Repos would be too broad, so we skip it + // Also skip "authored-by" without a username + if ( + filter.mode !== "all" && + !(filter.mode === "authored-by" && !filter.authoredBy) + ) { + queries.push(parts.join(" ")); + } + } + + // Group specific repos by mode+authoredBy (for authored-by, different authors need separate queries) + if (specificRepos.length > 0) { + // Use a composite key: mode + authoredBy for authored-by mode + const byModeKey = new Map< + string, + { mode: FilterMode; authoredBy?: string; repos: string[] } + >(); + for (const repo of specificRepos) { + const key = + repo.mode === "authored-by" + ? `${repo.mode}:${repo.authoredBy || ""}` + : repo.mode; + const existing = byModeKey.get(key); + if (existing) { + existing.repos.push(repo.name); + } else { + byModeKey.set(key, { + mode: repo.mode, + authoredBy: repo.authoredBy, + repos: [repo.name], + }); + } + } + + for (const [, { mode, authoredBy, repos }] of byModeKey) { + // Skip authored-by without a username + if (mode === "authored-by" && !authoredBy) continue; + + const parts = ["is:pr", "archived:false"]; + if (stateFilter) parts.push(stateFilter); + // Multiple repo: qualifiers act as OR + parts.push(...repos.map((r) => `repo:${r}`)); + const modeFilter = getModeFilter(mode, authoredBy); + if (modeFilter) parts.push(modeFilter); + queries.push(parts.join(" ")); + } + } + + return queries; +} + +// Queries for text search: every PR in the feed's specific repos regardless of +// mode (so PRs outside the user's review/authored filters are found), plus the +// "All Repos" filters as-is since they have no repo list to widen to. +export function buildSearchScopeQueries(config: FilterConfig): string[] { + const enabled = config.repos.filter((r) => r.enabled !== false); + const queries = buildSearchQueries({ + ...config, + repos: enabled.filter(isAllReposFilter), + }); + + const names = [ + ...new Set(enabled.filter((r) => !isAllReposFilter(r)).map((r) => r.name)), + ]; + if (names.length > 0) { + const parts = ["is:pr", "archived:false"]; + if (config.state === "open") parts.push("is:open"); + if (config.state === "closed") parts.push("is:closed"); + parts.push(...names.map((n) => `repo:${n}`)); + queries.push(parts.join(" ")); + } + return queries; +} diff --git a/src/browser/lib/fetch-all-pages.test.ts b/src/browser/lib/fetch-all-pages.test.ts new file mode 100644 index 0000000..86d54c4 --- /dev/null +++ b/src/browser/lib/fetch-all-pages.test.ts @@ -0,0 +1,24 @@ +import { expect, test } from "bun:test"; +import { fetchAllPages } from "./fetch-all-pages"; + +test("fetchAllPages returns every item in order across full and partial pages", async () => { + const source = Array.from({ length: 182 }, (_, index) => index); + const requestedPages: number[] = []; + + const items = await fetchAllPages(async (page) => { + requestedPages.push(page); + return source.slice((page - 1) * 100, page * 100); + }); + + expect(items).toEqual(source); + expect(requestedPages).toEqual([1, 2]); + + const exactPageRequests: number[] = []; + const exactPageSource = source.slice(0, 100); + const exactPageItems = await fetchAllPages(async (page) => { + exactPageRequests.push(page); + return exactPageSource.slice((page - 1) * 100, page * 100); + }); + expect(exactPageItems).toEqual(exactPageSource); + expect(exactPageRequests).toEqual([1, 2]); +}); diff --git a/src/browser/lib/fetch-all-pages.ts b/src/browser/lib/fetch-all-pages.ts new file mode 100644 index 0000000..79d548a --- /dev/null +++ b/src/browser/lib/fetch-all-pages.ts @@ -0,0 +1,15 @@ +/** Fetch sequential pages in API order until the final, short page. */ +export async function fetchAllPages( + fetchPage: (page: number) => Promise, + pageSize = 100 +): Promise { + const items: T[] = []; + let page = 1; + + while (true) { + const data = await fetchPage(page); + items.push(...data); + if (data.length < pageSize) return items; + page++; + } +} diff --git a/src/browser/lib/find-highlight.test.ts b/src/browser/lib/find-highlight.test.ts new file mode 100644 index 0000000..c1588ab --- /dev/null +++ b/src/browser/lib/find-highlight.test.ts @@ -0,0 +1,8 @@ +import { expect, test } from "bun:test"; +import { findOccurrences } from "./find-highlight"; + +test("findOccurrences finds every case-insensitive, non-overlapping match", () => { + expect(findOccurrences("Foo foo FOO", "foo")).toEqual([0, 4, 8]); + expect(findOccurrences("aaaa", "aa")).toEqual([0, 2]); + expect(findOccurrences("abc", "")).toEqual([]); +}); diff --git a/src/browser/lib/find-highlight.ts b/src/browser/lib/find-highlight.ts new file mode 100644 index 0000000..c2ba0dd --- /dev/null +++ b/src/browser/lib/find-highlight.ts @@ -0,0 +1,84 @@ +import { useEffect, type RefObject } from "react"; + +const MATCH = "find-match"; +const ACTIVE = "find-active"; + +/** Offsets of every case-insensitive, non-overlapping occurrence of needle. */ +export function findOccurrences(text: string, needle: string): number[] { + if (!needle) return []; + const haystack = text.toLocaleLowerCase(); + const result: number[] = []; + for ( + let at = haystack.indexOf(needle); + at !== -1; + at = haystack.indexOf(needle, at + needle.length) + ) { + result.push(at); + } + return result; +} + +/** + * Highlights occurrences of `query` inside `[data-find-code]` elements under + * the container using the CSS Custom Highlight API (no re-rendering). Matches + * inside a `[data-find-active]` ancestor use the stronger "active" style. + * Re-applies when virtualized rows mount or syntax highlighting lands. + */ +export function useFindHighlights( + containerRef: RefObject, + query: string, + enabled: boolean +) { + useEffect(() => { + const container = containerRef.current; + if (!container || typeof CSS === "undefined" || !CSS.highlights) return; + const needle = query.toLocaleLowerCase(); + if (!enabled || !needle) { + CSS.highlights.delete(MATCH); + CSS.highlights.delete(ACTIVE); + return; + } + + let frame = 0; + const apply = () => { + frame = 0; + const matchRanges: Range[] = []; + const activeRanges: Range[] = []; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT); + for (let node = walker.nextNode(); node; node = walker.nextNode()) { + const parent = node.parentElement; + if (!parent?.closest("[data-find-code]")) continue; + const target = parent.closest("[data-find-active]") + ? activeRanges + : matchRanges; + for (const at of findOccurrences(node.textContent ?? "", needle)) { + const range = new Range(); + range.setStart(node, at); + range.setEnd(node, at + needle.length); + target.push(range); + } + } + CSS.highlights.set(MATCH, new Highlight(...matchRanges)); + CSS.highlights.set(ACTIVE, new Highlight(...activeRanges)); + }; + const schedule = () => { + if (!frame) frame = requestAnimationFrame(apply); + }; + + apply(); + const observer = new MutationObserver(schedule); + observer.observe(container, { + childList: true, + subtree: true, + characterData: true, + attributes: true, + attributeFilter: ["data-find-active"], + }); + return () => { + observer.disconnect(); + if (frame) cancelAnimationFrame(frame); + CSS.highlights.delete(MATCH); + CSS.highlights.delete(ACTIVE); + }; + }, [containerRef, query, enabled]); +} diff --git a/src/browser/lib/folder-paths.test.ts b/src/browser/lib/folder-paths.test.ts new file mode 100644 index 0000000..673fbd2 --- /dev/null +++ b/src/browser/lib/folder-paths.test.ts @@ -0,0 +1,13 @@ +import { test, expect } from "bun:test"; +import { folderPathsUnder } from "./folder-paths"; + +test("folder-paths: includes the folder and all nested folders but not siblings or files", () => { + const files = ["a/b/c/x.ts", "a/b/y.ts", "a/z.ts", "a/bb/w.ts", "other/q.ts"]; + expect(folderPathsUnder("a/b", files).sort()).toEqual(["a/b", "a/b/c"]); + expect(folderPathsUnder("a", files).sort()).toEqual([ + "a", + "a/b", + "a/b/c", + "a/bb", + ]); +}); diff --git a/src/browser/lib/folder-paths.ts b/src/browser/lib/folder-paths.ts new file mode 100644 index 0000000..98b2366 --- /dev/null +++ b/src/browser/lib/folder-paths.ts @@ -0,0 +1,16 @@ +// The folder itself plus every folder nested under it, derived from file paths +export function folderPathsUnder( + folderPath: string, + filenames: string[] +): string[] { + const folders = new Set([folderPath]); + const prefix = `${folderPath}/`; + for (const filename of filenames) { + if (!filename.startsWith(prefix)) continue; + const parts = filename.split("/"); + for (let i = folderPath.split("/").length + 1; i < parts.length; i++) { + folders.add(parts.slice(0, i).join("/")); + } + } + return [...folders]; +} diff --git a/src/browser/lib/hunk-lines.test.ts b/src/browser/lib/hunk-lines.test.ts new file mode 100644 index 0000000..acef313 --- /dev/null +++ b/src/browser/lib/hunk-lines.test.ts @@ -0,0 +1,54 @@ +import { expect, test } from "bun:test"; +import gitDiffParser from "gitdiff-parser"; +import { computeHunkLines } from "./hunk-lines"; + +function hunkLines(patch: string) { + const [file] = gitDiffParser.parse( + `diff --git a/x.cs b/x.cs\n--- a/x.cs\n+++ b/x.cs\n${patch}` + ); + return computeHunkLines(file.hunks[0].changes).map((l) => { + const text = l.content.map((s) => s.value).join(""); + if (l.type === "normal") { + return ` ${l.oldLineNumber},${l.newLineNumber} ${text}`; + } + return `${l.type === "insert" ? "+" : "-"} ${l.lineNumber} ${text}`; + }); +} + +test("re-indented lines render as context, like GitHub's hide-whitespace view", () => { + // `try { ... }` wrapped in `if (ok) { ... }`. Git anchored on the `{` + // line at mismatched indentation, splitting the re-indented body. + const patch = [ + "@@ -1,8 +1,11 @@", + " var x = 1;", + "-try", + "+if (ok)", + " {", + "- run();", + "- {", + "- done();", + "- }", + "+ try", + "+ {", + "+ run();", + "+ {", + "+ done();", + "+ }", + "+ }", + " }", + ].join("\n"); + + expect(hunkLines(patch)).toEqual([ + " 1,1 var x = 1;", + "+ 2 if (ok)", + "+ 3 {", + " 2,4 try", + " 3,5 {", + " 4,6 run();", + " 5,7 {", + " 6,8 done();", + "+ 9 }", + " 7,10 }", + " 8,11 }", + ]); +}); diff --git a/src/browser/lib/hunk-lines.ts b/src/browser/lib/hunk-lines.ts new file mode 100644 index 0000000..f396882 --- /dev/null +++ b/src/browser/lib/hunk-lines.ts @@ -0,0 +1,203 @@ +import type { + Change as _Change, + DeleteChange, + InsertChange, + NormalChange, +} from "gitdiff-parser"; +import { DefaultLinesDiffComputer, type LinesDiff } from "vscode-diff"; + +export interface RawLineSegment { + value: string; + type: "insert" | "delete" | "normal"; +} + +type ReplaceKey = T extends unknown + ? Omit & Record + : never; + +export type Line = ReplaceKey<_Change, "content", RawLineSegment[]>; + +const changeToLine = (change: _Change): Line => ({ + ...change, + content: [{ value: change.content, type: "normal" }], +}); + +// VS Code's diff algorithm (DefaultLinesDiffComputer), used to align the +// delete/insert lines of each change block and compute character-level +// inner changes - the same output VS Code renders in its diff editor. +const linesDiffComputer = new DefaultLinesDiffComputer(); + +/** 0-based line index -> [startColumn, endColumn) pairs (1-based columns). */ +type EmphasisRanges = Map>; + +/** Split a (possibly multi-line) editor Range into per-line column ranges. */ +function collectRangeEmphasis( + map: EmphasisRanges, + range: { + startLineNumber: number; + startColumn: number; + endLineNumber: number; + endColumn: number; + }, + lines: string[] +) { + for (let ln = range.startLineNumber; ln <= range.endLineNumber; ln++) { + const idx = ln - 1; + if (idx < 0 || idx >= lines.length) continue; + const start = ln === range.startLineNumber ? range.startColumn : 1; + const end = + ln === range.endLineNumber ? range.endColumn : lines[idx].length + 1; + if (end <= start) continue; + const existing = map.get(idx); + if (existing) { + existing.push([start, end]); + } else { + map.set(idx, [[start, end]]); + } + } +} + +/** Turn a line plus its emphasis column ranges into render segments. */ +function buildSegments( + content: string, + ranges: Array<[number, number]> | undefined, + emphType: "insert" | "delete" +): RawLineSegment[] { + if (!ranges || ranges.length === 0) { + return [{ value: content, type: "normal" }]; + } + + // Sort and merge overlapping/adjacent ranges + ranges.sort((a, b) => a[0] - b[0]); + const merged: Array<[number, number]> = []; + for (const r of ranges) { + const last = merged[merged.length - 1]; + if (last && r[0] <= last[1]) { + last[1] = Math.max(last[1], r[1]); + } else { + merged.push([r[0], r[1]]); + } + } + + // If the emphasis covers the whole line, skip it - the row background + // already communicates the change (matches VS Code/GitHub). + if ( + content.length > 0 && + merged.length === 1 && + merged[0][0] <= 1 && + merged[0][1] >= content.length + 1 + ) { + return [{ value: content, type: "normal" }]; + } + + const segments: RawLineSegment[] = []; + let pos = 1; + for (const [rawStart, rawEnd] of merged) { + const start = Math.max(pos, Math.min(rawStart, content.length + 1)); + const end = Math.max(start, Math.min(rawEnd, content.length + 1)); + if (start > pos) { + segments.push({ + value: content.slice(pos - 1, start - 1), + type: "normal", + }); + } + if (end > start) { + segments.push({ + value: content.slice(start - 1, end - 1), + type: emphType, + }); + } + pos = end; + } + if (pos <= content.length) { + segments.push({ value: content.slice(pos - 1), type: "normal" }); + } + return segments.filter((seg) => seg.value.length > 0); +} + +/** + * Build a hunk's display lines. The hunk's old side (context + deletes) is + * re-diffed against its new side (context + inserts) with VS Code's diff + * computer, ignoring leading/trailing whitespace. Lines that only changed + * indentation (e.g. code wrapped in a new `if`) become context lines, like + * GitHub's "hide whitespace" view, and the remaining changes get + * character-level emphasis. + */ +export function computeHunkLines(changes: _Change[]): Line[] { + const dels: Array = []; + const adds: Array = []; + for (const c of changes) { + if (c.type !== "insert") dels.push(c); + if (c.type !== "delete") adds.push(c); + } + if (dels.length === adds.length && dels.length === changes.length) { + return changes.map(changeToLine); + } + + const delLines = dels.map((d) => d.content); + const addLines = adds.map((a) => a.content); + + let diff: LinesDiff["changes"]; + try { + diff = linesDiffComputer.computeDiff(delLines, addLines, { + ignoreTrimWhitespace: true, + computeMoves: false, + maxComputationTimeMs: 500, + }).changes; + } catch { + return changes.map(changeToLine); + } + + const oldEmphasis: EmphasisRanges = new Map(); + const newEmphasis: EmphasisRanges = new Map(); + for (const change of diff) { + if (!change.innerChanges) continue; + for (const inner of change.innerChanges) { + collectRangeEmphasis(oldEmphasis, inner.originalRange, delLines); + collectRangeEmphasis(newEmphasis, inner.modifiedRange, addLines); + } + } + + const oldNumber = (c: DeleteChange | NormalChange) => + c.type === "normal" ? c.oldLineNumber : c.lineNumber; + const newNumber = (c: InsertChange | NormalChange) => + c.type === "normal" ? c.newLineNumber : c.lineNumber; + + // 0-based cursors; unchanged runs between changes have equal length + const out: Line[] = []; + let di = 0; + let ai = 0; + const emitUnchanged = (delEnd: number) => { + for (; di < delEnd; di++, ai++) { + out.push({ + type: "normal", + isNormal: true, + oldLineNumber: oldNumber(dels[di]), + newLineNumber: newNumber(adds[ai]), + content: [{ value: addLines[ai], type: "normal" }], + }); + } + }; + + for (const change of diff) { + emitUnchanged(change.original.startLineNumber - 1); + for (; di < change.original.endLineNumberExclusive - 1; di++) { + out.push({ + type: "delete", + isDelete: true, + lineNumber: oldNumber(dels[di]), + content: buildSegments(delLines[di], oldEmphasis.get(di), "delete"), + }); + } + for (; ai < change.modified.endLineNumberExclusive - 1; ai++) { + out.push({ + type: "insert", + isInsert: true, + lineNumber: newNumber(adds[ai]), + content: buildSegments(addLines[ai], newEmphasis.get(ai), "insert"), + }); + } + } + emitUnchanged(dels.length); + return out; +} diff --git a/src/browser/lib/latest-reviews.test.ts b/src/browser/lib/latest-reviews.test.ts new file mode 100644 index 0000000..21dc792 --- /dev/null +++ b/src/browser/lib/latest-reviews.test.ts @@ -0,0 +1,37 @@ +import { test, expect } from "bun:test"; +import type { Review } from "@/browser/contexts/github"; +import { getLatestReviewsByUser } from "./latest-reviews"; + +function review(id: number, login: string, state: string, minute: number) { + return { + id, + state, + user: { login }, + submitted_at: new Date(Date.UTC(2026, 0, 1, 0, minute)).toISOString(), + } as Review; +} + +test("keeps one review per user, preferring the latest decision", () => { + const reviews = [ + review(1, "alice", "CHANGES_REQUESTED", 1), + review(2, "bob", "APPROVED", 2), + review(3, "alice", "APPROVED", 3), + review(4, "alice", "COMMENTED", 4), + review(5, "carol", "COMMENTED", 5), + review(6, "dave", "APPROVED", 6), + review(7, "dave", "DISMISSED", 7), + ]; + + expect(getLatestReviewsByUser(reviews).map((r) => r.id)).toEqual([3, 2, 5]); +}); + +test("omits users with a pending re-request", () => { + const reviews = [ + review(1, "alice", "APPROVED", 1), + review(2, "bob", "APPROVED", 2), + ]; + + expect(getLatestReviewsByUser(reviews, ["alice"]).map((r) => r.id)).toEqual([ + 2, + ]); +}); diff --git a/src/browser/lib/latest-reviews.ts b/src/browser/lib/latest-reviews.ts new file mode 100644 index 0000000..0b7deda --- /dev/null +++ b/src/browser/lib/latest-reviews.ts @@ -0,0 +1,39 @@ +import type { Review } from "@/browser/contexts/github"; + +/** + * One review per user, matching GitHub's sidebar: the user's latest + * APPROVED/CHANGES_REQUESTED review wins; a later dismissal clears it; users + * who only commented are listed with their latest COMMENTED review. + * Users in `pendingLogins` (re-requested) are omitted — they render as pending. + */ +export function getLatestReviewsByUser( + reviews: Review[], + pendingLogins: Iterable = [] +): Review[] { + const pending = new Set(pendingLogins); + const decisive = new Map(); + const commented = new Map(); + const sorted = reviews + .filter((r) => r.submitted_at && r.user && !pending.has(r.user.login)) + .sort( + (a, b) => + new Date(a.submitted_at!).getTime() - + new Date(b.submitted_at!).getTime() + ); + + for (const review of sorted) { + const login = review.user!.login; + if (review.state === "APPROVED" || review.state === "CHANGES_REQUESTED") { + decisive.set(login, review); + } else if (review.state === "DISMISSED") { + decisive.set(login, null); + } else if (review.state === "COMMENTED") { + commented.set(login, review); + } + } + + const logins = new Set([...decisive.keys(), ...commented.keys()]); + return [...logins] + .map((login) => decisive.get(login) ?? commented.get(login)) + .filter((r): r is Review => !!r); +} diff --git a/src/browser/lib/line-width.test.ts b/src/browser/lib/line-width.test.ts new file mode 100644 index 0000000..2978d8f --- /dev/null +++ b/src/browser/lib/line-width.test.ts @@ -0,0 +1,10 @@ +import { expect, test } from "bun:test"; +import { htmlLineWidth } from "./line-width"; + +test("htmlLineWidth counts visible characters, erring wide", () => { + expect(htmlLineWidth('const a = 1;')).toBe(12); + expect(htmlLineWidth("a <= b && c'")).toBe(12); + expect(htmlLineWidth("\tx")).toBe(5); + expect(htmlLineWidth("日本🙂")).toBe(6); + expect(htmlLineWidth("")).toBe(0); +}); diff --git a/src/browser/lib/line-width.ts b/src/browser/lib/line-width.ts new file mode 100644 index 0000000..1670ae9 --- /dev/null +++ b/src/browser/lib/line-width.ts @@ -0,0 +1,16 @@ +/** + * Estimated rendered width of a line of highlighted HTML, in monospace `ch` + * units. Used to size unwrapped (horizontally scrolling) diffs, where + * virtualized rows can't be measured. Errs wide: tabs count as 4 and anything + * past U+1100 (CJK, emoji, …) as 2, since overestimating only adds slack. + */ +export function htmlLineWidth(html: string): number { + const text = html + .replace(/<[^>]*>/g, "") + .replace(/&(?:#\d+|#x[0-9a-f]+|[a-z]+\d*);/gi, "_"); + let width = 0; + for (const char of text) { + width += char === "\t" ? 4 : char.codePointAt(0)! >= 0x1100 ? 2 : 1; + } + return width; +} diff --git a/src/browser/lib/patch-lines.test.ts b/src/browser/lib/patch-lines.test.ts new file mode 100644 index 0000000..4d96182 --- /dev/null +++ b/src/browser/lib/patch-lines.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test"; +import { parsePatchLines } from "./patch-lines"; + +test("parsePatchLines keeps line numbers across hunks and ignores metadata", () => { + expect( + parsePatchLines( + "@@ -2,2 +2,3 @@ heading\n same\n-old\n+new\n+extra\n\\ No newline at end of file\n@@ -10 +11 @@\n tail" + ) + ).toEqual([ + { type: "hunk", content: "@@ -2,2 +2,3 @@ heading" }, + { type: "normal", content: "same", oldLine: 2, newLine: 2 }, + { type: "delete", content: "old", oldLine: 3 }, + { type: "insert", content: "new", newLine: 3 }, + { type: "insert", content: "extra", newLine: 4 }, + { type: "hunk", content: "@@ -10 +11 @@" }, + { type: "normal", content: "tail", oldLine: 10, newLine: 11 }, + ]); +}); diff --git a/src/browser/lib/patch-lines.ts b/src/browser/lib/patch-lines.ts new file mode 100644 index 0000000..2028501 --- /dev/null +++ b/src/browser/lib/patch-lines.ts @@ -0,0 +1,36 @@ +export interface PatchLine { + type: "insert" | "delete" | "normal" | "hunk"; + content: string; + oldLine?: number; + newLine?: number; +} + +/** Turn a GitHub file patch into display rows without fetching file contents. */ +export function parsePatchLines(patch: string): PatchLine[] { + const rows: PatchLine[] = []; + let oldLine = 0; + let newLine = 0; + + for (const line of patch.split("\n")) { + if (line.startsWith("@@")) { + const match = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/.exec(line); + if (!match) continue; + oldLine = Number(match[1]); + newLine = Number(match[2]); + rows.push({ type: "hunk", content: line }); + } else if (line.startsWith("+")) { + rows.push({ type: "insert", content: line.slice(1), newLine: newLine++ }); + } else if (line.startsWith("-")) { + rows.push({ type: "delete", content: line.slice(1), oldLine: oldLine++ }); + } else if (line.startsWith(" ")) { + rows.push({ + type: "normal", + content: line.slice(1), + oldLine: oldLine++, + newLine: newLine++, + }); + } + } + + return rows; +} diff --git a/src/browser/lib/persistent-cache.ts b/src/browser/lib/persistent-cache.ts new file mode 100644 index 0000000..fec03ae --- /dev/null +++ b/src/browser/lib/persistent-cache.ts @@ -0,0 +1,171 @@ +/** + * Durable key/value storage for GitHub API data, backed by IndexedDB. + * + * IndexedDB keeps reads off the startup path (unlike localStorage, which had + * to be parsed in full on load) and holds far more data. Writes are queued and + * flushed in one transaction per tick; reads see queued writes immediately. + */ + +export interface PersistedEntry { + data: T; + timestamp: number; +} + +export interface PersistentStore { + get(key: string): Promise | undefined>; + set(key: string, entry: PersistedEntry): void; + deleteWhere(match: (key: string) => boolean): void; + clear(): void; +} + +/** In-memory store with the same semantics; used when IndexedDB is missing. */ +export class MemoryPersistentStore implements PersistentStore { + private entries = new Map(); + + async get(key: string) { + return this.entries.get(key) as PersistedEntry | undefined; + } + + set(key: string, entry: PersistedEntry) { + this.entries.set(key, entry); + } + + deleteWhere(match: (key: string) => boolean) { + for (const key of this.entries.keys()) { + if (match(key)) this.entries.delete(key); + } + } + + clear() { + this.entries.clear(); + } +} + +const DB_NAME = "pulldash-cache"; +const DB_VERSION = 1; +const STORE_NAME = "entries"; +// Entries nobody has refreshed in this long are dropped on startup. +const MAX_ENTRY_AGE_MS = 14 * 24 * 60 * 60 * 1000; + +function promisify(request: IDBRequest): Promise { + return new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); +} + +class IndexedDBStore implements PersistentStore { + private db: Promise; + private queued = new Map(); + private flushScheduled = false; + + constructor() { + this.db = new Promise((resolve) => { + const request = indexedDB.open(DB_NAME, DB_VERSION); + request.onupgradeneeded = () => { + const store = request.result.createObjectStore(STORE_NAME); + store.createIndex("timestamp", "timestamp"); + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => resolve(null); + request.onblocked = () => resolve(null); + }); + void this.prune(); + } + + async get(key: string): Promise | undefined> { + const queued = this.queued.get(key); + if (queued) return queued as PersistedEntry; + const db = await this.db; + if (!db) return undefined; + try { + const tx = db.transaction(STORE_NAME, "readonly"); + return (await promisify(tx.objectStore(STORE_NAME).get(key))) as + | PersistedEntry + | undefined; + } catch { + return undefined; + } + } + + set(key: string, entry: PersistedEntry) { + this.queued.set(key, entry); + if (this.flushScheduled) return; + this.flushScheduled = true; + setTimeout(() => void this.flush(), 0); + } + + deleteWhere(match: (key: string) => boolean) { + for (const key of this.queued.keys()) { + if (match(key)) this.queued.delete(key); + } + // Transactions on the same store run in creation order, so reads issued + // after this call never observe the deleted entries. + void this.withStore("readwrite", (store) => { + const request = store.openKeyCursor(); + request.onsuccess = () => { + const cursor = request.result; + if (!cursor) return; + if (match(String(cursor.key))) store.delete(cursor.primaryKey); + cursor.continue(); + }; + }); + } + + clear() { + this.queued.clear(); + void this.withStore("readwrite", (store) => store.clear()); + } + + private async flush() { + this.flushScheduled = false; + const entries = [...this.queued]; + if (entries.length === 0) return; + const db = await this.db; + if (!db) return; + try { + const tx = db.transaction(STORE_NAME, "readwrite"); + const store = tx.objectStore(STORE_NAME); + for (const [key, entry] of entries) store.put(entry, key); + } catch { + // Quota or serialization errors: the in-memory cache still works. + } finally { + // Reads issued from here on queue behind this transaction. A newer + // write to the same key replaced the map entry and must stay queued. + for (const [key, entry] of entries) { + if (this.queued.get(key) === entry) this.queued.delete(key); + } + } + } + + private async prune() { + await this.withStore("readwrite", (store) => { + const cutoff = IDBKeyRange.upperBound(Date.now() - MAX_ENTRY_AGE_MS); + const request = store.index("timestamp").openKeyCursor(cutoff); + request.onsuccess = () => { + const cursor = request.result; + if (!cursor) return; + store.delete(cursor.primaryKey); + cursor.continue(); + }; + }); + } + + private async withStore( + mode: IDBTransactionMode, + run: (store: IDBObjectStore) => void + ) { + const db = await this.db; + if (!db) return; + try { + run(db.transaction(STORE_NAME, mode).objectStore(STORE_NAME)); + } catch { + // Ignore storage errors + } + } +} + +export function openPersistentStore(): PersistentStore { + if (typeof indexedDB === "undefined") return new MemoryPersistentStore(); + return new IndexedDBStore(); +} diff --git a/src/browser/lib/pr-fingerprint.test.ts b/src/browser/lib/pr-fingerprint.test.ts new file mode 100644 index 0000000..d6b0160 --- /dev/null +++ b/src/browser/lib/pr-fingerprint.test.ts @@ -0,0 +1,77 @@ +import { expect, test } from "bun:test"; +import type { PullRequest } from "@/api/types"; +import type { CheckRun, CombinedStatus } from "@/browser/contexts/github"; +import { + checksRollupState, + fingerprintMatchesPR, + parsePRFingerprint, + type PRFingerprint, +} from "./pr-fingerprint"; + +const pr = { + updated_at: "2026-09-30T18:52:23Z", + head: { sha: "head" }, + base: { sha: "base" }, + commits: 3, + comments: 1, +} as PullRequest; + +const live: PRFingerprint = { + updatedAt: "2026-09-30T18:52:23Z", + headSha: "head", + baseSha: "base", + commits: 3, + comments: 1, + ciState: null, +}; + +test("fingerprintMatchesPR rejects any moved field", () => { + expect(fingerprintMatchesPR(live, pr)).toBe(true); + expect( + fingerprintMatchesPR({ ...live, updatedAt: "2026-09-30T18:52:24Z" }, pr) + ).toBe(false); + expect(fingerprintMatchesPR({ ...live, headSha: "pushed" }, pr)).toBe(false); + expect(fingerprintMatchesPR({ ...live, comments: 2 }, pr)).toBe(false); +}); + +test("parsePRFingerprint folds EXPECTED and ERROR into the REST states", () => { + const parse = (state: "EXPECTED" | "ERROR" | "SUCCESS") => + parsePRFingerprint({ + repository: { + pullRequest: { + updatedAt: live.updatedAt, + headRefOid: "head", + baseRefOid: "base", + commits: { totalCount: 3 }, + comments: { totalCount: 1 }, + statusCheckRollup: { state }, + }, + }, + })?.ciState; + expect([parse("EXPECTED"), parse("ERROR"), parse("SUCCESS")]).toEqual([ + "PENDING", + "FAILURE", + "SUCCESS", + ]); +}); + +test("checksRollupState ranks failure over pending over success", () => { + const checks = (runs: Partial[], states: string[] = []) => ({ + checkRuns: runs as CheckRun[], + status: { statuses: states.map((state) => ({ state })) } as CombinedStatus, + }); + expect(checksRollupState(checks([]))).toBeNull(); + expect( + checksRollupState(checks([{ status: "completed", conclusion: "skipped" }])) + ).toBe("SUCCESS"); + expect( + checksRollupState( + checks([{ status: "in_progress", conclusion: null }], ["success"]) + ) + ).toBe("PENDING"); + expect( + checksRollupState( + checks([{ status: "in_progress", conclusion: null }], ["error"]) + ) + ).toBe("FAILURE"); +}); diff --git a/src/browser/lib/pr-fingerprint.ts b/src/browser/lib/pr-fingerprint.ts new file mode 100644 index 0000000..57bb45e --- /dev/null +++ b/src/browser/lib/pr-fingerprint.ts @@ -0,0 +1,110 @@ +import type { PullRequest } from "@/api/types"; +import type { CheckRun, CombinedStatus } from "@/browser/contexts/github"; + +export type CIState = "SUCCESS" | "FAILURE" | "PENDING" | null; + +/** Cheap summary of a PR's current state, used to vet cached data. */ +export interface PRFingerprint { + updatedAt: string; + headSha: string; + baseSha: string; + commits: number; + comments: number; + ciState: CIState; +} + +export const PR_FINGERPRINT_QUERY = ` + query ($owner: String!, $repo: String!, $number: Int!) { + repository(owner: $owner, name: $repo) { + pullRequest(number: $number) { + updatedAt + headRefOid + baseRefOid + commits { totalCount } + comments { totalCount } + statusCheckRollup { state } + } + } + } +`; + +export interface PRFingerprintResponse { + repository: { + pullRequest: { + updatedAt: string; + headRefOid: string; + baseRefOid: string; + commits: { totalCount: number }; + comments: { totalCount: number }; + statusCheckRollup: { + state: "EXPECTED" | "ERROR" | "FAILURE" | "PENDING" | "SUCCESS"; + } | null; + } | null; + }; +} + +export function parsePRFingerprint( + data: PRFingerprintResponse +): PRFingerprint | null { + const pr = data.repository.pullRequest; + if (!pr) return null; + const state = pr.statusCheckRollup?.state ?? null; + return { + updatedAt: pr.updatedAt, + headSha: pr.headRefOid, + baseSha: pr.baseRefOid, + commits: pr.commits.totalCount, + comments: pr.comments.totalCount, + ciState: + state === "EXPECTED" ? "PENDING" : state === "ERROR" ? "FAILURE" : state, + }; +} + +/** + * Whether a cached PR still describes the live one. Same head and base + * commits mean the files and patches are identical; GitHub bumps updatedAt + * on most other activity (comments, reviews, edits, labels). + */ +export function fingerprintMatchesPR( + live: PRFingerprint, + pr: PullRequest +): boolean { + return ( + Date.parse(live.updatedAt) === Date.parse(pr.updated_at) && + live.headSha === pr.head.sha && + live.baseSha === pr.base.sha && + live.commits === pr.commits && + live.comments === pr.comments + ); +} + +const FAILING_CONCLUSIONS = new Set([ + "failure", + "cancelled", + "timed_out", + "action_required", + "startup_failure", +]); + +/** Collapse REST check runs and statuses the way GitHub's rollup does. */ +export function checksRollupState(checks: { + checkRuns: CheckRun[]; + status: CombinedStatus; +}): CIState { + const { checkRuns } = checks; + const statuses = checks.status.statuses ?? []; + if (checkRuns.length === 0 && statuses.length === 0) return null; + if ( + checkRuns.some((run) => FAILING_CONCLUSIONS.has(run.conclusion ?? "")) || + statuses.some((s) => s.state === "failure" || s.state === "error") + ) { + return "FAILURE"; + } + if ( + checkRuns.some((run) => run.status !== "completed") || + statuses.some((s) => s.state === "pending") + ) { + return "PENDING"; + } + return "SUCCESS"; +} diff --git a/src/browser/lib/pr-query.test.ts b/src/browser/lib/pr-query.test.ts new file mode 100644 index 0000000..4da6c0d --- /dev/null +++ b/src/browser/lib/pr-query.test.ts @@ -0,0 +1,62 @@ +import { test, expect } from "bun:test"; +import { + buildTextSearchQueries, + extractRepoFromUrl, + mergeSearchResults, + parsePRQuery, +} from "./pr-query"; +import type { PRSearchResult } from "../contexts/github"; + +test("pr-query: parses URLs and shorthand, otherwise treats input as text", () => { + const expected = { + kind: "pr" as const, + owner: "acme", + repo: "web", + number: 12, + }; + expect(parsePRQuery("https://github.com/acme/web/pull/12/files")).toEqual( + expected + ); + expect(parsePRQuery(" acme/web#12 ")).toEqual(expected); + expect(parsePRQuery("acme/web/pull/12")).toEqual(expected); + expect(parsePRQuery("login bug")).toEqual({ + kind: "text", + text: "login bug", + }); + expect(parsePRQuery("#12")).toEqual({ kind: "text", text: "#12" }); +}); + +test("pr-query: text is appended to every feed query, never searched alone", () => { + const feed = ["is:pr repo:a/b", "is:pr review-requested:@me"]; + expect(buildTextSearchQueries(feed, " fix ")).toEqual([ + "is:pr repo:a/b in:title,body fix", + "is:pr review-requested:@me in:title,body fix", + ]); + expect(buildTextSearchQueries(["is:pr repo:a/b"], "feat/login-fix")).toEqual([ + "is:pr repo:a/b in:title,body feat/login-fix", + "is:pr repo:a/b head:feat/login-fix", + ]); + expect(buildTextSearchQueries(feed, " ")).toEqual([]); + expect(buildTextSearchQueries([], "fix")).toEqual([]); +}); + +test("pr-query: merge dedupes, sorts newest first and limits", () => { + const pr = (id: number, updated_at: string) => + ({ id, updated_at }) as PRSearchResult; + const merged = mergeSearchResults( + [ + [pr(1, "2024-01-01"), pr(2, "2024-03-01")], + [pr(2, "2024-03-01"), pr(3, "2024-02-01")], + ], + 2 + ); + expect(merged.map((p) => p.id)).toEqual([2, 3]); +}); + +test("pr-query: extracts repo from API url", () => { + expect(extractRepoFromUrl("https://api.github.com/repos/a/b")).toEqual({ + owner: "a", + repo: "b", + }); + expect(extractRepoFromUrl("nope")).toBeNull(); +}); diff --git a/src/browser/lib/pr-query.ts b/src/browser/lib/pr-query.ts new file mode 100644 index 0000000..1928c8f --- /dev/null +++ b/src/browser/lib/pr-query.ts @@ -0,0 +1,69 @@ +import type { PRSearchResult } from "../contexts/github"; + +export type ParsedPRQuery = + | { kind: "pr"; owner: string; repo: string; number: number } + | { kind: "text"; text: string }; + +const URL_RE = /github\.com\/([^/\s]+)\/([^/\s]+)\/pull\/(\d+)/; +const SHORTHAND_RE = /^([\w.-]+)\/([\w.-]+)(?:#|\/pull\/)(\d+)$/; + +// Distinguishes a pasted PR reference (URL or owner/repo#N) from free text +export function parsePRQuery(input: string): ParsedPRQuery { + const text = input.trim(); + const match = text.match(URL_RE) ?? text.match(SHORTHAND_RE); + if (match) { + return { + kind: "pr", + owner: match[1], + repo: match[2], + number: parseInt(match[3], 10), + }; + } + return { kind: "text", text }; +} + +export function extractRepoFromUrl( + url: string +): { owner: string; repo: string } | null { + const match = url.match(/repos\/([^/]+)\/([^/]+)/); + if (match) { + return { owner: match[1], repo: match[2] }; + } + return null; +} + +// A single token with a separator (feature/foo, fix-login) may be a pasted +// branch name; plain words are almost always text. +const BRANCH_RE = /^[^\s]*[/_.-][^\s]*$/; + +// Scope free text to the feed: append it to every feed query so results can +// only come from repos/modes the feed already covers. Branch-like text also +// gets an exact head-branch query per feed query. +export function buildTextSearchQueries( + feedQueries: string[], + text: string +): string[] { + const trimmed = text.trim(); + if (!trimmed) return []; + const branch = BRANCH_RE.test(trimmed); + return feedQueries.flatMap((q) => [ + `${q} in:title,body ${trimmed}`, + ...(branch ? [`${q} head:${trimmed}`] : []), + ]); +} + +// Merge results from several queries, dedupe, newest first +export function mergeSearchResults( + results: PRSearchResult[][], + limit: number +): PRSearchResult[] { + const seen = new Set(); + const merged: PRSearchResult[] = []; + for (const item of results.flat()) { + if (seen.has(item.id)) continue; + seen.add(item.id); + merged.push(item); + } + merged.sort((a, b) => b.updated_at.localeCompare(a.updated_at)); + return merged.slice(0, limit); +} diff --git a/src/browser/lib/pr-stacks.test.ts b/src/browser/lib/pr-stacks.test.ts new file mode 100644 index 0000000..13c3567 --- /dev/null +++ b/src/browser/lib/pr-stacks.test.ts @@ -0,0 +1,178 @@ +import { test, expect } from "bun:test"; +import { + discoverStacks, + orderStacked, + prKey, + stackRuns, + stackSlot, + toStackNode, + type StackNode, + type StackProbe, + type StackSlot, +} from "./pr-stacks"; + +function node( + number: number, + baseRefName: string, + headRefName: string, + extra: Partial = {} +): StackNode { + return { + owner: "o", + repo: "r", + number, + title: `PR ${number}`, + state: "open", + baseRefName, + headRefName, + isCrossRepository: false, + nativeStack: null, + ...extra, + }; +} + +// Answers probes from a fixed set of open PRs, like the GraphQL lookups by +// headRefName (up) and baseRefName (down). +function probeFrom(all: StackNode[], calls: number[][] = []): StackProbe { + return async (frontier) => { + calls.push(frontier.map((n) => n.number)); + return frontier.flatMap((f) => + all.filter( + (n) => + n !== f && + (n.headRefName === f.baseRefName || + (!f.isCrossRepository && n.baseRefName === f.headRefName)) + ) + ); + }; +} + +const members = ( + result: Awaited>, + number: number +) => result.placements.get(prKey("o", "r", number))?.members; + +test("pr-stacks: walks a branch chain both ways from a middle PR", async () => { + const all = [ + node(1, "main", "a"), + node(2, "a", "b"), + node(3, "b", "c"), + node(9, "main", "unrelated"), + ]; + const calls: number[][] = []; + const result = await discoverStacks([all[1], all[3]], probeFrom(all, calls)); + + expect(members(result, 2)).toEqual(["o/r#1", "o/r#2", "o/r#3"]); + expect(members(result, 3)).toBe(members(result, 1)); + expect(result.placements.has(prKey("o", "r", 9))).toBe(false); + // Seeds, then their neighbours; already-seen PRs are not re-probed + expect(calls).toEqual([ + [2, 9], + [1, 3], + ]); +}); + +test("pr-stacks: native stacks are taken as-is and never probed", async () => { + const bottom = node(4, "main", "x"); + const top = node(5, "x", "y"); + const stack = { number: 7, members: [bottom, top] }; + const calls: number[][] = []; + const result = await discoverStacks( + [{ ...top, nativeStack: stack }], + probeFrom([], calls) + ); + + expect(members(result, 4)).toEqual(["o/r#4", "o/r#5"]); + expect(result.placements.get(prKey("o", "r", 4))?.id).toBe("o/r:stack:7"); + expect(calls).toEqual([]); +}); + +test("pr-stacks: ignores closed PRs and fork heads that share a branch name", async () => { + const fork = node(1, "main", "feature", { isCrossRepository: true }); + const child = node(2, "feature", "next"); + const merged = node(3, "main", "old", { state: "merged" }); + const result = await discoverStacks( + [fork, child, merged, node(4, "old", "new")], + probeFrom([]) + ); + expect(result.placements.size).toBe(0); +}); + +test("pr-stacks: branching stacks list each branch after its parent", async () => { + const all = [ + node(1, "main", "a"), + node(3, "a", "c"), + node(2, "a", "b"), + node(4, "b", "d"), + ]; + const result = await discoverStacks([all[0]], probeFrom(all)); + expect(members(result, 1)).toEqual(["o/r#1", "o/r#2", "o/r#4", "o/r#3"]); +}); + +type Row = { n: number; stack?: StackSlot }; +const slot = (id: string, position: number, size: number): StackSlot => ({ + id, + position, + size, +}); + +test("pr-stacks: orderStacked groups stacks at their first listed member", () => { + const items: Row[] = [ + { n: 10 }, + { n: 3, stack: slot("s", 3, 3) }, + { n: 11 }, + { n: 1, stack: slot("s", 1, 3) }, + ]; + const extra: Row[] = [{ n: 2, stack: slot("s", 2, 3) }]; + const ordered = orderStacked(items, extra); + expect(ordered.map((r) => r.n)).toEqual([10, 1, 2, 3, 11]); + + expect(stackRuns(ordered).map((run) => run.items.map((r) => r.n))).toEqual([ + [10], + [1, 2, 3], + [11], + ]); +}); + +test("pr-stacks: a lone stack member renders as a plain row", () => { + const items: Row[] = [{ n: 1, stack: slot("s", 2, 3) }]; + expect(orderStacked(items)).toEqual(items); + expect(stackRuns(items)).toEqual([{ stack: null, items: [items[0]] }]); +}); + +test("pr-stacks: stackSlot reports 1-based position from the base", () => { + expect(stackSlot({ id: "s", members: ["a", "b"] }, "b")).toEqual( + slot("s", 2, 2) + ); + expect(stackSlot({ id: "s", members: ["a"] }, "z")).toBeUndefined(); +}); + +test("pr-stacks: toStackNode orders native stack entries by position", () => { + const pr = (number: number, state: "OPEN" | "MERGED" = "OPEN") => ({ + number, + title: `PR ${number}`, + state, + isDraft: false, + baseRefName: "main", + headRefName: `b${number}`, + isCrossRepository: false, + repository: { name: "r", owner: { login: "o" } }, + }); + const result = toStackNode({ + ...pr(2), + stack: { + number: 5, + entries: { + nodes: [ + { position: 2, pullRequest: pr(2) }, + { position: 1, pullRequest: pr(1, "MERGED") }, + ], + }, + }, + }); + expect(result.nativeStack?.number).toBe(5); + expect(result.nativeStack?.members.map((m) => [m.number, m.state])).toEqual([ + [1, "merged"], + [2, "open"], + ]); +}); diff --git a/src/browser/lib/pr-stacks.ts b/src/browser/lib/pr-stacks.ts new file mode 100644 index 0000000..d675997 --- /dev/null +++ b/src/browser/lib/pr-stacks.ts @@ -0,0 +1,313 @@ +// Stacked PR detection. GitHub-native stacks come straight from the +// PullRequest.stack field; anything else (Graphite, spr, hand-chained +// branches) is inferred from open PRs whose base branch is another open PR's +// head branch in the same repo. + +export type StackPRState = "open" | "draft" | "merged" | "closed"; + +export interface StackNode { + owner: string; + repo: string; + number: number; + title: string; + state: StackPRState; + baseRefName: string; + headRefName: string; + isCrossRepository: boolean; + /** GitHub-native stack, members ordered from the base branch up. */ + nativeStack: { number: number; members: StackNode[] } | null; +} + +export interface StackPlacement { + id: string; + /** Member PR keys ordered from the base branch up. */ + members: string[]; +} + +/** One PR's slot in a stack, as stored on list rows. */ +export interface StackSlot { + id: string; + /** 1 is closest to the base branch. */ + position: number; + size: number; +} + +export interface StackDiscovery { + nodes: Map; + /** Keyed by member PR key; members of one stack share a placement. */ + placements: Map; +} + +/** Looks up open PRs stacked directly above or below each frontier PR. */ +export type StackProbe = (frontier: StackNode[]) => Promise; + +/** More open PRs than this on one branch makes it an integration branch. */ +export const MAX_STACK_CHILDREN = 4; +const MAX_ROUNDS = 8; +const MAX_NODES = 200; + +export function prKey(owner: string, repo: string, number: number): string { + return `${owner}/${repo}#${number}`.toLowerCase(); +} + +function nodeKey(node: StackNode): string { + return prKey(node.owner, node.repo, node.number); +} + +function repoKey(node: StackNode): string { + return `${node.owner}/${node.repo}`.toLowerCase(); +} + +const isOpen = (node: StackNode) => + node.state === "open" || node.state === "draft"; + +export async function discoverStacks( + seeds: StackNode[], + probe: StackProbe +): Promise { + const nodes = new Map(); + const placements = new Map(); + + // Returns true when the node still needs heuristic exploration + const add = (node: StackNode): boolean => { + const key = nodeKey(node); + if (nodes.has(key) || placements.has(key)) return false; + nodes.set(key, node); + if (node.nativeStack) { + const members = [...node.nativeStack.members]; + const placement: StackPlacement = { + id: `${repoKey(node)}:stack:${node.nativeStack.number}`, + members: members.map(nodeKey), + }; + for (const member of members) { + const memberKey = nodeKey(member); + if (!nodes.has(memberKey)) nodes.set(memberKey, member); + placements.set(memberKey, placement); + } + return false; + } + return isOpen(node); + }; + + let frontier = seeds.filter(add); + for (let round = 0; round < MAX_ROUNDS && frontier.length > 0; round++) { + const found = await probe(frontier); + frontier = nodes.size < MAX_NODES ? found.filter(add) : []; + } + + // Chain the remaining open PRs: a PR whose base is another same-repo PR's + // head sits on top of it. + const chained = [...nodes.values()].filter( + (n) => isOpen(n) && !placements.has(nodeKey(n)) + ); + const byHead = new Map(); + for (const node of chained) { + if (node.isCrossRepository) continue; + byHead.set(`${repoKey(node)}:${node.headRefName}`, node); + } + const children = new Map(); + for (const node of chained) { + const parent = byHead.get(`${repoKey(node)}:${node.baseRefName}`); + if (!parent || parent === node) continue; + const list = children.get(parent) ?? []; + list.push(node); + children.set(parent, list); + } + for (const [parent, kids] of children) { + if (kids.length > MAX_STACK_CHILDREN) children.delete(parent); + } + const parentOf = new Map(); + for (const [parent, kids] of children) { + for (const kid of kids) parentOf.set(kid, parent); + } + + const visited = new Set(); + const walk = (node: StackNode, out: StackNode[]) => { + if (visited.has(node)) return; + visited.add(node); + out.push(node); + const kids = children.get(node) ?? []; + kids.sort((a, b) => a.number - b.number); + for (const kid of kids) walk(kid, out); + }; + for (const node of chained) { + if (parentOf.has(node) || visited.has(node)) continue; + const members: StackNode[] = []; + walk(node, members); + if (members.length < 2) continue; + const placement: StackPlacement = { + id: `${repoKey(node)}:chain:${node.headRefName}`, + members: members.map(nodeKey), + }; + for (const member of members) placements.set(nodeKey(member), placement); + } + + return { nodes, placements }; +} + +export interface PRStack { + /** 1 is closest to the base branch. */ + position: number; + members: StackNode[]; +} + +export function stackFromDiscovery( + { nodes, placements }: StackDiscovery, + key: string +): PRStack | null { + const placement = placements.get(key); + if (!placement) return null; + return { + position: placement.members.indexOf(key) + 1, + members: placement.members.flatMap((k) => nodes.get(k) ?? []), + }; +} + +export function stackSlot( + placement: StackPlacement, + key: string +): StackSlot | undefined { + const index = placement.members.indexOf(key); + if (index < 0) return undefined; + return { + id: placement.id, + position: index + 1, + size: placement.members.length, + }; +} + +/** + * Keep the list's order, but pull every stack together at the slot of its + * first-listed member, ordered from the base branch up. `extra` holds stack + * members that only appear because a stack-mate is listed. + */ +export function orderStacked( + items: T[], + extra: T[] = [] +): T[] { + const byStack = new Map(); + for (const item of [...items, ...extra]) { + if (!item.stack) continue; + const group = byStack.get(item.stack.id) ?? []; + group.push(item); + byStack.set(item.stack.id, group); + } + + const ordered: T[] = []; + const emitted = new Set(); + for (const item of items) { + const group = item.stack && byStack.get(item.stack.id); + if (!item.stack || !group || group.length < 2) { + ordered.push(item); + continue; + } + if (emitted.has(item.stack.id)) continue; + emitted.add(item.stack.id); + ordered.push( + ...group.sort((a, b) => a.stack!.position - b.stack!.position) + ); + } + return ordered; +} + +export type StackRun = + | { stack: null; items: [T] } + | { stack: StackSlot; items: T[] }; + +/** Split an ordered list into single rows and runs of one stack. */ +export function stackRuns( + items: T[] +): StackRun[] { + const runs: StackRun[] = []; + for (const item of items) { + const last = runs[runs.length - 1]; + if (item.stack && last?.stack?.id === item.stack.id) { + last.items.push(item); + } else if (item.stack) { + runs.push({ stack: item.stack, items: [item] }); + } else { + runs.push({ stack: null, items: [item] }); + } + } + // A lone stack member (stack-mates filtered out elsewhere) renders plainly + return runs.map((run) => + run.stack && run.items.length < 2 + ? { stack: null, items: [run.items[0]] } + : run + ); +} + +// --------------------------------------------------------------------------- +// GraphQL +// --------------------------------------------------------------------------- + +const STACK_PR_FIELDS = ` + number + title + state + isDraft + baseRefName + headRefName + isCrossRepository + repository { name owner { login } }`; + +/** Selection on a PullRequest that `toStackNode` parses. */ +export const STACK_NODE_FIELDS = ` + ${STACK_PR_FIELDS} + stack { + number + entries(first: 50) { + nodes { position pullRequest { ${STACK_PR_FIELDS} } } + } + }`; + +interface GqlStackPR { + number: number; + title: string; + state: "OPEN" | "CLOSED" | "MERGED"; + isDraft: boolean; + baseRefName: string; + headRefName: string; + isCrossRepository: boolean; + repository: { name: string; owner: { login: string } }; +} + +export interface GqlStackNode extends GqlStackPR { + stack: { + number: number; + entries: { + nodes: Array<{ position: number; pullRequest: GqlStackPR | null }>; + }; + } | null; +} + +function toPlainNode(pr: GqlStackPR): StackNode { + return { + owner: pr.repository.owner.login, + repo: pr.repository.name, + number: pr.number, + title: pr.title, + state: + pr.state === "MERGED" + ? "merged" + : pr.state === "CLOSED" + ? "closed" + : pr.isDraft + ? "draft" + : "open", + baseRefName: pr.baseRefName, + headRefName: pr.headRefName, + isCrossRepository: pr.isCrossRepository, + nativeStack: null, + }; +} + +export function toStackNode(pr: GqlStackNode): StackNode { + const node = toPlainNode(pr); + const entries = pr.stack?.entries.nodes ?? []; + if (!pr.stack || entries.length < 2) return node; + const members = [...entries] + .sort((a, b) => a.position - b.position) + .flatMap((e) => (e.pullRequest ? [toPlainNode(e.pullRequest)] : [])); + return { ...node, nativeStack: { number: pr.stack.number, members } }; +} diff --git a/src/browser/lib/rebase-range.test.ts b/src/browser/lib/rebase-range.test.ts new file mode 100644 index 0000000..cf07bac --- /dev/null +++ b/src/browser/lib/rebase-range.test.ts @@ -0,0 +1,260 @@ +import { test, expect } from "bun:test"; +import type { PullRequestFile } from "@/api/types"; +import { + COMPARE_FILE_LIMIT, + merge3, + rangeFromTrees, + rebaseRangeFiles, + rebasedPatch, + treeChanges, + type RebaseRangeInput, +} from "./rebase-range"; + +const lines = (...l: string[]) => l.map((x) => `${x}\n`).join(""); + +test("merge3 combines separate changes and keeps ours on conflicts", () => { + const base = lines("a", "b", "c", "d", "e"); + // Separate and touching changes merge cleanly + expect( + merge3(base, lines("A", "b", "c", "d", "e"), lines("a", "B", "c", "d", "E")) + ).toBe(lines("A", "B", "c", "d", "E")); + // Same lines changed differently: ours wins + expect( + merge3( + base, + lines("a", "ours", "c", "d", "e"), + lines("a", "theirs", "c", "d", "e") + ) + ).toBe(lines("a", "ours", "c", "d", "e")); + // Insertions at the same point collide; identical changes don't + expect( + merge3( + base, + lines("a", "x", "b", "c", "d", "e"), + lines("a", "y", "b", "c", "d", "e") + ) + ).toBe(lines("a", "x", "b", "c", "d", "e")); + expect( + merge3(base, lines("a", "c", "d", "e"), lines("a", "c", "d", "e")) + ).toBe(lines("a", "c", "d", "e")); + // Missing trailing newline survives + expect(merge3("a\nb", "A\nb", "a\nB")).toBe("A\nB"); +}); + +test("merge3 keeps both sides of a conflict when head did", () => { + const base = lines("a", "z"); + const ours = lines("a", "x", "z"); + const theirs = lines("a", "y", "z"); + // Head kept both (main's first) and then added a line + expect(merge3(base, ours, theirs, lines("a", "y", "x", "w", "z"))).toBe( + lines("a", "y", "x", "z") + ); + // Head dropped the reviewed line: the drop shows, main's line doesn't + const dropped = rebasedPatch(base, ours, theirs, lines("a", "y", "z")); + expect(dropped).toMatchObject({ identical: false }); + if (dropped.identical) return; + expect(dropped.patch).toContain("-x"); + expect(dropped.patch).not.toContain("+y"); +}); + +test("rebasedPatch reports identical results and skips binary files", () => { + expect(rebasedPatch("a\n", "a\n", "b\n", "b\n")).toEqual({ identical: true }); + expect(rebasedPatch("\0", "x", "y", "z")).toMatchObject({ patch: undefined }); +}); + +const file = ( + filename: string, + extra: Partial = {} +): PullRequestFile => + ({ + filename, + status: "modified", + sha: `sha-${filename}`, + additions: 1, + deletions: 1, + changes: 2, + patch: `@@ original ${filename}`, + ...extra, + }) as PullRequestFile; + +function scenario() { + const contents: Record = { + // PR edited line 1 before review, main edited line 5, then the PR + // edited line 3 after merging main. + "old:both.ts": lines("1", "2", "3", "4", "5"), + "start:both.ts": lines("one", "2", "3", "4", "5"), + "new:both.ts": lines("1", "2", "3", "4", "five"), + "end:both.ts": lines("one", "2", "three", "4", "five"), + // No PR work since review; only main's change arrived. + "old:absorbed.ts": lines("a", "b"), + "start:absorbed.ts": lines("A", "b"), + "new:absorbed.ts": lines("a", "B"), + "end:absorbed.ts": lines("A", "B"), + // Identical at start and end: the merge threw away main's change. + "old:reverted.ts": lines("x", "y"), + "start:reverted.ts": lines("X", "y"), + "new:reverted.ts": lines("x", "Y"), + "end:reverted.ts": lines("X", "y"), + }; + const fetched: string[] = []; + const input: RebaseRangeInput = { + rangeFiles: [ + file("main-only.ts"), + file("pr-only.ts"), + file("both.ts"), + file("absorbed.ts"), + ], + prStartFiles: [file("both.ts"), file("absorbed.ts"), file("reverted.ts")], + prEndFiles: [ + file("pr-only.ts"), + file("both.ts"), + file("absorbed.ts"), + file("reverted.ts", { sha: "head-reverted" }), + ], + baseFiles: [ + file("main-only.ts"), + file("both.ts"), + file("absorbed.ts"), + file("reverted.ts"), + ], + startSha: "start", + endSha: "end", + oldMergeBase: "old", + newMergeBase: "new", + getContent: async (path, ref) => { + fetched.push(`${ref}:${path}`); + return contents[`${ref}:${path}`] ?? ""; + }, + rebase: async (...args) => rebasedPatch(...args), + }; + return { input, fetched }; +} + +test("rebaseRangeFiles leaves out changes merged from the base branch", async () => { + const { input, fetched } = scenario(); + const result = await rebaseRangeFiles(input); + + expect(result.map((f) => f.filename)).toEqual([ + "pr-only.ts", + "both.ts", + "reverted.ts", + ]); + const [prOnly, both, reverted] = result; + // Untouched by main: GitHub's patch is used as is, without fetching + expect(prOnly.patch).toBe("@@ original pr-only.ts"); + expect(prOnly.rebased_base).toBeUndefined(); + expect(fetched.some((f) => /main-only|pr-only/.test(f))).toBe(false); + + expect(both.patch).toContain("-3\n+three"); + expect(both.patch).not.toContain("+five"); + expect(both).toMatchObject({ additions: 1, deletions: 1 }); + expect(both.rebased_base).toEqual({ + key: "start+new", + content: lines("one", "2", "3", "4", "five"), + }); + + expect(reverted.patch).toContain("-Y\n+y"); + expect(reverted.sha).toBe("head-reverted"); +}); + +test("rebaseRangeFiles compares content when file lists are truncated", async () => { + const filler = Array.from({ length: COMPARE_FILE_LIMIT }, (_, i) => + file(`filler-${i}`) + ); + + // PR lists truncated: range files can't be ruled out without content + // (main-only.ts is empty everywhere here, so it rebases away). + const truncatedPR = scenario(); + const result = await rebaseRangeFiles({ + ...truncatedPR.input, + rangeFiles: [file("main-only.ts"), file("both.ts")], + prStartFiles: [...truncatedPR.input.prStartFiles, ...filler], + }); + expect(result.map((f) => f.filename)).toEqual(["both.ts"]); + expect(truncatedPR.fetched).toContain("start:main-only.ts"); + + // Range and base lists truncated: PR files cut from the range still show, + // and unrelated range files are dropped without fetching. + const truncatedRange = scenario(); + const fromPR = await rebaseRangeFiles({ + ...truncatedRange.input, + rangeFiles: filler, + baseFiles: [...truncatedRange.input.baseFiles, ...filler], + }); + expect(fromPR.map((f) => f.filename)).toEqual(["both.ts", "reverted.ts"]); + expect(fromPR[0].patch).toContain("-3\n+three"); + expect(truncatedRange.fetched.some((f) => f.includes("filler"))).toBe(false); +}); + +test("rangeFromTrees diffs tree listings without the compare file limit", async () => { + const tree = (entries: Record) => + new Map(Object.entries(entries)); + // Blob oids; contents below. "keep.ts" is reused from the range compare. + const oldBase = tree({ "main.ts": "m1", "both.ts": "b1", "moved.ts": "v" }); + const start = tree({ + "main.ts": "m1", + "both.ts": "b2", + "keep.ts": "k1", + "img.png": "p1", + "moved.ts": "v", + }); + const newBase = tree({ "main.ts": "m2", "both.ts": "b3", "moved.ts": "v" }); + const end = tree({ + "main.ts": "m2", + "both.ts": "b4", + "keep.ts": "k2", + "img.png": "p2", + "new/moved.ts": "v", + }); + const changes = treeChanges(oldBase, start, newBase, end); + expect(changes.map((c) => c.path).sort()).toEqual([ + "both.ts", + "img.png", + "keep.ts", + "moved.ts", + "new/moved.ts", + ]); + + const contents: Record = { + b1: lines("1", "2", "3"), + b2: lines("one", "2", "3"), + b3: lines("1", "2", "three"), + b4: lines("one", "two", "three"), + p1: null, + p2: null, + }; + const requested: string[] = []; + const result = await rangeFromTrees({ + changes, + rangeFiles: [file("keep.ts", { sha: "k2", patch: "@@ keep" })], + startSha: "start", + endSha: "end", + oldMergeBase: "old", + newMergeBase: "new", + getBlobs: async (requests) => { + requested.push(...requests.map((r) => r.oid)); + return new Map( + requests.map((r) => [r.oid, r.oid in contents ? contents[r.oid] : ""]) + ); + }, + rebase: async (...args) => rebasedPatch(...args), + }); + const byName = new Map(result.map((f) => [f.filename, f])); + + expect([...byName.keys()].sort()).toEqual([ + "both.ts", + "img.png", + "keep.ts", + "new/moved.ts", + ]); + expect(byName.get("keep.ts")?.patch).toBe("@@ keep"); + expect(requested).not.toContain("k1"); + expect(byName.get("both.ts")?.patch).toContain("-2\n+two"); + expect(byName.get("both.ts")?.patch).not.toContain("+three"); + expect(byName.get("img.png")).toMatchObject({ status: "modified" }); + expect(byName.get("img.png")?.patch).toBeUndefined(); + expect(byName.get("new/moved.ts")).toMatchObject({ + status: "renamed", + previous_filename: "moved.ts", + }); +}); diff --git a/src/browser/lib/rebase-range.ts b/src/browser/lib/rebase-range.ts new file mode 100644 index 0000000..875c36b --- /dev/null +++ b/src/browser/lib/rebase-range.ts @@ -0,0 +1,605 @@ +/** + * Keep base-branch merges out of "changes since commit X" diffs. + * + * A plain compare of X...head includes everything a merge of the base branch + * brought in. Instead, each file's content at X gets the base branch's changes + * replayed onto it (a 3-way merge, as if X were rebased onto the new merge + * base), and that is diffed against head. What remains is only work done on + * the PR, including how merge conflicts were resolved. + */ +import type { PullRequestFile } from "@/api/types"; +import { diffArrays } from "diff"; +import { generatePatch } from "./recover-patches"; + +/** + * A file in a narrowed range. `rebased_base` is set when the old side of the + * patch is the start commit with base-branch changes replayed, so it exists in + * no commit; `key` identifies that content for caching parsed diffs. + */ +export type RangeFile = PullRequestFile & { + rebased_base?: { key: string; content: string }; +}; + +/** GitHub's compare endpoint lists at most this many files. */ +export const COMPARE_FILE_LIMIT = 300; + +function splitLines(content: string): string[] { + return content.match(/[^\n]*\n|[^\n]+$/g) ?? []; +} + +interface Change { + start: number; + end: number; + lines: string[]; + side: "ours" | "theirs"; +} + +/** Changed base-line ranges between `base` and `other`, with their new lines. */ +function changes( + base: string[], + other: string[], + side: Change["side"] +): Change[] { + const parts = diffArrays(base, other, { timeout: 5000 }); + if (!parts) throw new Error("Merging the file exceeded the work limit"); + const result: Change[] = []; + let pos = 0; + let current: Change | null = null; + for (const part of parts) { + if (!part.added && !part.removed) { + if (current) result.push(current); + current = null; + pos += part.count; + continue; + } + current ??= { start: pos, end: pos, lines: [], side }; + if (part.removed) { + pos += part.count; + current.end = pos; + } else { + current.lines.push(...part.value); + } + } + if (current) result.push(current); + return result; +} + +/** Apply one side's changes in a group to base lines [start, end). */ +function applySide( + base: string[], + group: Change[], + side: Change["side"], + start: number, + end: number +): string[] { + const out: string[] = []; + let pos = start; + for (const change of group) { + if (change.side !== side) continue; + out.push(...base.slice(pos, change.start), ...change.lines); + pos = change.end; + } + out.push(...base.slice(pos, end)); + return out; +} + +/** Most conflicts per file worth resolving against head; beyond, ours wins. */ +const MAX_RESOLVED_CONFLICTS = 10; + +/** Lines that differ between two versions, or Infinity past the work limit. */ +function diffSize(a: string[], b: string[]): number { + const parts = diffArrays(a, b, { timeout: 1000 }); + if (!parts) return Infinity; + return parts.reduce((n, p) => (p.added || p.removed ? n + p.count : n), 0); +} + +/** + * 3-way merge of line-based text. Where both sides changed the same lines, + * `ours` wins, so a conflict resolution shows up as a change against what was + * reviewed. Given the merged result (`head`), a conflict may instead keep + * both sides (in either order) when that is closer to how head resolved it, + * since neither side's lines are new work there. Theirs never replaces ours + * outright: if the resolution dropped reviewed lines, that should show. + */ +export function merge3( + base: string, + ours: string, + theirs: string, + head?: string +): string { + if (ours === base) return theirs; + if (theirs === base || theirs === ours) return ours; + const baseLines = splitLines(base); + const all = [ + ...changes(baseLines, splitLines(ours), "ours"), + ...changes(baseLines, splitLines(theirs), "theirs"), + ].sort((a, b) => a.start - b.start || a.end - b.end); + + // Merged text as fixed runs, with conflicts left as alternatives. + const segments: (string[] | { options: string[][] })[] = []; + let pos = 0; + let i = 0; + while (i < all.length) { + const group = [all[i]]; + const start = all[i].start; + let end = all[i].end; + // Changes overlap when their base ranges intersect; two insertions at the + // same point also collide. Changes that only touch are merged cleanly. + while (++i < all.length) { + const next = all[i]; + const insertionAtEnd = group.some( + (c) => c.start === end && c.end === end + ); + if ( + next.start < end || + (next.start === end && next.end === end && insertionAtEnd) + ) { + group.push(next); + end = Math.max(end, next.end); + } else break; + } + segments.push(baseLines.slice(pos, start)); + if (group.every((c) => c.side === "theirs")) { + segments.push(applySide(baseLines, group, "theirs", start, end)); + } else if (group.every((c) => c.side === "ours")) { + segments.push(applySide(baseLines, group, "ours", start, end)); + } else { + const mine = applySide(baseLines, group, "ours", start, end); + const other = applySide(baseLines, group, "theirs", start, end); + segments.push({ + options: [mine, [...mine, ...other], [...other, ...mine]], + }); + } + pos = end; + } + segments.push(baseLines.slice(pos)); + + const conflicts = segments.filter((s) => !Array.isArray(s)).length; + const choice = new Map(); + const assemble = () => + segments.flatMap((s, index) => + Array.isArray(s) ? s : s.options[choice.get(index) ?? 0] + ); + if (head !== undefined && conflicts <= MAX_RESOLVED_CONFLICTS) { + const headLines = splitLines(head); + // Greedy: settle each conflict in turn against the choices so far. + segments.forEach((segment, index) => { + if (Array.isArray(segment)) return; + let best = 0; + let bestSize = Infinity; + segment.options.forEach((_, option) => { + choice.set(index, option); + const size = diffSize(assemble(), headLines); + if (size < bestSize) { + best = option; + bestSize = size; + } + }); + choice.set(index, best); + }); + } + return assemble().join(""); +} + +/** + * Result of diffing a rebased start file against head. `patch` is undefined + * for binary files. + */ +export type RebaseResult = + | { identical: true } + | { identical: false; rebased: string; patch: string | undefined }; + +/** + * Patch from the start content (with base-branch changes from `baseOld` to + * `baseNew` replayed) to `head`. + */ +export function rebasedPatch( + baseOld: string, + ours: string, + baseNew: string, + head: string +): RebaseResult { + if ([baseOld, ours, baseNew, head].some((c) => c.includes("\0"))) { + return { identical: false, rebased: ours, patch: undefined }; + } + const rebased = merge3(baseOld, ours, baseNew, head); + if (rebased === head) return { identical: true }; + return { identical: false, patch: generatePatch(rebased, head), rebased }; +} + +function countChanges(patch: string) { + let additions = 0; + let deletions = 0; + for (const line of patch.split("\n")) { + if (line.startsWith("+")) additions++; + else if (line.startsWith("-")) deletions++; + } + return { additions, deletions, changes: additions + deletions }; +} + +function paths(files: PullRequestFile[]): Set { + const set = new Set(); + for (const file of files) { + set.add(file.filename); + if (file.previous_filename) set.add(file.previous_filename); + } + return set; +} + +export interface RebaseRangeInput { + /** Compare of start...end. */ + rangeFiles: PullRequestFile[]; + /** PR changes at the start commit (oldMergeBase...start). */ + prStartFiles: PullRequestFile[]; + /** PR changes at the end commit (newMergeBase...end). */ + prEndFiles: PullRequestFile[]; + /** Base-branch changes merged in (oldMergeBase...newMergeBase). */ + baseFiles: PullRequestFile[]; + startSha: string; + endSha: string; + oldMergeBase: string; + newMergeBase: string; + /** File content at a ref; "" when the file does not exist there. */ + getContent: (path: string, ref: string) => Promise; + rebase: ( + baseOld: string, + ours: string, + baseNew: string, + head: string + ) => Promise; +} + +/** + * Range files with base-branch changes removed. Files only the base branch + * changed are dropped; files both sides changed get a patch against the + * rebased start content. Also surfaces files where the merge discarded or + * altered base-branch changes (identical at start and end, so absent from the + * range compare), and PR files cut from a truncated range compare. + */ +export async function rebaseRangeFiles( + input: RebaseRangeInput +): Promise { + const { + rangeFiles, + prStartFiles, + prEndFiles, + baseFiles, + startSha, + endSha, + oldMergeBase, + newMergeBase, + getContent, + rebase, + } = input; + // Truncated lists can't prove a file is untouched, so fall back to content. + const prComplete = + prStartFiles.length < COMPARE_FILE_LIMIT && + prEndFiles.length < COMPARE_FILE_LIMIT; + const rangeComplete = rangeFiles.length < COMPARE_FILE_LIMIT; + const baseComplete = baseFiles.length < COMPARE_FILE_LIMIT; + const prPaths = paths([...prStartFiles, ...prEndFiles]); + const basePaths = paths(baseFiles); + const rangePaths = paths(rangeFiles); + // Where the PR renamed a file, its path on the base branch. + const renamedFrom = (files: PullRequestFile[]) => + new Map( + files.flatMap((f) => + f.previous_filename ? [[f.filename, f.previous_filename] as const] : [] + ) + ); + const startRenames = renamedFrom(prStartFiles); + const endRenames = renamedFrom(prEndFiles); + const key = `${startSha}+${newMergeBase}`; + const touches = (set: Set, ...candidates: (string | undefined)[]) => + candidates.some((p) => p !== undefined && set.has(p)); + + interface Job { + file: PullRequestFile; + startPath: string; + endPath: string; + startMissing: boolean; + endMissing: boolean; + } + + const result: (RangeFile | null)[] = []; + const jobs: { index: number; job: Job }[] = []; + const addJob = (job: Job, fallback: RangeFile | null) => { + jobs.push({ index: result.length, job }); + result.push(fallback); + }; + + for (const file of rangeFiles) { + if (prComplete && !touches(prPaths, file.filename, file.previous_filename)) + continue; + if ( + baseComplete && + !touches(basePaths, file.filename, file.previous_filename) + ) { + result.push(file); + continue; + } + addJob( + { + file, + startPath: file.previous_filename ?? file.filename, + endPath: file.filename, + startMissing: file.status === "added", + endMissing: file.status === "removed", + }, + file + ); + } + + if (prComplete) { + const seen = new Set(); + for (const file of [...prEndFiles, ...prStartFiles]) { + const path = file.filename; + if (seen.has(path) || rangePaths.has(path)) continue; + seen.add(path); + // A complete range compare proves the file is the same at start and + // end, so only a base-branch change can make the merge differ. + if ( + rangeComplete && + baseComplete && + !touches(basePaths, path, startRenames.get(path), endRenames.get(path)) + ) + continue; + addJob( + { + file: { ...file, previous_filename: undefined, patch: undefined }, + startPath: path, + endPath: path, + startMissing: false, + endMissing: false, + }, + null + ); + } + } + + let next = 0; + await Promise.all( + Array.from({ length: Math.min(4, jobs.length) }, async () => { + while (next < jobs.length) { + const { index, job } = jobs[next++]; + try { + const oldBasePath = startRenames.get(job.startPath) ?? job.startPath; + const newBasePath = endRenames.get(job.endPath) ?? job.endPath; + const [baseOld, baseNew, start, end] = await Promise.all([ + getContent(oldBasePath, oldMergeBase), + getContent(newBasePath, newMergeBase), + job.startMissing ? "" : getContent(job.startPath, startSha), + job.endMissing ? "" : getContent(job.endPath, endSha), + ]); + const outcome = await rebase(baseOld, start, baseNew, end); + if (outcome.identical) { + result[index] = null; + } else if (outcome.patch !== undefined) { + const { rebased } = outcome; + result[index] = { + ...job.file, + ...countChanges(outcome.patch), + status: + job.file.status === "renamed" + ? "renamed" + : rebased === "" + ? "added" + : end === "" + ? "removed" + : "modified", + patch: outcome.patch, + rebased_base: { key, content: rebased }, + }; + } + } catch (error) { + console.error( + `Could not rebase diff for ${job.file.filename}`, + error + ); + } + } + }) + ); + + return result.filter((f): f is RangeFile => f !== null); +} + +/** Blob oid by path for every file in a commit. */ +export type Tree = Map; + +/** A path's blob oids at the four commits; undefined where it is absent. */ +export interface TreeChange { + path: string; + oldBase?: string; + start?: string; + newBase?: string; + end?: string; +} + +/** + * Paths that may hold PR changes between start and end. Unlike the compare + * endpoint this has no file limit. A path is skipped when it is the same at + * both ends with no base change, or when the PR leaves it untouched at both + * ends (any difference is the base branch's). + */ +export function treeChanges( + oldBase: Tree, + start: Tree, + newBase: Tree, + end: Tree +): TreeChange[] { + const result: TreeChange[] = []; + const all = new Set([ + ...oldBase.keys(), + ...start.keys(), + ...newBase.keys(), + ...end.keys(), + ]); + for (const path of all) { + const change = { + path, + oldBase: oldBase.get(path), + start: start.get(path), + newBase: newBase.get(path), + end: end.get(path), + }; + if (change.start === change.end && change.oldBase === change.newBase) + continue; + if (change.start === change.oldBase && change.end === change.newBase) + continue; + result.push(change); + } + return result; +} + +export interface BlobRequest { + oid: string; + /** Where the blob lives, for fetching it by path when needed. */ + path: string; + ref: string; +} + +export interface TreeRangeInput { + changes: TreeChange[]; + /** Compare of start...end, possibly truncated; its patches are reused. */ + rangeFiles: PullRequestFile[]; + startSha: string; + endSha: string; + oldMergeBase: string; + newMergeBase: string; + /** Blob text by oid; null for binary blobs. */ + getBlobs: (requests: BlobRequest[]) => Promise>; + rebase: TreeRangeRebase; +} + +type TreeRangeRebase = ( + baseOld: string, + ours: string, + baseNew: string, + head: string +) => Promise; + +function treeFile( + path: string, + sha: string, + status: PullRequestFile["status"], + extra: Partial = {} +): RangeFile { + return { + sha, + filename: path, + status, + additions: 0, + deletions: 0, + changes: 0, + blob_url: "", + raw_url: "", + contents_url: "", + ...extra, + }; +} + +/** + * Range files built from tree listings, for ranges too large for the compare + * endpoint's file limit. Same rules as rebaseRangeFiles; paths the base branch + * didn't touch are plain start-to-end diffs. Renames are only detected for + * files moved without changes. + */ +export async function rangeFromTrees( + input: TreeRangeInput +): Promise { + const { changes, startSha, endSha, oldMergeBase, newMergeBase } = input; + const key = `${startSha}+${newMergeBase}`; + const rangeByPath = new Map( + input.rangeFiles + .filter((f) => !f.previous_filename && f.patch) + .map((f) => [f.filename, f]) + ); + const plain = (c: TreeChange) => c.oldBase === c.newBase; + const reusable = (c: TreeChange) => { + const file = rangeByPath.get(c.path); + if (!file || !plain(c)) return null; + return (c.end ?? c.start) === file.sha ? file : null; + }; + + // Exact moves pair up as renames, as GitHub would show them. + const moves = new Map(); + const added = new Map(); + for (const c of changes) { + if (plain(c) && c.end && !c.start) added.set(c.end, c); + } + for (const c of changes) { + const target = c.start && !c.end && plain(c) && added.get(c.start); + if (target && !moves.has(target.path)) { + moves.set(target.path, c); + moves.set(c.path, c); + } + } + + const requests: BlobRequest[] = []; + const pending = changes.filter((c) => !moves.has(c.path) && !reusable(c)); + for (const c of pending) { + if (c.start) requests.push({ oid: c.start, path: c.path, ref: startSha }); + if (c.end) requests.push({ oid: c.end, path: c.path, ref: endSha }); + if (plain(c)) continue; + if (c.oldBase) + requests.push({ oid: c.oldBase, path: c.path, ref: oldMergeBase }); + if (c.newBase) + requests.push({ oid: c.newBase, path: c.path, ref: newMergeBase }); + } + const blobs = await input.getBlobs(requests); + const text = (oid: string | undefined) => + oid === undefined ? "" : blobs.get(oid); + + const result: RangeFile[] = []; + for (const c of changes) { + const reused = reusable(c); + if (reused) result.push(reused); + const from = moves.get(c.path); + if (from && from.path !== c.path) { + result.push( + treeFile(c.path, c.end!, "renamed", { previous_filename: from.path }) + ); + } + } + + await Promise.all( + pending.map(async (c) => { + const start = text(c.start); + const end = text(c.end); + const baseOld = plain(c) ? start : text(c.oldBase); + const baseNew = plain(c) ? start : text(c.newBase); + const sha = c.end ?? c.start ?? ""; + const status = (oldMissing: boolean): PullRequestFile["status"] => + oldMissing ? "added" : c.end ? "modified" : "removed"; + if ([start, end, baseOld, baseNew].some((t) => t == null)) { + // Binary (or unavailable): shown without a patch when the PR side changed + if (c.start !== c.end) + result.push(treeFile(c.path, sha, status(!c.start))); + return; + } + try { + const outcome = await input.rebase(baseOld!, start!, baseNew!, end!); + if (outcome.identical) return; + if (outcome.patch === undefined) { + if (c.start !== c.end) + result.push(treeFile(c.path, sha, status(!c.start))); + return; + } + const oldMissing = !c.start && (plain(c) || !c.newBase); + result.push( + treeFile(c.path, sha, status(oldMissing), { + ...countChanges(outcome.patch), + patch: outcome.patch, + ...(plain(c) + ? {} + : { rebased_base: { key, content: outcome.rebased } }), + }) + ); + } catch (error) { + console.error(`Could not rebase diff for ${c.path}`, error); + } + }) + ); + return result; +} diff --git a/src/browser/lib/recover-patches.test.ts b/src/browser/lib/recover-patches.test.ts new file mode 100644 index 0000000..e5f013b --- /dev/null +++ b/src/browser/lib/recover-patches.test.ts @@ -0,0 +1,97 @@ +import { expect, test } from "bun:test"; +import type { PullRequestFile } from "@/api/types"; +import { + carryOverRecoveredPatches, + generatePatch, + recoverPatches, +} from "./recover-patches"; +import { parsePatchLines } from "./patch-lines"; + +function file(overrides: Partial = {}): PullRequestFile { + return { + sha: "blob", + filename: "file.cs", + status: "modified", + additions: 1, + deletions: 1, + changes: 2, + blob_url: "", + raw_url: "", + contents_url: "", + ...overrides, + }; +} + +test("generates a complete large text patch and leaves binary and unchanged files unavailable", () => { + const before = Array.from({ length: 2000 }, (_, i) => `old ${i}`).join("\n"); + const after = Array.from({ length: 2000 }, (_, i) => `new ${i}`).join("\n"); + const lines = parsePatchLines(generatePatch(before, after)!); + expect(lines.filter((line) => line.type === "delete")).toHaveLength(2000); + expect(lines.filter((line) => line.type === "insert")).toHaveLength(2000); + expect(generatePatch("binary\0old", "binary\0new")).toBeUndefined(); + expect(generatePatch("same", "same")).toBeUndefined(); +}); + +test("recovers missing patches at exact refs, including renames, additions and deletions", async () => { + const files = [ + file({ patch: "existing" }), + file({ + filename: "renamed.cs", + previous_filename: "old.cs", + status: "renamed", + }), + file({ filename: "added.cs", status: "added" }), + file({ filename: "deleted.cs", status: "removed" }), + file({ filename: "failed.cs" }), + file({ filename: "mode.cs", changes: 0 }), + ]; + const requests: string[] = []; + const result = await recoverPatches( + files, + "range-start", + "range-end", + async (path, ref) => { + requests.push(`${ref}:${path}`); + if (path === "failed.cs") throw new Error("unavailable"); + return ref === "range-start" ? "old\n" : "new\n"; + }, + async (oldContent, newContent) => generatePatch(oldContent, newContent) + ); + expect(requests.sort()).toEqual( + [ + "range-start:old.cs", + "range-end:renamed.cs", + "range-end:added.cs", + "range-start:deleted.cs", + "range-start:failed.cs", + "range-end:failed.cs", + ].sort() + ); + expect(result[0]).toBe(files[0]); + expect( + result.slice(1, 4).every((entry) => entry.patch?.startsWith("@@ ")) + ).toBe(true); + expect(result[4]).toBe(files[4]); + expect(result[5]).toBe(files[5]); + expect(files[1].patch).toBeUndefined(); +}); + +test("carryOverRecoveredPatches reuses patches only for identical changes", () => { + const previous = [ + file({ filename: "a.cs", patch: "@@ -1 +1 @@\n-a\n+b" }), + file({ filename: "b.cs", patch: "@@ -1 +1 @@\n-c\n+d" }), + ]; + const result = carryOverRecoveredPatches( + [ + file({ filename: "a.cs" }), + file({ filename: "b.cs", sha: "new-blob" }), + file({ filename: "c.cs", patch: "@@ -1 +1 @@\n-e\n+f" }), + ], + previous + ); + expect(result.map((f) => f.patch)).toEqual([ + "@@ -1 +1 @@\n-a\n+b", + undefined, + "@@ -1 +1 @@\n-e\n+f", + ]); +}); diff --git a/src/browser/lib/recover-patches.ts b/src/browser/lib/recover-patches.ts new file mode 100644 index 0000000..5120932 --- /dev/null +++ b/src/browser/lib/recover-patches.ts @@ -0,0 +1,90 @@ +import type { PullRequestFile } from "@/api/types"; +import { createTwoFilesPatch } from "diff"; + +/** Generate only hunk text, matching GitHub's patch field. Runs in a worker. */ +export function generatePatch( + oldContent: string, + newContent: string +): string | undefined { + if (oldContent.includes("\0") || newContent.includes("\0")) return undefined; + const patch = createTwoFilesPatch( + "file", + "file", + oldContent, + newContent, + undefined, + undefined, + { timeout: 5000 } + ); + if (!patch) + throw new Error("Generating the file diff exceeded the work limit"); + const start = patch.indexOf("@@ "); + return start === -1 ? undefined : patch.slice(start); +} + +/** Recover omitted text patches with bounded concurrency; preserve supplied patches. */ +export async function recoverPatches( + files: PullRequestFile[], + baseRef: string, + headRef: string, + getContent: (path: string, ref: string) => Promise, + makePatch: ( + oldContent: string, + newContent: string + ) => Promise +): Promise { + const result = [...files]; + let next = 0; + await Promise.all( + Array.from({ length: Math.min(4, files.length) }, async () => { + while (next < files.length) { + const index = next++; + const file = files[index]; + if (file.patch || file.changes === 0) continue; + try { + const [oldContent, newContent] = await Promise.all([ + file.status === "added" + ? "" + : getContent(file.previous_filename ?? file.filename, baseRef), + file.status === "removed" ? "" : getContent(file.filename, headRef), + ]); + const patch = await makePatch(oldContent, newContent); + if (patch) result[index] = { ...file, patch }; + } catch (error) { + console.error(`Could not recover diff for ${file.filename}`, error); + } + } + }) + ); + return result; +} + +const recoveryKey = (file: PullRequestFile) => + [ + file.filename, + file.previous_filename, + file.sha, + file.additions, + file.deletions, + ].join("\0"); + +/** + * GitHub keeps omitting the same large patches on every fetch. Reuse patches + * rebuilt for an earlier copy of the list when the file change is identical. + */ +export function carryOverRecoveredPatches( + files: PullRequestFile[], + previous: PullRequestFile[] +): PullRequestFile[] { + if (!files.some((file) => !file.patch && file.changes > 0)) return files; + const patches = new Map( + previous.flatMap((file) => + file.patch ? [[recoveryKey(file), file.patch] as const] : [] + ) + ); + return files.map((file) => { + if (file.patch || file.changes === 0) return file; + const patch = patches.get(recoveryKey(file)); + return patch ? { ...file, patch } : file; + }); +} diff --git a/src/browser/lib/request-cache.test.ts b/src/browser/lib/request-cache.test.ts new file mode 100644 index 0000000..c674db0 --- /dev/null +++ b/src/browser/lib/request-cache.test.ts @@ -0,0 +1,42 @@ +import { expect, test } from "bun:test"; +import { MemoryPersistentStore } from "./persistent-cache"; +import { RequestCache, cacheKeyMatches } from "./request-cache"; + +test("cacheKeyMatches covers nested keys but not sibling PR numbers", () => { + expect(cacheKeyMatches("pr:o/r/1", "pr:o/r/1")).toBe(true); + expect(cacheKeyMatches("pr:o/r/1:files", "pr:o/r/1")).toBe(true); + expect(cacheKeyMatches("workflow-runs:o/r/abc", "workflow-runs:o/r")).toBe( + true + ); + expect(cacheKeyMatches("pr:o/r/12", "pr:o/r/1")).toBe(false); + expect(cacheKeyMatches("pr:o/r/10:files", "pr:o/r/1")).toBe(false); +}); + +test("peek returns durable entries with their original age", async () => { + const persistent = new MemoryPersistentStore(); + persistent.set("pr:o/r/1", { data: { title: "old" }, timestamp: 1 }); + const cache = new RequestCache(persistent); + + expect(cache.get("pr:o/r/1")).toBeNull(); + expect(await cache.peek("pr:o/r/1")).toEqual({ + data: { title: "old" }, + timestamp: 1, + }); + // Promoted to memory, but still too old to skip revalidation + expect(cache.getStale("pr:o/r/1")).toEqual({ + data: { title: "old" }, + isStale: true, + }); +}); + +test("invalidate removes durable copies for the pattern only", async () => { + const persistent = new MemoryPersistentStore(); + const cache = new RequestCache(persistent); + cache.set("pr:o/r/1:comments", ["a"], true); + cache.set("pr:o/r/12:comments", ["b"], true); + + cache.invalidate("pr:o/r/1"); + + expect(await persistent.get("pr:o/r/1:comments")).toBeUndefined(); + expect(await cache.peek("pr:o/r/12:comments")).not.toBeNull(); +}); diff --git a/src/browser/lib/request-cache.ts b/src/browser/lib/request-cache.ts new file mode 100644 index 0000000..f2b1ee8 --- /dev/null +++ b/src/browser/lib/request-cache.ts @@ -0,0 +1,122 @@ +import type { PersistentStore } from "./persistent-cache"; + +interface CacheEntry { + data: T; + timestamp: number; +} + +export const DEFAULT_CACHE_TTL = 30_000; // 30 seconds + +/** + * Whether `key` belongs to `pattern`: the key itself or anything nested + * under it. `pr:o/r/1` covers `pr:o/r/1:files` but not `pr:o/r/12`. + */ +export function cacheKeyMatches(key: string, pattern: string): boolean { + return ( + key === pattern || + key.startsWith(`${pattern}:`) || + key.startsWith(`${pattern}/`) + ); +} + +/** + * Two-tier cache: synchronous in-memory entries for hot reads, plus an + * optional durable copy so data survives reloads (stale-while-revalidate). + */ +export class RequestCache { + private cache = new Map>(); + private pending = new Map>(); + + constructor(private persistent: PersistentStore) {} + + /** + * Get cached data. Returns null if no cache or cache is stale. + */ + get(key: string, ttl = DEFAULT_CACHE_TTL): T | null { + const entry = this.cache.get(key); + if (!entry) return null; + if (Date.now() - entry.timestamp > ttl) { + // The durable copy (if any) stays around for peek(). + this.cache.delete(key); + return null; + } + return entry.data as T; + } + + /** + * Get in-memory data even if stale (for SWR pattern). + * Returns { data, isStale } or null if no cache exists. + */ + getStale( + key: string, + freshTtl = DEFAULT_CACHE_TTL + ): { data: T; isStale: boolean } | null { + const entry = this.cache.get(key); + if (!entry) return null; + const isStale = Date.now() - entry.timestamp > freshTtl; + return { data: entry.data as T, isStale }; + } + + /** + * Last known value regardless of age, from memory or durable storage. + * Durable hits are promoted to memory with their original timestamp, so + * freshness checks still treat them as old. + */ + async peek(key: string): Promise | null> { + const memory = this.cache.get(key); + if (memory) return memory as CacheEntry; + const stored = await this.persistent.get(key); + if (!stored) return null; + // A fresher in-memory value may have landed while we were reading. + const current = this.cache.get(key); + if (current) return current as CacheEntry; + this.cache.set(key, stored); + return stored; + } + + set(key: string, data: T, persist = false): void { + const entry = { data, timestamp: Date.now() }; + this.cache.set(key, entry); + if (persist) this.persistent.set(key, entry); + } + + getPending(key: string): Promise | null { + return (this.pending.get(key) as Promise | undefined) ?? null; + } + + setPending(key: string, promise: Promise): void { + this.pending.set(key, promise); + const clear = () => { + if (this.pending.get(key) === promise) this.pending.delete(key); + }; + promise.then(clear, clear); + } + + clearPending(key: string): void { + this.pending.delete(key); + } + + /** + * Drop entries for `pattern` (see cacheKeyMatches) or, with a function, + * every key it accepts. No argument clears everything. + */ + invalidate(pattern?: string | ((key: string) => boolean)): void { + if (pattern === undefined) { + this.cache.clear(); + this.pending.clear(); + this.persistent.clear(); + return; + } + const match = + typeof pattern === "string" + ? (key: string) => cacheKeyMatches(key, pattern) + : pattern; + for (const key of this.cache.keys()) { + if (match(key)) this.cache.delete(key); + } + for (const key of this.pending.keys()) { + if (match(key)) this.pending.delete(key); + } + this.persistent.deleteWhere(match); + } +} diff --git a/src/browser/lib/review-range.test.ts b/src/browser/lib/review-range.test.ts new file mode 100644 index 0000000..80eb84e --- /dev/null +++ b/src/browser/lib/review-range.test.ts @@ -0,0 +1,98 @@ +import { test, expect } from "bun:test"; +import { + commitsAfter, + commitsInRange, + findLastReviewedSha, + isRangeAvailable, + rangeCacheKey, + rangeToSelection, + selectionToRange, +} from "./review-range"; + +const commits = [{ sha: "a" }, { sha: "b" }, { sha: "c" }, { sha: "d" }]; + +test("findLastReviewedSha picks the viewer's latest submitted review", () => { + const reviews = [ + { + user: { login: "me" }, + state: "COMMENTED", + commit_id: "a", + submitted_at: "2026-01-01T00:00:00Z", + }, + { + user: { login: "other" }, + state: "APPROVED", + commit_id: "c", + submitted_at: "2026-01-03T00:00:00Z", + }, + { + user: { login: "me" }, + state: "CHANGES_REQUESTED", + commit_id: "b", + submitted_at: "2026-01-02T00:00:00Z", + }, + { + user: { login: "me" }, + state: "PENDING", + commit_id: "d", + submitted_at: null, + }, + ]; + expect(findLastReviewedSha(reviews, "me")).toBe("b"); + expect(findLastReviewedSha(reviews, "nobody")).toBeNull(); + expect(findLastReviewedSha(reviews, null)).toBeNull(); +}); + +test("commitsAfter returns following commits or null when start is gone", () => { + expect(commitsAfter(commits, "b")?.map((c) => c.sha)).toEqual(["c", "d"]); + expect(commitsAfter(commits, "d")).toEqual([]); + expect(commitsAfter(commits, "zzz")).toBeNull(); +}); + +test("isRangeAvailable requires a known start with newer commits", () => { + expect(isRangeAvailable(commits, "b", "d")).toBe(true); + expect(isRangeAvailable(commits, "b", "c")).toBe(true); + expect(isRangeAvailable(commits, "d", "d")).toBe(false); + expect(isRangeAvailable(commits, "c", "b")).toBe(false); + expect(isRangeAvailable(commits, "zzz", "d")).toBe(false); + expect(isRangeAvailable(commits, "b", "zzz")).toBe(false); + expect(commitsInRange(commits, "b", "c")?.map((c) => c.sha)).toEqual(["c"]); +}); + +test("selectionToRange and rangeToSelection round-trip commit selections", () => { + const withParent = [ + { sha: "a", parents: [{ sha: "base" }] }, + ...commits.slice(1), + ]; + expect(selectionToRange(withParent, 0, 3)).toBeNull(); + expect(selectionToRange(withParent, 0, 0)).toEqual({ + startSha: "base", + endSha: "a", + }); + expect(selectionToRange(withParent, 2, 3)).toEqual({ startSha: "b" }); + expect(selectionToRange(withParent, 1, 2)).toEqual({ + startSha: "a", + endSha: "c", + }); + expect(selectionToRange(commits, 0, 1)).toBeUndefined(); + + expect( + rangeToSelection(withParent, { startSha: "base", endSha: "a" }) + ).toEqual([0, 0]); + expect(rangeToSelection(withParent, { startSha: "b" })).toEqual([2, 3]); + expect(rangeToSelection(withParent, { startSha: "zzz" })).toBeNull(); + expect(commitsInRange(withParent, "base", "b")?.map((c) => c.sha)).toEqual([ + "a", + "b", + ]); +}); + +test("rangeCacheKey separates full and ranged diffs of the same blob", () => { + expect(rangeCacheKey(null, "blob")).toBe("blob"); + expect(rangeCacheKey({ startSha: "b", source: "manual" }, "blob")).toBe( + "b:head:blob" + ); + expect( + rangeCacheKey({ startSha: "b", endSha: "c", source: "manual" }, "blob") + ).toBe("b:c:blob"); +}); diff --git a/src/browser/lib/review-range.ts b/src/browser/lib/review-range.ts new file mode 100644 index 0000000..0c9de56 --- /dev/null +++ b/src/browser/lib/review-range.ts @@ -0,0 +1,136 @@ +/** + * Pure helpers for "changes since your last review" - narrowing a PR diff to + * the commits added after a chosen start commit. + */ + +interface ReviewLike { + user?: { login: string } | null; + state?: string; + commit_id?: string | null; + submitted_at?: string | null; +} + +interface CommitLike { + sha: string; + parents?: { sha: string }[]; +} + +/** A start commit for a narrowed diff, plus how it was chosen. */ +export interface DiffRange { + startSha: string; + /** Last commit included in the diff. Omitted for ranges through the PR head. */ + endSha?: string; + /** "review" = the viewer's last submitted review; "manual" = commit picker */ + source: "review" | "manual"; +} + +/** + * The commit the viewer's most recent submitted review was made against. + * Pending (unsubmitted) reviews are ignored; GitHub reports those as PENDING. + */ +export function findLastReviewedSha( + reviews: ReviewLike[], + login: string | null +): string | null { + if (!login) return null; + let best: ReviewLike | null = null; + for (const review of reviews) { + if (review.user?.login !== login) continue; + if (review.state === "PENDING") continue; + if (!review.commit_id) continue; + if ( + !best || + (review.submitted_at ?? "").localeCompare(best.submitted_at ?? "") > 0 + ) { + best = review; + } + } + return best?.commit_id ?? null; +} + +/** + * Commits in `commits` (oldest first, as GitHub returns them) that come after + * `startSha`. Returns null when `startSha` is not in the list, which means a + * force-push rewrote history and the range is no longer meaningful. + */ +export function commitsAfter( + commits: T[], + startSha: string +): T[] | null { + const idx = commits.findIndex((c) => c.sha === startSha); + if (idx === -1) return null; + return commits.slice(idx + 1); +} + +/** + * Index of the commit a range starts after. The first commit's parent maps to + * -1 so ranges can include the first commit. Null when the start is unknown. + */ +function startIndex(commits: CommitLike[], startSha: string): number | null { + const idx = commits.findIndex((c) => c.sha === startSha); + if (idx !== -1) return idx; + return commits[0]?.parents?.[0]?.sha === startSha ? -1 : null; +} + +/** Commits after the start, through the end (inclusive), in PR order. */ +export function commitsInRange( + commits: T[], + startSha: string, + endSha: string +): T[] | null { + const start = startIndex(commits, startSha); + const end = commits.findIndex((c) => c.sha === endSha); + if (start === null || end <= start) return null; + return commits.slice(start + 1, end + 1); +} + +/** + * Inclusive commit indexes covered by a range, for highlighting the picker. + * Null when the range no longer matches the commit list. + */ +export function rangeToSelection( + commits: CommitLike[], + range: Pick +): [number, number] | null { + const start = startIndex(commits, range.startSha); + const end = range.endSha + ? commits.findIndex((c) => c.sha === range.endSha) + : commits.length - 1; + if (start === null || end <= start) return null; + return [start + 1, end]; +} + +/** + * The diff range showing commits `from` through `to` (inclusive indexes). + * Returns null for the whole PR, and undefined when the first commit's parent + * is unknown so the selection can't be expressed as a compare. + */ +export function selectionToRange( + commits: CommitLike[], + from: number, + to: number +): Pick | null | undefined { + const last = commits.length - 1; + if (from <= 0 && to >= last) return null; + const startSha = + from > 0 ? commits[from - 1]?.sha : commits[0]?.parents?.[0]?.sha; + const endSha = commits[to]?.sha; + if (!startSha || !endSha || to < from) return undefined; + return to === last ? { startSha } : { startSha, endSha }; +} + +/** + * Whether both commits still belong to the PR in chronological order. + */ +export function isRangeAvailable( + commits: CommitLike[], + startSha: string, + endSha: string +): boolean { + return commitsInRange(commits, startSha, endSha) !== null; +} + +/** Cache key prefix for diffs parsed against a narrowed range. */ +export function rangeCacheKey(range: DiffRange | null, sha: string): string { + return range ? `${range.startSha}:${range.endSha ?? "head"}:${sha}` : sha; +} diff --git a/src/browser/lib/review-threads.test.ts b/src/browser/lib/review-threads.test.ts new file mode 100644 index 0000000..5e321f4 --- /dev/null +++ b/src/browser/lib/review-threads.test.ts @@ -0,0 +1,71 @@ +import { test, expect } from "bun:test"; +import type { ReviewComment } from "@/api/types"; +import type { ReviewThread } from "@/browser/contexts/github"; +import { enrichCommentsWithThreads } from "./review-threads"; + +function comment( + id: number, + extra: Partial = {} +): ReviewComment { + return { id, body: `c${id}`, ...extra } as ReviewComment; +} + +function thread( + id: string, + commentIds: number[], + isResolved: boolean, + resolvedBy: ReviewThread["resolvedBy"] = null +): ReviewThread { + return { + id, + isResolved, + resolvedBy, + pullRequestReview: null, + comments: { + nodes: commentIds.map((databaseId) => ({ databaseId })), + } as ReviewThread["comments"], + }; +} + +test("review-threads: stamps thread id and resolved state onto every comment in the thread", () => { + const out = enrichCommentsWithThreads( + [comment(1), comment(2), comment(3)], + [ + thread("T1", [1, 2], true, { login: "kyle", avatarUrl: "https://a/k" }), + thread("T2", [3], false), + ] + ); + expect(out[0]).toMatchObject({ + pull_request_review_thread_id: "T1", + is_resolved: true, + resolved_by: { login: "kyle", avatar_url: "https://a/k" }, + }); + expect(out[1]).toMatchObject({ + pull_request_review_thread_id: "T1", + is_resolved: true, + }); + expect(out[2]).toMatchObject({ + pull_request_review_thread_id: "T2", + is_resolved: false, + resolved_by: null, + }); +}); + +test("review-threads: leaves comments alone when no thread matches", () => { + const already = comment(9, { + pull_request_review_thread_id: "T9", + is_resolved: true, + }); + const out = enrichCommentsWithThreads( + [already, comment(10)], + [thread("T1", [1], true)] + ); + expect(out[0]).toBe(already); + expect(out[1].pull_request_review_thread_id).toBeUndefined(); + expect(out[1].is_resolved).toBeUndefined(); +}); + +test("review-threads: empty thread list returns input unchanged", () => { + const input = [comment(1, { is_resolved: true })]; + expect(enrichCommentsWithThreads(input, [])).toBe(input); +}); diff --git a/src/browser/lib/review-threads.ts b/src/browser/lib/review-threads.ts new file mode 100644 index 0000000..b48eb78 --- /dev/null +++ b/src/browser/lib/review-threads.ts @@ -0,0 +1,40 @@ +import type { ReviewComment } from "@/api/types"; +import type { ReviewThread } from "@/browser/contexts/github"; + +/** + * Stamp GraphQL review-thread info (thread id, resolved state, resolver) onto + * REST review comments. The REST API has no notion of threads, so without this + * every inline thread renders as unresolved and can't be resolved from the UI. + * + * Comments that don't match any thread keep whatever they already had, so + * passing an empty thread list (e.g. GraphQL failed) never wipes known state. + */ +export function enrichCommentsWithThreads( + comments: ReviewComment[], + threads: ReviewThread[] +): ReviewComment[] { + if (threads.length === 0) return comments; + + const byCommentId = new Map(); + for (const thread of threads) { + for (const c of thread.comments.nodes) { + byCommentId.set(c.databaseId, thread); + } + } + + return comments.map((comment) => { + const thread = byCommentId.get(comment.id); + if (!thread) return comment; + return { + ...comment, + pull_request_review_thread_id: thread.id, + is_resolved: thread.isResolved, + resolved_by: thread.resolvedBy + ? { + login: thread.resolvedBy.login, + avatar_url: thread.resolvedBy.avatarUrl, + } + : null, + }; + }); +} diff --git a/src/browser/lib/safe-html.test.ts b/src/browser/lib/safe-html.test.ts new file mode 100644 index 0000000..6500414 --- /dev/null +++ b/src/browser/lib/safe-html.test.ts @@ -0,0 +1,28 @@ +import { test, expect } from "bun:test"; +import { isAllowedHtmlAttribute, isAllowedHtmlTag } from "./safe-html"; + +test("safe-html: blocks executable tags, keeps content tags", () => { + for (const tag of ["script", "SCRIPT", "iframe", "object", "foreignObject"]) { + expect(isAllowedHtmlTag(tag)).toBe(false); + } + for (const tag of ["p", "a", "img", "code", "svg", "details"]) { + expect(isAllowedHtmlTag(tag)).toBe(true); + } +}); + +test("safe-html: blocks event handlers and script URLs", () => { + expect(isAllowedHtmlAttribute("onerror", "x")).toBe(false); + expect(isAllowedHtmlAttribute("href", "javascript:alert(1)")).toBe(false); + expect(isAllowedHtmlAttribute("href", " java\tscript:alert(1)")).toBe(false); + expect(isAllowedHtmlAttribute("src", "data:text/html,x")).toBe(false); + expect(isAllowedHtmlAttribute("srcset", "a.png 1x, javascript:x 2x")).toBe( + false + ); + + expect(isAllowedHtmlAttribute("href", "https://github.com/a")).toBe(true); + expect(isAllowedHtmlAttribute("href", "#heading")).toBe(true); + expect(isAllowedHtmlAttribute("href", "/owner/repo")).toBe(true); + expect(isAllowedHtmlAttribute("href", "mailto:a@b.c")).toBe(true); + expect(isAllowedHtmlAttribute("src", "image.png")).toBe(true); + expect(isAllowedHtmlAttribute("class", "javascript:")).toBe(true); +}); diff --git a/src/browser/lib/safe-html.ts b/src/browser/lib/safe-html.ts new file mode 100644 index 0000000..f84b226 --- /dev/null +++ b/src/browser/lib/safe-html.ts @@ -0,0 +1,56 @@ +/** + * Defense-in-depth filter for GitHub-rendered HTML (`body_html`). GitHub + * sanitizes it server-side; this keeps a slip there from running script on + * our origin, where the user's token lives. + */ + +const BLOCKED_TAGS = new Set([ + "script", + "style", + "iframe", + "frame", + "frameset", + "object", + "embed", + "applet", + "form", + "base", + "link", + "meta", + "noscript", + "template", + "foreignobject", +]); + +const URL_ATTRIBUTES = new Set([ + "href", + "src", + "xlink:href", + "action", + "formaction", + "poster", + "background", + "cite", + "data", +]); + +const SAFE_URL_RE = /^(https?:|mailto:|#|\/|\.{0,2}\/|[^:]*$)/i; + +export function isAllowedHtmlTag(tag: string): boolean { + return !BLOCKED_TAGS.has(tag.toLowerCase()); +} + +export function isAllowedHtmlAttribute(name: string, value: string): boolean { + const lower = name.toLowerCase(); + if (lower.startsWith("on")) return false; + if (lower === "srcset") { + return value + .split(",") + .every((candidate) => SAFE_URL_RE.test(candidate.trim())); + } + if (URL_ATTRIBUTES.has(lower)) { + // Browsers ignore control chars and whitespace inside URL schemes. + return SAFE_URL_RE.test(value.replace(/[\u0000- ]/g, "")); + } + return true; +} diff --git a/src/browser/lib/semantic-client.ts b/src/browser/lib/semantic-client.ts new file mode 100644 index 0000000..e7fa650 --- /dev/null +++ b/src/browser/lib/semantic-client.ts @@ -0,0 +1,117 @@ +/** + * Browser client for the semantic review API (local server only). + * + * All calls degrade gracefully: on the hosted deployment (or any fetch + * failure) the provider list comes back empty and the UI hides the feature. + */ + +import type { SemanticReview } from "@/semantic/schema"; +import type { AnalysisInput, ProviderInfo } from "@/semantic/providers/types"; + +export type { AnalysisInput, ProviderInfo }; + +export async function fetchSemanticProviders(): Promise { + try { + const res = await fetch("/api/semantic/providers"); + if (!res.ok) return []; + const data = (await res.json()) as { providers?: ProviderInfo[] }; + return Array.isArray(data.providers) ? data.providers : []; + } catch { + return []; + } +} + +/** Returns the cached review for this PR + head SHA, or null if none. */ +export async function fetchCachedSemanticReview( + owner: string, + repo: string, + number: number, + headSha: string +): Promise { + try { + const res = await fetch( + `/api/semantic/result/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/${number}/${encodeURIComponent(headSha)}` + ); + if (!res.ok) return null; + const data = (await res.json()) as { review?: SemanticReview }; + return data.review ?? null; + } catch { + return null; + } +} + +export async function startSemanticAnalysis( + provider: string, + input: AnalysisInput +): Promise { + const res = await fetch("/api/semantic/analyze", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ provider, input }), + }); + if (!res.ok) { + const data = (await res.json().catch(() => null)) as { + error?: string; + } | null; + throw new Error(data?.error ?? `analyze failed (${res.status})`); + } + const data = (await res.json()) as { jobId: string }; + return data.jobId; +} + +export interface SemanticJobHandlers { + onProgress: (message: string) => void; + onDone: (result: { review: SemanticReview; warnings?: string[] }) => void; + onError: (error: string) => void; +} + +/** + * Subscribe to a job's SSE progress stream. Returns an unsubscribe function. + * The server replays the full progress log on connect, so late subscribers + * (and reconnects) see everything. + */ +export function streamSemanticJob( + jobId: string, + handlers: SemanticJobHandlers +): () => void { + const es = new EventSource(`/api/semantic/jobs/${jobId}`); + + es.addEventListener("progress", (e) => { + handlers.onProgress((e as MessageEvent).data); + }); + + es.addEventListener("done", (e) => { + es.close(); + try { + handlers.onDone(JSON.parse((e as MessageEvent).data)); + } catch { + handlers.onError("invalid result payload"); + } + }); + + // Note: the server's terminal "error" event carries data; native + // EventSource connection errors fire the same event name without data. + es.addEventListener("error", (e) => { + const data = (e as MessageEvent).data as string | undefined; + if (data !== undefined) { + es.close(); + try { + const parsed = JSON.parse(data) as { error?: string }; + handlers.onError(parsed.error ?? "analysis failed"); + } catch { + handlers.onError("analysis failed"); + } + } else if (es.readyState === EventSource.CLOSED) { + handlers.onError("lost connection to analysis job"); + } + // Otherwise EventSource is reconnecting; the replay will catch us up. + }); + + return () => es.close(); +} + +export async function cancelSemanticJob(jobId: string): Promise { + try { + await fetch(`/api/semantic/jobs/${jobId}`, { method: "DELETE" }); + } catch {} +} diff --git a/src/browser/lib/sidebar-width.tsx b/src/browser/lib/sidebar-width.tsx new file mode 100644 index 0000000..2895532 --- /dev/null +++ b/src/browser/lib/sidebar-width.tsx @@ -0,0 +1,96 @@ +// ============================================================================ +// Resizable sidebar width +// ============================================================================ +// +// App-global preference persisted in localStorage, shared by the file-tree +// sidebar and the semantic-review sidebar so switching view modes doesn't +// change the layout. Resizing is desktop-only; the mobile drawer keeps the +// stored width but is capped by max-width in the sidebar's own classes. + +import { useCallback, useRef, useSyncExternalStore } from "react"; + +export const SIDEBAR_WIDTH_KEY = "pulldash_sidebar_width"; + +export const MIN_SIDEBAR_WIDTH = 200; +export const MAX_SIDEBAR_WIDTH = 520; +export const DEFAULT_SIDEBAR_WIDTH = 272; + +export function clampSidebarWidth(width: number): number { + if (!Number.isFinite(width)) return DEFAULT_SIDEBAR_WIDTH; + return Math.min(MAX_SIDEBAR_WIDTH, Math.max(MIN_SIDEBAR_WIDTH, width)); +} + +function getStoredWidth(): number { + try { + const stored = Number(localStorage.getItem(SIDEBAR_WIDTH_KEY)); + if (stored > 0) return clampSidebarWidth(stored); + } catch {} + return DEFAULT_SIDEBAR_WIDTH; +} + +let width = getStoredWidth(); +const listeners = new Set<() => void>(); + +function setSidebarWidth(next: number): void { + const clamped = clampSidebarWidth(next); + if (clamped === width) return; + width = clamped; + try { + localStorage.setItem(SIDEBAR_WIDTH_KEY, String(clamped)); + } catch {} + for (const l of listeners) l(); +} + +function subscribe(listener: () => void): () => void { + listeners.add(listener); + return () => listeners.delete(listener); +} + +export function useSidebarWidth(): number { + return useSyncExternalStore( + subscribe, + () => width, + () => width + ); +} + +/** + * Invisible drag handle for the sidebar's right edge. Render as the last + * child of the sidebar `