diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 59bbfd72..30b492de 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -5,6 +5,11 @@ on: workflows: ["Publish"] types: [completed] workflow_dispatch: + inputs: + version: + description: 'Version to benchmark ("dev" for local, or semver like "2.4.0" for npm)' + required: false + default: "dev" permissions: {} @@ -34,36 +39,46 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - - name: Download native addon from Publish - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: native-linux-x64 - path: ${{ runner.temp }}/native-artifact - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Install native addon from Publish - if: github.event_name == 'workflow_run' + - name: Determine benchmark mode + id: mode run: | - ARTIFACT_DIR="${{ runner.temp }}/native-artifact" - NODE_FILE="$ARTIFACT_DIR/codegraph-core.node" - if [ ! -f "$NODE_FILE" ]; then - echo "::error::Native addon not found at $NODE_FILE" - ls -la "$ARTIFACT_DIR" || true - exit 1 - fi - PKG_DIR="node_modules/@optave/codegraph-linux-x64-gnu" - mkdir -p "$PKG_DIR" - cp "$NODE_FILE" "$PKG_DIR/codegraph-core.node" - if [ ! -f "$PKG_DIR/package.json" ]; then - echo '{"name":"@optave/codegraph-linux-x64-gnu","main":"codegraph-core.node"}' > "$PKG_DIR/package.json" + if [ "${{ github.event_name }}" = "workflow_run" ]; then + # Release — find latest semver tag + TAG=$(git tag --sort=-version:refname --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -v dev | head -1) + VERSION="${TAG#v}" + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.version }}" = "dev" ] || [ -z "${{ inputs.version }}" ]; then + echo "source=local" >> "$GITHUB_OUTPUT" + echo "version=dev" >> "$GITHUB_OUTPUT" + else + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" fi - echo "Installed native addon from Publish workflow run ${{ github.event.workflow_run.id }}" - echo " sha256: $(sha256sum "$PKG_DIR/codegraph-core.node" | cut -d' ' -f1)" + + - name: Wait for npm propagation + if: steps.mode.outputs.source == 'npm' + run: | + VERSION="${{ steps.mode.outputs.version }}" + echo "Waiting for @optave/codegraph@${VERSION} on npm..." + for i in $(seq 1 20); do + if npm view "@optave/codegraph@${VERSION}" version 2>/dev/null; then + echo "Package available on npm" + exit 0 + fi + echo " Attempt $i/20 — not yet available, waiting 30s..." + sleep 30 + done + echo "::error::Package @optave/codegraph@${VERSION} not found on npm after 10 minutes" + exit 1 - name: Run build benchmark - run: node scripts/benchmark.js 2>/dev/null > benchmark-result.json + run: | + ARGS="--version ${{ steps.mode.outputs.version }}" + if [ "${{ steps.mode.outputs.source }}" = "npm" ]; then + ARGS="$ARGS --npm" + fi + node scripts/benchmark.js $ARGS 2>/dev/null > benchmark-result.json - name: Update build report run: node scripts/update-benchmark-report.js benchmark-result.json @@ -83,30 +98,30 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Extract version from result - id: version - run: echo "version=$(node -p "require('./benchmark-result.json').version")" >> "$GITHUB_OUTPUT" - - name: Commit and push via PR if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.mode.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - BRANCH="benchmark/build-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + if [ "$VERSION" = "dev" ]; then + BRANCH="benchmark/build-dev-$(date +%Y%m%d-%H%M%S)" + else + BRANCH="benchmark/build-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + fi git checkout -b "$BRANCH" git add generated/BUILD-BENCHMARKS.md README.md - git commit -m "docs: update build performance benchmarks (v${VERSION})" + git commit -m "docs: update build performance benchmarks (${VERSION})" git push origin "$BRANCH" gh pr create \ --base main \ --head "$BRANCH" \ - --title "docs: update build performance benchmarks (v${VERSION})" \ - --body "Automated build benchmark update for **v${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + --title "docs: update build performance benchmarks (${VERSION})" \ + --body "Automated build benchmark update for **${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." embedding-benchmark: runs-on: ubuntu-latest @@ -133,33 +148,37 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - - name: Download native addon from Publish - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: native-linux-x64 - path: ${{ runner.temp }}/native-artifact - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Install native addon from Publish - if: github.event_name == 'workflow_run' + - name: Determine benchmark mode + id: mode run: | - ARTIFACT_DIR="${{ runner.temp }}/native-artifact" - NODE_FILE="$ARTIFACT_DIR/codegraph-core.node" - if [ ! -f "$NODE_FILE" ]; then - echo "::error::Native addon not found at $NODE_FILE" - ls -la "$ARTIFACT_DIR" || true - exit 1 - fi - PKG_DIR="node_modules/@optave/codegraph-linux-x64-gnu" - mkdir -p "$PKG_DIR" - cp "$NODE_FILE" "$PKG_DIR/codegraph-core.node" - if [ ! -f "$PKG_DIR/package.json" ]; then - echo '{"name":"@optave/codegraph-linux-x64-gnu","main":"codegraph-core.node"}' > "$PKG_DIR/package.json" + if [ "${{ github.event_name }}" = "workflow_run" ]; then + TAG=$(git tag --sort=-version:refname --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -v dev | head -1) + VERSION="${TAG#v}" + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.version }}" = "dev" ] || [ -z "${{ inputs.version }}" ]; then + echo "source=local" >> "$GITHUB_OUTPUT" + echo "version=dev" >> "$GITHUB_OUTPUT" + else + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" fi - echo "Installed native addon from Publish workflow run ${{ github.event.workflow_run.id }}" - echo " sha256: $(sha256sum "$PKG_DIR/codegraph-core.node" | cut -d' ' -f1)" + + - name: Wait for npm propagation + if: steps.mode.outputs.source == 'npm' + run: | + VERSION="${{ steps.mode.outputs.version }}" + echo "Waiting for @optave/codegraph@${VERSION} on npm..." + for i in $(seq 1 20); do + if npm view "@optave/codegraph@${VERSION}" version 2>/dev/null; then + echo "Package available on npm" + exit 0 + fi + echo " Attempt $i/20 — not yet available, waiting 30s..." + sleep 30 + done + echo "::error::Package @optave/codegraph@${VERSION} not found on npm after 10 minutes" + exit 1 - name: Cache HuggingFace models uses: actions/cache@v4 @@ -174,7 +193,12 @@ jobs: - name: Run embedding benchmark env: HF_TOKEN: ${{ secrets.HF_TOKEN }} - run: node scripts/embedding-benchmark.js 2>/dev/null > embedding-benchmark-result.json + run: | + ARGS="--version ${{ steps.mode.outputs.version }}" + if [ "${{ steps.mode.outputs.source }}" = "npm" ]; then + ARGS="$ARGS --npm" + fi + node scripts/embedding-benchmark.js $ARGS 2>/dev/null > embedding-benchmark-result.json - name: Update embedding report run: node scripts/update-embedding-report.js embedding-benchmark-result.json @@ -194,30 +218,30 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Extract version from result - id: version - run: echo "version=$(node -p "require('./embedding-benchmark-result.json').version")" >> "$GITHUB_OUTPUT" - - name: Commit and push via PR if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.mode.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - BRANCH="benchmark/embedding-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + if [ "$VERSION" = "dev" ]; then + BRANCH="benchmark/embedding-dev-$(date +%Y%m%d-%H%M%S)" + else + BRANCH="benchmark/embedding-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + fi git checkout -b "$BRANCH" git add generated/EMBEDDING-BENCHMARKS.md - git commit -m "docs: update embedding benchmarks (v${VERSION})" + git commit -m "docs: update embedding benchmarks (${VERSION})" git push origin "$BRANCH" gh pr create \ --base main \ --head "$BRANCH" \ - --title "docs: update embedding benchmarks (v${VERSION})" \ - --body "Automated embedding benchmark update for **v${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + --title "docs: update embedding benchmarks (${VERSION})" \ + --body "Automated embedding benchmark update for **${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." query-benchmark: runs-on: ubuntu-latest @@ -244,36 +268,45 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - - name: Download native addon from Publish - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: native-linux-x64 - path: ${{ runner.temp }}/native-artifact - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Install native addon from Publish - if: github.event_name == 'workflow_run' + - name: Determine benchmark mode + id: mode run: | - ARTIFACT_DIR="${{ runner.temp }}/native-artifact" - NODE_FILE="$ARTIFACT_DIR/codegraph-core.node" - if [ ! -f "$NODE_FILE" ]; then - echo "::error::Native addon not found at $NODE_FILE" - ls -la "$ARTIFACT_DIR" || true - exit 1 - fi - PKG_DIR="node_modules/@optave/codegraph-linux-x64-gnu" - mkdir -p "$PKG_DIR" - cp "$NODE_FILE" "$PKG_DIR/codegraph-core.node" - if [ ! -f "$PKG_DIR/package.json" ]; then - echo '{"name":"@optave/codegraph-linux-x64-gnu","main":"codegraph-core.node"}' > "$PKG_DIR/package.json" + if [ "${{ github.event_name }}" = "workflow_run" ]; then + TAG=$(git tag --sort=-version:refname --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -v dev | head -1) + VERSION="${TAG#v}" + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.version }}" = "dev" ] || [ -z "${{ inputs.version }}" ]; then + echo "source=local" >> "$GITHUB_OUTPUT" + echo "version=dev" >> "$GITHUB_OUTPUT" + else + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" fi - echo "Installed native addon from Publish workflow run ${{ github.event.workflow_run.id }}" - echo " sha256: $(sha256sum "$PKG_DIR/codegraph-core.node" | cut -d' ' -f1)" + + - name: Wait for npm propagation + if: steps.mode.outputs.source == 'npm' + run: | + VERSION="${{ steps.mode.outputs.version }}" + echo "Waiting for @optave/codegraph@${VERSION} on npm..." + for i in $(seq 1 20); do + if npm view "@optave/codegraph@${VERSION}" version 2>/dev/null; then + echo "Package available on npm" + exit 0 + fi + echo " Attempt $i/20 — not yet available, waiting 30s..." + sleep 30 + done + echo "::error::Package @optave/codegraph@${VERSION} not found on npm after 10 minutes" + exit 1 - name: Run query benchmark - run: node scripts/query-benchmark.js 2>/dev/null > query-benchmark-result.json + run: | + ARGS="--version ${{ steps.mode.outputs.version }}" + if [ "${{ steps.mode.outputs.source }}" = "npm" ]; then + ARGS="$ARGS --npm" + fi + node scripts/query-benchmark.js $ARGS 2>/dev/null > query-benchmark-result.json - name: Update query report run: node scripts/update-query-report.js query-benchmark-result.json @@ -293,30 +326,30 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Extract version from result - id: version - run: echo "version=$(node -p "require('./query-benchmark-result.json').version")" >> "$GITHUB_OUTPUT" - - name: Commit and push via PR if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.mode.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - BRANCH="benchmark/query-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + if [ "$VERSION" = "dev" ]; then + BRANCH="benchmark/query-dev-$(date +%Y%m%d-%H%M%S)" + else + BRANCH="benchmark/query-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + fi git checkout -b "$BRANCH" git add generated/QUERY-BENCHMARKS.md - git commit -m "docs: update query benchmarks (v${VERSION})" + git commit -m "docs: update query benchmarks (${VERSION})" git push origin "$BRANCH" gh pr create \ --base main \ --head "$BRANCH" \ - --title "docs: update query benchmarks (v${VERSION})" \ - --body "Automated query benchmark update for **v${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + --title "docs: update query benchmarks (${VERSION})" \ + --body "Automated query benchmark update for **${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." incremental-benchmark: runs-on: ubuntu-latest @@ -343,36 +376,45 @@ jobs: - name: Install dependencies run: npm ci --prefer-offline --no-audit --no-fund - - name: Download native addon from Publish - if: github.event_name == 'workflow_run' - uses: actions/download-artifact@v4 - with: - name: native-linux-x64 - path: ${{ runner.temp }}/native-artifact - github-token: ${{ secrets.GITHUB_TOKEN }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Install native addon from Publish - if: github.event_name == 'workflow_run' + - name: Determine benchmark mode + id: mode run: | - ARTIFACT_DIR="${{ runner.temp }}/native-artifact" - NODE_FILE="$ARTIFACT_DIR/codegraph-core.node" - if [ ! -f "$NODE_FILE" ]; then - echo "::error::Native addon not found at $NODE_FILE" - ls -la "$ARTIFACT_DIR" || true - exit 1 - fi - PKG_DIR="node_modules/@optave/codegraph-linux-x64-gnu" - mkdir -p "$PKG_DIR" - cp "$NODE_FILE" "$PKG_DIR/codegraph-core.node" - if [ ! -f "$PKG_DIR/package.json" ]; then - echo '{"name":"@optave/codegraph-linux-x64-gnu","main":"codegraph-core.node"}' > "$PKG_DIR/package.json" + if [ "${{ github.event_name }}" = "workflow_run" ]; then + TAG=$(git tag --sort=-version:refname --list 'v[0-9]*.[0-9]*.[0-9]*' | grep -v dev | head -1) + VERSION="${TAG#v}" + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + elif [ "${{ inputs.version }}" = "dev" ] || [ -z "${{ inputs.version }}" ]; then + echo "source=local" >> "$GITHUB_OUTPUT" + echo "version=dev" >> "$GITHUB_OUTPUT" + else + echo "source=npm" >> "$GITHUB_OUTPUT" + echo "version=${{ inputs.version }}" >> "$GITHUB_OUTPUT" fi - echo "Installed native addon from Publish workflow run ${{ github.event.workflow_run.id }}" - echo " sha256: $(sha256sum "$PKG_DIR/codegraph-core.node" | cut -d' ' -f1)" + + - name: Wait for npm propagation + if: steps.mode.outputs.source == 'npm' + run: | + VERSION="${{ steps.mode.outputs.version }}" + echo "Waiting for @optave/codegraph@${VERSION} on npm..." + for i in $(seq 1 20); do + if npm view "@optave/codegraph@${VERSION}" version 2>/dev/null; then + echo "Package available on npm" + exit 0 + fi + echo " Attempt $i/20 — not yet available, waiting 30s..." + sleep 30 + done + echo "::error::Package @optave/codegraph@${VERSION} not found on npm after 10 minutes" + exit 1 - name: Run incremental benchmark - run: node scripts/incremental-benchmark.js 2>/dev/null > incremental-benchmark-result.json + run: | + ARGS="--version ${{ steps.mode.outputs.version }}" + if [ "${{ steps.mode.outputs.source }}" = "npm" ]; then + ARGS="$ARGS --npm" + fi + node scripts/incremental-benchmark.js $ARGS 2>/dev/null > incremental-benchmark-result.json - name: Update incremental report run: node scripts/update-incremental-report.js incremental-benchmark-result.json @@ -392,27 +434,27 @@ jobs: echo "changed=true" >> "$GITHUB_OUTPUT" fi - - name: Extract version from result - id: version - run: echo "version=$(node -p "require('./incremental-benchmark-result.json').version")" >> "$GITHUB_OUTPUT" - - name: Commit and push via PR if: steps.changes.outputs.changed == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ steps.version.outputs.version }} + VERSION: ${{ steps.mode.outputs.version }} run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - BRANCH="benchmark/incremental-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + if [ "$VERSION" = "dev" ]; then + BRANCH="benchmark/incremental-dev-$(date +%Y%m%d-%H%M%S)" + else + BRANCH="benchmark/incremental-v${VERSION}-$(date +%Y%m%d-%H%M%S)" + fi git checkout -b "$BRANCH" git add generated/INCREMENTAL-BENCHMARKS.md - git commit -m "docs: update incremental benchmarks (v${VERSION})" + git commit -m "docs: update incremental benchmarks (${VERSION})" git push origin "$BRANCH" gh pr create \ --base main \ --head "$BRANCH" \ - --title "docs: update incremental benchmarks (v${VERSION})" \ - --body "Automated incremental benchmark update for **v${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." + --title "docs: update incremental benchmarks (${VERSION})" \ + --body "Automated incremental benchmark update for **${VERSION}** from workflow run [#${{ github.run_number }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})." diff --git a/README.md b/README.md index 9c0206ad..bb017413 100644 --- a/README.md +++ b/README.md @@ -423,14 +423,12 @@ Self-measured on every release via CI ([build benchmarks](generated/BUILD-BENCHM | Metric | Latest | |---|---| -| Build speed (native) | **1.9 ms/file** | -| Build speed (WASM) | **9.3 ms/file** | -| Query time | **3ms** | -| No-op rebuild (native) | **4ms** | -| 1-file rebuild (native) | **89ms** | -| Query: fn-deps | **2.1ms** | -| Query: path | **1.2ms** | -| ~50,000 files (est.) | **~95.0s build** | +| Build speed | **5.1 ms/file** | +| Query time | **2ms** | +| No-op rebuild | **5ms** | +| 1-file rebuild | **192ms** | +| Query: fn-deps | **0.5ms** | +| ~50,000 files (est.) | **~255.0s build** | Metrics are normalized per file for cross-version comparability. Times above are for a full initial build — incremental rebuilds only re-parse changed files. diff --git a/crates/codegraph-core/src/complexity.rs b/crates/codegraph-core/src/complexity.rs index 4fbdc406..df2bdaf1 100644 --- a/crates/codegraph-core/src/complexity.rs +++ b/crates/codegraph-core/src/complexity.rs @@ -999,7 +999,7 @@ pub fn compute_all_metrics( cognitive, cyclomatic, max_nesting, - halstead: Some(halstead.unwrap_or(HalsteadMetrics { + halstead: halstead.or(Some(HalsteadMetrics { n1: 0, n2: 0, big_n1: 0, big_n2: 0, vocabulary: 0, length: 0, volume: 0.0, difficulty: 0.0, effort: 0.0, bugs: 0.0, diff --git a/generated/BUILD-BENCHMARKS.md b/generated/BUILD-BENCHMARKS.md index 13f2c835..20c60dba 100644 --- a/generated/BUILD-BENCHMARKS.md +++ b/generated/BUILD-BENCHMARKS.md @@ -5,8 +5,7 @@ Metrics are normalized per file for cross-version comparability. | Version | Engine | Date | Files | Build (ms/file) | Query (ms) | Nodes/file | Edges/file | DB (bytes/file) | |---------|--------|------|------:|----------------:|-----------:|-----------:|-----------:|----------------:| -| 2.4.0 | native | 2026-02-27 | 122 | 1.9 ~ | 2.5 ↑67% | 6.4 ↑10% | 10.9 ↑20% | 5238 ↑36% | -| 2.4.0 | wasm | 2026-02-27 | 122 | 9.3 ↑41% | 3.4 ↑62% | 6.4 ↑10% | 10.9 ↑20% | 5506 ↑43% | +| 2.4.0 | wasm | 2026-02-28 | 123 | 5.1 ↓23% | 2.2 ↑5% | 6.5 ↑12% | 10.7 ↑18% | 4695 ↑22% | | 2.3.0 | native | 2026-02-24 | 99 | 1.9 ~ | 1.5 ↑7% | 5.8 ↑7% | 9.1 ~ | 3848 ~ | | 2.3.0 | wasm | 2026-02-24 | 99 | 6.6 ~ | 2.1 ↑11% | 5.8 ~ | 9.1 ↑3% | 3848 ~ | | 2.1.0 | native | 2026-02-23 | 92 | 1.9 ↓24% | 1.4 ↑17% | 5.4 ↑6% | 9.1 ↓47% | 3829 ↓14% | @@ -16,39 +15,16 @@ Metrics are normalized per file for cross-version comparability. ### Raw totals (latest) -#### Native (Rust) - -| Metric | Value | -|--------|-------| -| Build time | 229ms | -| Query time | 3ms | -| Nodes | 778 | -| Edges | 1,333 | -| DB size | 624 KB | -| Files | 122 | - #### WASM | Metric | Value | |--------|-------| -| Build time | 1.1s | -| Query time | 3ms | -| Nodes | 778 | -| Edges | 1,333 | -| DB size | 656 KB | -| Files | 122 | - -### Build Phase Breakdown (latest) - -| Phase | Native | WASM | -|-------|-------:|-----:| -| Parse | 116 ms | 639.2 ms | -| Insert nodes | 14.1 ms | 15.9 ms | -| Resolve imports | 10.2 ms | 13.4 ms | -| Build edges | 61.9 ms | 59.1 ms | -| Structure | 4.1 ms | 7.3 ms | -| Roles | 4.7 ms | 5.2 ms | -| Complexity | 4.6 ms | 367.8 ms | +| Build time | 630ms | +| Query time | 2ms | +| Nodes | 801 | +| Edges | 1,320 | +| DB size | 564 KB | +| Files | 123 | ### Estimated performance at 50,000 files @@ -56,24 +32,22 @@ Extrapolated linearly from per-file metrics above. | Metric | Native (Rust) | WASM | |--------|---:|---:| -| Build time | 95.0s | 465.0s | -| DB size | 249.8 MB | 262.5 MB | -| Nodes | 320,000 | 320,000 | -| Edges | 545,000 | 545,000 | +| Build time | n/a | 255.0s | +| DB size | n/a | 223.9 MB | +| Nodes | n/a | 325,000 | +| Edges | n/a | 535,000 | ### Incremental Rebuilds | Version | Engine | No-op (ms) | 1-file (ms) | |---------|--------|----------:|-----------:| -| 2.4.0 | native | 4 | 89 | -| 2.4.0 | wasm | 4 | 362 | +| 2.4.0 | wasm | 5 | 192 | ### Query Latency | Version | Engine | fn-deps (ms) | fn-impact (ms) | path (ms) | roles (ms) | |---------|--------|------------:|--------------:|----------:|----------:| -| 2.4.0 | native | 2.1 | 1.6 | 1.2 | 1.1 | -| 2.4.0 | wasm | 2.1 | 1.6 | 1.2 | 1.1 | +| 2.4.0 | wasm | 0.5 | 0.5 | null | 0.9 | ### Notes @@ -99,68 +73,31 @@ extractor is needed to recover the regression. [ { "version": "2.4.0", - "date": "2026-02-27", - "files": 122, + "date": "2026-02-28", + "files": 123, "wasm": { - "buildTimeMs": 1138, - "queryTimeMs": 3.4, - "nodes": 778, - "edges": 1333, - "dbSizeBytes": 671744, + "buildTimeMs": 630, + "queryTimeMs": 2.2, + "nodes": 801, + "edges": 1320, + "dbSizeBytes": 577536, "perFile": { - "buildTimeMs": 9.3, - "nodes": 6.4, - "edges": 10.9, - "dbSizeBytes": 5506 + "buildTimeMs": 5.1, + "nodes": 6.5, + "edges": 10.7, + "dbSizeBytes": 4695 }, - "noopRebuildMs": 4, - "oneFileRebuildMs": 362, + "noopRebuildMs": 5, + "oneFileRebuildMs": 192, "queries": { - "fnDepsMs": 2.1, - "fnImpactMs": 1.6, - "pathMs": 1.2, - "rolesMs": 1.1 + "fnDepsMs": 0.5, + "fnImpactMs": 0.5, + "pathMs": null, + "rolesMs": 0.9 }, - "phases": { - "parseMs": 639.2, - "insertMs": 15.9, - "resolveMs": 13.4, - "edgesMs": 59.1, - "structureMs": 7.3, - "rolesMs": 5.2, - "complexityMs": 367.8 - } + "phases": null }, - "native": { - "buildTimeMs": 229, - "queryTimeMs": 2.5, - "nodes": 778, - "edges": 1333, - "dbSizeBytes": 638976, - "perFile": { - "buildTimeMs": 1.9, - "nodes": 6.4, - "edges": 10.9, - "dbSizeBytes": 5238 - }, - "noopRebuildMs": 4, - "oneFileRebuildMs": 89, - "queries": { - "fnDepsMs": 2.1, - "fnImpactMs": 1.6, - "pathMs": 1.2, - "rolesMs": 1.1 - }, - "phases": { - "parseMs": 116, - "insertMs": 14.1, - "resolveMs": 10.2, - "edgesMs": 61.9, - "structureMs": 4.1, - "rolesMs": 4.7, - "complexityMs": 4.6 - } - } + "native": null }, { "version": "2.3.0", diff --git a/scripts/benchmark.js b/scripts/benchmark.js index f18015ff..d37b8422 100644 --- a/scripts/benchmark.js +++ b/scripts/benchmark.js @@ -12,24 +12,24 @@ import fs from 'node:fs'; import path from 'node:path'; import { performance } from 'node:perf_hooks'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; import Database from 'better-sqlite3'; +import { resolveBenchmarkSource, srcImport } from './lib/bench-config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); -// Read version from package.json -const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const { version, srcDir, cleanup } = await resolveBenchmarkSource(); const dbPath = path.join(root, '.codegraph', 'graph.db'); // Import programmatic API (use file:// URLs for Windows compatibility) -const { buildGraph } = await import(pathToFileURL(path.join(root, 'src', 'builder.js')).href); +const { buildGraph } = await import(srcImport(srcDir, 'builder.js')); const { fnDepsData, fnImpactData, pathData, rolesData, statsData } = await import( - pathToFileURL(path.join(root, 'src', 'queries.js')).href + srcImport(srcDir, 'queries.js') ); const { isNativeAvailable } = await import( - pathToFileURL(path.join(root, 'src', 'native.js')).href + srcImport(srcDir, 'native.js') ); const INCREMENTAL_RUNS = 3; @@ -133,10 +133,10 @@ async function benchmarkEngine(engine) { } const queries = { - fnDepsMs: benchQuery(fnDepsData, targets.hub, dbPath, { depth: 3, noTests: true }), - fnImpactMs: benchQuery(fnImpactData, targets.hub, dbPath, { depth: 3, noTests: true }), - pathMs: benchQuery(pathData, targets.hub, targets.leaf, dbPath, { noTests: true }), - rolesMs: benchQuery(rolesData, dbPath, { noTests: true }), + fnDepsMs: fnDepsData ? benchQuery(fnDepsData, targets.hub, dbPath, { depth: 3, noTests: true }) : null, + fnImpactMs: fnImpactData ? benchQuery(fnImpactData, targets.hub, dbPath, { depth: 3, noTests: true }) : null, + pathMs: pathData ? benchQuery(pathData, targets.hub, targets.leaf, dbPath, { noTests: true }) : null, + rolesMs: rolesData ? benchQuery(rolesData, dbPath, { noTests: true }) : null, }; return { @@ -173,7 +173,7 @@ if (isNativeAvailable()) { console.log = origLog; const result = { - version: pkg.version, + version, date: new Date().toISOString().slice(0, 10), files: wasm.files, wasm: { @@ -205,3 +205,5 @@ const result = { }; console.log(JSON.stringify(result, null, 2)); + +cleanup(); diff --git a/scripts/embedding-benchmark.js b/scripts/embedding-benchmark.js index 3d7ef173..3e97c684 100644 --- a/scripts/embedding-benchmark.js +++ b/scripts/embedding-benchmark.js @@ -17,15 +17,16 @@ import path from 'node:path'; import { performance } from 'node:perf_hooks'; import { fileURLToPath } from 'node:url'; import Database from 'better-sqlite3'; +import { resolveBenchmarkSource, srcImport } from './lib/bench-config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); -const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const { version, srcDir, cleanup } = await resolveBenchmarkSource(); const dbPath = path.join(root, '.codegraph', 'graph.db'); const { buildEmbeddings, MODELS, searchData, disposeModel } = await import( - new URL('../src/embedder.js', import.meta.url).href + srcImport(srcDir, 'embedder.js') ); // Redirect console.log to stderr so only JSON goes to stdout @@ -136,7 +137,7 @@ for (const key of modelKeys) { console.log = origLog; const output = { - version: pkg.version, + version, date: new Date().toISOString().slice(0, 10), strategy: 'structured', symbols: symbols.length, @@ -144,3 +145,5 @@ const output = { }; console.log(JSON.stringify(output, null, 2)); + +cleanup(); diff --git a/scripts/incremental-benchmark.js b/scripts/incremental-benchmark.js index 94f4963a..426b56fd 100644 --- a/scripts/incremental-benchmark.js +++ b/scripts/incremental-benchmark.js @@ -13,20 +13,22 @@ import fs from 'node:fs'; import path from 'node:path'; import { performance } from 'node:perf_hooks'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; +import { resolveBenchmarkSource, srcImport } from './lib/bench-config.js'; + const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); -const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const { version, srcDir, cleanup } = await resolveBenchmarkSource(); const dbPath = path.join(root, '.codegraph', 'graph.db'); -const { buildGraph } = await import(pathToFileURL(path.join(root, 'src', 'builder.js')).href); -const { statsData } = await import(pathToFileURL(path.join(root, 'src', 'queries.js')).href); +const { buildGraph } = await import(srcImport(srcDir, 'builder.js')); +const { statsData } = await import(srcImport(srcDir, 'queries.js')); const { resolveImportPath, resolveImportsBatch, resolveImportPathJS } = await import( - pathToFileURL(path.join(root, 'src', 'resolve.js')).href + srcImport(srcDir, 'resolve.js') ); const { isNativeAvailable } = await import( - pathToFileURL(path.join(root, 'src', 'native.js')).href + srcImport(srcDir, 'native.js') ); // Redirect console.log to stderr so only JSON goes to stdout @@ -181,7 +183,7 @@ console.error(` native=${resolve.nativeBatchMs}ms js=${resolve.jsFallbackMs}ms` console.log = origLog; const result = { - version: pkg.version, + version, date: new Date().toISOString().slice(0, 10), files, wasm: { @@ -200,3 +202,5 @@ const result = { }; console.log(JSON.stringify(result, null, 2)); + +cleanup(); diff --git a/scripts/lib/bench-config.js b/scripts/lib/bench-config.js new file mode 100644 index 00000000..804a6b3f --- /dev/null +++ b/scripts/lib/bench-config.js @@ -0,0 +1,125 @@ +/** + * Shared benchmark configuration — CLI arg parsing and source resolution. + * + * All 4 benchmark scripts use this to determine: + * - version label ("dev" for local, semver for npm releases) + * - srcDir (local src/ or npm-installed package src/) + * - cleanup function (removes temp dir for npm mode) + */ + +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; + +/** + * Parse `--version ` and `--npm` from process.argv. + */ +export function parseArgs() { + const args = process.argv.slice(2); + let version = null; + let npm = false; + + for (let i = 0; i < args.length; i++) { + if (args[i] === '--version' && i + 1 < args.length) { + version = args[++i]; + } else if (args[i] === '--npm') { + npm = true; + } + } + + return { version, npm }; +} + +/** + * Resolve where to import codegraph source from. + * + * @returns {{ version: string, srcDir: string, cleanup: () => void }} + * - version: "dev" (local) or the semver string (npm) + * - srcDir: absolute path to the codegraph src/ directory to import from + * - cleanup: call when done — removes the temp dir in npm mode, no-op otherwise + */ +export async function resolveBenchmarkSource() { + const { version: cliVersion, npm } = parseArgs(); + + if (!npm) { + // Local mode — use repo src/, label as "dev" unless overridden + const root = path.resolve(path.dirname(new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, '$1')), '..', '..'); + return { + version: cliVersion || 'dev', + srcDir: path.join(root, 'src'), + cleanup() {}, + }; + } + + // npm mode — install @optave/codegraph@ into a temp dir + const version = cliVersion || 'latest'; + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-bench-')); + + console.error(`Installing @optave/codegraph@${version} into ${tmpDir}...`); + + // Write a minimal package.json so npm install works + fs.writeFileSync(path.join(tmpDir, 'package.json'), JSON.stringify({ private: true })); + + // Retry with backoff for npm propagation delays + const maxRetries = 5; + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + execFileSync('npm', ['install', `@optave/codegraph@${version}`, '--no-audit', '--no-fund'], { + cwd: tmpDir, + stdio: 'pipe', + timeout: 120_000, + shell: true, + }); + break; + } catch (err) { + if (attempt === maxRetries) { + // Clean up before throwing + fs.rmSync(tmpDir, { recursive: true, force: true }); + throw new Error(`Failed to install @optave/codegraph@${version} after ${maxRetries} attempts: ${err.message}`); + } + const delay = attempt * 15_000; // 15s, 30s, 45s, 60s + console.error(` Attempt ${attempt} failed, retrying in ${delay / 1000}s...`); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + + const pkgDir = path.join(tmpDir, 'node_modules', '@optave', 'codegraph'); + const srcDir = path.join(pkgDir, 'src'); + + if (!fs.existsSync(srcDir)) { + fs.rmSync(tmpDir, { recursive: true, force: true }); + throw new Error(`Installed package does not contain src/ at ${srcDir}`); + } + + // Resolve the actual version from the installed package + const installedPkg = JSON.parse(fs.readFileSync(path.join(pkgDir, 'package.json'), 'utf8')); + const resolvedVersion = cliVersion || installedPkg.version; + + console.error(`Installed @optave/codegraph@${installedPkg.version}`); + + return { + version: resolvedVersion, + srcDir, + cleanup() { + try { + fs.rmSync(tmpDir, { recursive: true, force: true }); + console.error(`Cleaned up temp dir: ${tmpDir}`); + } catch { + // best-effort + } + }, + }; +} + +/** + * Build a file:// URL suitable for dynamic import. + * + * @param {string} srcDir Absolute path to the codegraph src/ directory + * @param {string} file Relative filename within src/ (e.g. 'builder.js') + * @returns {string} file:// URL string + */ +export function srcImport(srcDir, file) { + return pathToFileURL(path.join(srcDir, file)).href; +} diff --git a/scripts/query-benchmark.js b/scripts/query-benchmark.js index de1716df..4347c0a9 100644 --- a/scripts/query-benchmark.js +++ b/scripts/query-benchmark.js @@ -15,21 +15,22 @@ import { execFileSync } from 'node:child_process'; import fs from 'node:fs'; import path from 'node:path'; import { performance } from 'node:perf_hooks'; -import { fileURLToPath, pathToFileURL } from 'node:url'; +import { fileURLToPath } from 'node:url'; import Database from 'better-sqlite3'; +import { resolveBenchmarkSource, srcImport } from './lib/bench-config.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const root = path.resolve(__dirname, '..'); -const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')); +const { version, srcDir, cleanup } = await resolveBenchmarkSource(); const dbPath = path.join(root, '.codegraph', 'graph.db'); -const { buildGraph } = await import(pathToFileURL(path.join(root, 'src', 'builder.js')).href); +const { buildGraph } = await import(srcImport(srcDir, 'builder.js')); const { fnDepsData, fnImpactData, diffImpactData, statsData } = await import( - pathToFileURL(path.join(root, 'src', 'queries.js')).href + srcImport(srcDir, 'queries.js') ); const { isNativeAvailable } = await import( - pathToFileURL(path.join(root, 'src', 'native.js')).href + srcImport(srcDir, 'native.js') ); // Redirect console.log to stderr so only JSON goes to stdout @@ -177,7 +178,7 @@ if (isNativeAvailable()) { console.log = origLog; const result = { - version: pkg.version, + version, date: new Date().toISOString().slice(0, 10), wasm: { targets: wasm.targets, @@ -196,3 +197,5 @@ const result = { }; console.log(JSON.stringify(result, null, 2)); + +cleanup(); diff --git a/scripts/update-benchmark-report.js b/scripts/update-benchmark-report.js index 359c63f9..2feedeab 100644 --- a/scripts/update-benchmark-report.js +++ b/scripts/update-benchmark-report.js @@ -45,12 +45,26 @@ if (fs.existsSync(benchmarkPath)) { } } -// Add new entry (deduplicate by version — replace if same version exists) +// Add new entry — dev entries are rolling, releases replace dev +const isDev = entry.version === 'dev'; const idx = history.findIndex((h) => h.version === entry.version); -if (idx >= 0) { - history[idx] = entry; -} else { - history.unshift(entry); +if (idx >= 0) history.splice(idx, 1); +// If this is a release, also remove any "dev" entry +if (!isDev) { + const devIdx = history.findIndex((h) => h.version === 'dev'); + if (devIdx >= 0) history.splice(devIdx, 1); +} +history.unshift(entry); + +/** + * Find the next non-dev entry after index `idx` for trend comparison. + * Dev trends compare against the latest release; release trends skip dev. + */ +function findPrevRelease(hist, fromIdx) { + for (let i = fromIdx + 1; i < hist.length; i++) { + if (hist[i].version !== 'dev') return hist[i]; + } + return null; } // ── Helpers ────────────────────────────────────────────────────────────── @@ -108,7 +122,7 @@ md += for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); const nativeRow = engineRow(h, prev, 'native'); const wasmRow = engineRow(h, prev, 'wasm'); @@ -182,7 +196,7 @@ if (hasIncremental) { for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); for (const engineKey of ['native', 'wasm']) { const e = h[engineKey]; @@ -207,7 +221,7 @@ if (hasQueries) { for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); for (const engineKey of ['native', 'wasm']) { const e = h[engineKey]; @@ -246,7 +260,7 @@ console.error(`Updated ${path.relative(root, benchmarkPath)}`); // ── Regression detection ───────────────────────────────────────────────── const REGRESSION_THRESHOLD = 0.15; // 15% regression triggers a warning -const prev = history[1] || null; +const prev = findPrevRelease(history, 0); function checkRegression(label, current, previous) { if (previous == null || previous === 0) return; @@ -304,10 +318,10 @@ if (fs.existsSync(readmePath)) { rows += `| 1-file rebuild${prefLabel} | **${formatMs(pref.oneFileRebuildMs)}** |\n`; } - // Query latency rows (pick two representative queries) + // Query latency rows (pick two representative queries, skip if null) if (pref.queries) { - rows += `| Query: fn-deps | **${pref.queries.fnDepsMs}ms** |\n`; - rows += `| Query: path | **${pref.queries.pathMs}ms** |\n`; + if (pref.queries.fnDepsMs != null) rows += `| Query: fn-deps | **${pref.queries.fnDepsMs}ms** |\n`; + if (pref.queries.pathMs != null) rows += `| Query: path | **${pref.queries.pathMs}ms** |\n`; } // 50k-file estimate diff --git a/scripts/update-embedding-report.js b/scripts/update-embedding-report.js index 90fbe624..eebec37a 100644 --- a/scripts/update-embedding-report.js +++ b/scripts/update-embedding-report.js @@ -43,12 +43,21 @@ if (fs.existsSync(reportPath)) { } } -// Add new entry (deduplicate by version) +// Add new entry — dev entries are rolling, releases replace dev +const isDev = entry.version === 'dev'; const idx = history.findIndex((h) => h.version === entry.version); -if (idx >= 0) { - history[idx] = entry; -} else { - history.unshift(entry); +if (idx >= 0) history.splice(idx, 1); +if (!isDev) { + const devIdx = history.findIndex((h) => h.version === 'dev'); + if (devIdx >= 0) history.splice(devIdx, 1); +} +history.unshift(entry); + +function findPrevRelease(hist, fromIdx) { + for (let i = fromIdx + 1; i < hist.length; i++) { + if (hist[i].version !== 'dev') return hist[i]; + } + return null; } // ── Helpers ────────────────────────────────────────────────────────────── @@ -85,7 +94,7 @@ md += for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); for (const [modelKey, m] of Object.entries(h.models)) { const pm = prev?.models?.[modelKey] || null; @@ -135,7 +144,7 @@ console.error(`Updated ${path.relative(root, reportPath)}`); // ── Regression detection ───────────────────────────────────────────────── const REGRESSION_THRESHOLD = 0.15; // 15% regression triggers a warning -const prev = history[1] || null; +const prev = findPrevRelease(history, 0); function checkRegression(label, current, previous, lowerIsBetter = true) { if (previous == null || previous === 0) return; diff --git a/scripts/update-incremental-report.js b/scripts/update-incremental-report.js index 3630f927..b5ea2cb7 100644 --- a/scripts/update-incremental-report.js +++ b/scripts/update-incremental-report.js @@ -43,12 +43,21 @@ if (fs.existsSync(reportPath)) { } } -// Add new entry (deduplicate by version — replace if same version exists) +// Add new entry — dev entries are rolling, releases replace dev +const isDev = entry.version === 'dev'; const idx = history.findIndex((h) => h.version === entry.version); -if (idx >= 0) { - history[idx] = entry; -} else { - history.unshift(entry); +if (idx >= 0) history.splice(idx, 1); +if (!isDev) { + const devIdx = history.findIndex((h) => h.version === 'dev'); + if (devIdx >= 0) history.splice(devIdx, 1); +} +history.unshift(entry); + +function findPrevRelease(hist, fromIdx) { + for (let i = fromIdx + 1; i < hist.length; i++) { + if (hist[i].version !== 'dev') return hist[i]; + } + return null; } // ── Helpers ────────────────────────────────────────────────────────────── @@ -104,7 +113,7 @@ md += for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); const nativeRow = engineRow(h, prev, 'native'); const wasmRow = engineRow(h, prev, 'wasm'); @@ -151,7 +160,7 @@ console.error(`Updated ${path.relative(root, reportPath)}`); // ── Regression detection ───────────────────────────────────────────────── const REGRESSION_THRESHOLD = 0.15; // 15% regression triggers a warning -const prev = history[1] || null; +const prev = findPrevRelease(history, 0); function checkRegression(label, current, previous) { if (previous == null || previous === 0) return; diff --git a/scripts/update-query-report.js b/scripts/update-query-report.js index 292d6669..4cb66375 100644 --- a/scripts/update-query-report.js +++ b/scripts/update-query-report.js @@ -43,12 +43,21 @@ if (fs.existsSync(reportPath)) { } } -// Add new entry (deduplicate by version — replace if same version exists) +// Add new entry — dev entries are rolling, releases replace dev +const isDev = entry.version === 'dev'; const idx = history.findIndex((h) => h.version === entry.version); -if (idx >= 0) { - history[idx] = entry; -} else { - history.unshift(entry); +if (idx >= 0) history.splice(idx, 1); +if (!isDev) { + const devIdx = history.findIndex((h) => h.version === 'dev'); + if (devIdx >= 0) history.splice(devIdx, 1); +} +history.unshift(entry); + +function findPrevRelease(hist, fromIdx) { + for (let i = fromIdx + 1; i < hist.length; i++) { + if (hist[i].version !== 'dev') return hist[i]; + } + return null; } // ── Helpers ────────────────────────────────────────────────────────────── @@ -104,7 +113,7 @@ md += for (let i = 0; i < history.length; i++) { const h = history[i]; - const prev = history[i + 1] || null; + const prev = findPrevRelease(history, i); const nativeRow = engineRow(h, prev, 'native'); const wasmRow = engineRow(h, prev, 'wasm'); @@ -145,7 +154,7 @@ console.error(`Updated ${path.relative(root, reportPath)}`); // ── Regression detection ───────────────────────────────────────────────── const REGRESSION_THRESHOLD = 0.15; // 15% regression triggers a warning -const prev = history[1] || null; +const prev = findPrevRelease(history, 0); function checkRegression(label, current, previous) { if (previous == null || previous === 0) return;