From 3b0bf70d6fbd155390a6f38b09e3d8555d0f0c14 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 15:06:23 -0600 Subject: [PATCH 1/2] fix(scripts): preserve manual NOTES block in incremental report generator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/update-incremental-report.ts regenerates the report from scratch on every release. PR #1008 added a manual ... block explaining why 3.9.5 had null build metrics, but the 3.9.6 release silently dropped it because the script had no awareness of the sentinel pair. The version-history table now jumps 3.9.4 → 3.9.6 with no explanation. Extract any NOTES block from the existing file (mirroring how INCREMENTAL_BENCHMARK_DATA is preserved) and re-emit it just before the data comment in the regenerated markdown. Restore the lost 3.9.5 note and add a regression test gated on the env-overridable report path. Closes #1042 --- .../benchmarks/INCREMENTAL-BENCHMARKS.md | 4 + scripts/update-incremental-report.ts | 11 +- tests/unit/update-incremental-report.test.ts | 105 ++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) create mode 100644 tests/unit/update-incremental-report.test.ts diff --git a/generated/benchmarks/INCREMENTAL-BENCHMARKS.md b/generated/benchmarks/INCREMENTAL-BENCHMARKS.md index 9368a6817..9baec9a12 100644 --- a/generated/benchmarks/INCREMENTAL-BENCHMARKS.md +++ b/generated/benchmarks/INCREMENTAL-BENCHMARKS.md @@ -88,6 +88,10 @@ Import resolution: native batch vs JS fallback throughput. | Per-import (JS) | 0ms | | Speedup ratio | 1.2x | + +**Note (3.9.5):** No build/rebuild metrics for this release (both engines null) — only import resolution data was collected. Both the WASM and native workers reached the 1-file rebuild phase and then hung past the benchmark's 10-minute per-engine timeout (see `scripts/lib/fork-engine.ts`), so each was killed (`SIGKILL`) before returning results. Import resolution is unaffected because it runs in the parent process and doesn't depend on the full build. 3.9.5 is consequently absent from the top-level version-history comparison table since there are no build-time figures to compare against prior releases. The workflow run is [here](https://github.com/optave/ops-codegraph-tool/actions/runs/24863501577); the root cause will be investigated and the numbers backfilled in a follow-up if possible. + + /); @@ -41,6 +44,8 @@ if (fs.existsSync(reportPath)) { /* start fresh if corrupt */ } } + const notesMatch = content.match(/[\s\S]*?/); + if (notesMatch) notesBlock = notesMatch[0]; } // Add new entry — dev entries are rolling, releases replace dev @@ -155,6 +160,8 @@ if (r.nativeBatchMs != null && r.jsFallbackMs > 0) { } md += '\n'; +if (notesBlock) md += `${notesBlock}\n\n`; + md += `\n`; fs.mkdirSync(path.dirname(reportPath), { recursive: true }); diff --git a/tests/unit/update-incremental-report.test.ts b/tests/unit/update-incremental-report.test.ts new file mode 100644 index 000000000..5b1e98292 --- /dev/null +++ b/tests/unit/update-incremental-report.test.ts @@ -0,0 +1,105 @@ +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(__dirname, '..', '..'); +const scriptPath = path.join(repoRoot, 'scripts', 'update-incremental-report.ts'); + +// --experimental-strip-types works in Node 22.6+ through current 24.x; the +// renamed --strip-types was added then removed again across 24.x minor +// versions, so prefer the experimental name for compatibility. +const stripFlag = '--experimental-strip-types'; + +const SAMPLE_ENTRY = { + version: '9.9.9', + date: '2026-05-14', + files: 100, + wasm: { fullBuildMs: 1000, noopRebuildMs: 10, oneFileRebuildMs: 50 }, + native: { fullBuildMs: 500, noopRebuildMs: 5, oneFileRebuildMs: 25 }, + resolve: { + imports: 200, + nativeBatchMs: 2, + jsFallbackMs: 4, + perImportNativeMs: 0, + perImportJsMs: 0, + }, +}; + +let tmpDir: string; +let reportPath: string; +let entryPath: string; + +beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-incr-report-')); + reportPath = path.join(tmpDir, 'INCREMENTAL-BENCHMARKS.md'); + entryPath = path.join(tmpDir, 'entry.json'); + fs.writeFileSync(entryPath, JSON.stringify(SAMPLE_ENTRY)); +}); + +afterEach(() => { + if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true }); +}); + +function runScript() { + execFileSync('node', [stripFlag, scriptPath, entryPath], { + env: { ...process.env, CODEGRAPH_INCREMENTAL_REPORT_PATH: reportPath }, + stdio: 'pipe', + }); +} + +describe('update-incremental-report script', () => { + it('preserves a manual NOTES_START/NOTES_END block across regeneration', () => { + const NOTES = ` +**Note (9.9.8):** Workers hung past the 10-minute timeout and were SIGKILL'd. +`; + const initial = `# Codegraph Incremental Build Benchmarks + +${NOTES} + + +`; + fs.writeFileSync(reportPath, initial); + + runScript(); + + const out = fs.readFileSync(reportPath, 'utf8'); + expect(out).toContain(''); + expect(out).toContain(''); + expect(out).toContain("Workers hung past the 10-minute timeout and were SIGKILL'd"); + // Notes should appear before the data comment, after the latest summary + expect(out.indexOf('')).toBeLessThan( + out.indexOf(' +`; + fs.writeFileSync(reportPath, initial); + + runScript(); + + const out = fs.readFileSync(reportPath, 'utf8'); + expect(out).not.toContain('NOTES_START'); + expect(out).not.toContain('NOTES_END'); + }); +}); From d1623f6a60b490264acedb85d91f3bf327d1ae86 Mon Sep 17 00:00:00 2001 From: carlos-alm Date: Thu, 14 May 2026 23:17:26 -0600 Subject: [PATCH 2/2] fix(scripts): preserve all NOTES blocks via matchAll (#1119) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If multiple NOTES_START/NOTES_END blocks are added to annotate different anomalous releases, .match() silently dropped all but the first — the same data-loss class this PR was created to prevent. Switch to .matchAll() and join with blank lines so every block survives regeneration. Add a test covering two distinct NOTES blocks. --- scripts/update-incremental-report.ts | 9 ++++-- tests/unit/update-incremental-report.test.ts | 31 ++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/scripts/update-incremental-report.ts b/scripts/update-incremental-report.ts index 985db7675..e10ff8e41 100644 --- a/scripts/update-incremental-report.ts +++ b/scripts/update-incremental-report.ts @@ -44,8 +44,13 @@ if (fs.existsSync(reportPath)) { /* start fresh if corrupt */ } } - const notesMatch = content.match(/[\s\S]*?/); - if (notesMatch) notesBlock = notesMatch[0]; + // Use matchAll so multiple NOTES blocks (annotating different anomalous releases) + // are all preserved. The exact data-loss bug this fix targets stemmed from silently + // dropping a NOTES block; we must not reintroduce that failure mode for additional blocks. + const notesMatches = content.matchAll( + /[\s\S]*?/g, + ); + notesBlock = Array.from(notesMatches, (m) => m[0]).join('\n\n'); } // Add new entry — dev entries are rolling, releases replace dev diff --git a/tests/unit/update-incremental-report.test.ts b/tests/unit/update-incremental-report.test.ts index 5b1e98292..7df83cc0a 100644 --- a/tests/unit/update-incremental-report.test.ts +++ b/tests/unit/update-incremental-report.test.ts @@ -87,6 +87,37 @@ ${NOTES} ); }); + it('preserves multiple NOTES_START/NOTES_END blocks across regeneration', () => { + const NOTES_A = ` +**Note (9.9.8):** Workers hung past the 10-minute timeout and were SIGKILL'd. +`; + const NOTES_B = ` +**Note (9.9.7):** Build artifact corrupted during upload, metrics re-run manually. +`; + const initial = `# Codegraph Incremental Build Benchmarks + +${NOTES_A} + +${NOTES_B} + + +`; + fs.writeFileSync(reportPath, initial); + + runScript(); + + const out = fs.readFileSync(reportPath, 'utf8'); + // Both notes must survive regeneration — single-block preservation would + // silently drop the second block (the same data-loss class this PR fixes). + expect(out).toContain("Workers hung past the 10-minute timeout and were SIGKILL'd"); + expect(out).toContain('Build artifact corrupted during upload, metrics re-run manually'); + // Each block keeps its own pair of delimiters. + expect(out.match(//g)?.length).toBe(2); + expect(out.match(//g)?.length).toBe(2); + }); + it('does not invent a NOTES block when none was present', () => { const initial = `# Codegraph Incremental Build Benchmarks