{"_id":"@ahmednaguib/driftwatch","_rev":"6-84724ec2a3d3216f1c9629cc411ae48c","name":"@ahmednaguib/driftwatch","dist-tags":{"latest":"0.6.5"},"versions":{"0.6.0":{"name":"@ahmednaguib/driftwatch","version":"0.6.0","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"license":"Apache-2.0","_id":"@ahmednaguib/driftwatch@0.6.0","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"bin":{"driftwatch":"dist/cli/index.js"},"dist":{"shasum":"254ef86fda1fd6fff2d3d770e77afd0cfeebdb53","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.0.tgz","fileCount":316,"integrity":"sha512-GsKWO1QYtbGWoyvZWt35fiD5ChhcjkHAelkSrhnNY/Cja1J5HV4kXnDumHUZhZBjvLdD/dlQ4hxegFzgCdQf4A==","signatures":[{"sig":"MEUCIQCmIYiFJGUtMp8pXWCuvVVqb7oUVFXxGgChDfC+07gdKQIgWayF0kxwzNHxC0xfb5Y2g3h4yH/HQBbqDfynwDR3Tsg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":829611},"main":"./dist/core/index.js","type":"module","types":"./dist/core/index.d.ts","engines":{"node":">=20"},"exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"gitHead":"8b189b3b833a566e87007db45ffcc423a75b5aac","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","demo:movement":"npm run build && node scripts/movement-demo.mjs","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"repository":{"url":"git+https://github.com/AhmedNaguib20/Driftwatch.git","type":"git"},"_npmVersion":"10.8.2","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","directories":{},"_nodeVersion":"20.20.0","dependencies":{"yaml":"^2.6.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","chrome-launcher":"^1.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.17.0","vitest":"^2.1.8","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^20.17.10","typescript-eslint":"^8.18.1"},"_npmOperationalInternal":{"tmp":"tmp/driftwatch_0.6.0_1787669514083_0.6886915145728536","host":"s3://npm-registry-packages-npm-production"}},"0.6.1":{"name":"@ahmednaguib/driftwatch","version":"0.6.1","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"license":"Apache-2.0","_id":"@ahmednaguib/driftwatch@0.6.1","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"bin":{"driftwatch":"dist/cli/index.js"},"dist":{"shasum":"711418fc495452446bd2167cb8b4621686d6aa7a","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.1.tgz","fileCount":316,"integrity":"sha512-RhjdcKqoJvioQyFyEsGxVlkguaunkHr7dy9ie2fpidvIzABRYGOYnrxZesThxLWHoIyt0BmbLBWcjvcoBPC7rg==","signatures":[{"sig":"MEQCICi4M3BojOp7Z24sJDsWr9Y8aa59LUsBGOKHwwnnoOoyAiBpit0/GL9buH03vEl14ebaElPHRmRL077XA0oGcv+AzQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":833421},"main":"./dist/core/index.js","type":"module","types":"./dist/core/index.d.ts","engines":{"node":">=20"},"exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"gitHead":"0f4457fcee916e5bb1364d5501f31114c447f6d1","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","demo:movement":"npm run build && node scripts/movement-demo.mjs","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"repository":{"url":"git+https://github.com/AhmedNaguib20/Driftwatch.git","type":"git"},"_npmVersion":"10.8.2","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","directories":{},"_nodeVersion":"20.20.0","dependencies":{"yaml":"^2.6.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","chrome-launcher":"^1.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.17.0","vitest":"^2.1.8","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^20.17.10","typescript-eslint":"^8.18.1"},"_npmOperationalInternal":{"tmp":"tmp/driftwatch_0.6.1_1787679341404_0.14020402265549148","host":"s3://npm-registry-packages-npm-production"}},"0.6.2":{"name":"@ahmednaguib/driftwatch","version":"0.6.2","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"license":"Apache-2.0","_id":"@ahmednaguib/driftwatch@0.6.2","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"bin":{"driftwatch":"dist/cli/index.js","driftwatch-action":"dist/adapters/github/action-bin.js"},"dist":{"shasum":"dfed2e495ce8039b57902639bfdb08d4ba4318b7","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.2.tgz","fileCount":318,"integrity":"sha512-rVGuIV1aRWL2ukb2ST8Rwcf0+t6yOWKA+S33CyL9BnGpzh0eiYd8Tt/WISdIyf2vovcoh9OnsPmvufapz5IL2w==","signatures":[{"sig":"MEUCIBWuF++TlkNJXSnayNy2m6iX6MiJ09EHfw2MKq2lPfExAiEA6iuJ9/YTla0SIENaSWBAzwvDcjyfxZfFDnJZyxkDp64=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":836464},"main":"./dist/core/index.js","type":"module","types":"./dist/core/index.d.ts","engines":{"node":">=20"},"exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"gitHead":"593377add4ca759071587d8dec0edfa539194d57","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","demo:movement":"npm run build && node scripts/movement-demo.mjs","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"repository":{"url":"git+https://github.com/AhmedNaguib20/Driftwatch.git","type":"git"},"_npmVersion":"10.8.2","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","directories":{},"_nodeVersion":"20.20.0","dependencies":{"yaml":"^2.6.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","chrome-launcher":"^1.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.17.0","vitest":"^2.1.8","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^20.17.10","typescript-eslint":"^8.18.1"},"_npmOperationalInternal":{"tmp":"tmp/driftwatch_0.6.2_1787680372456_0.40558685117116267","host":"s3://npm-registry-packages-npm-production"}},"0.6.3":{"name":"@ahmednaguib/driftwatch","version":"0.6.3","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"license":"Apache-2.0","_id":"@ahmednaguib/driftwatch@0.6.3","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"bin":{"driftwatch":"dist/cli/index.js","driftwatch-action":"dist/adapters/github/action-bin.js"},"dist":{"shasum":"dd14e21e4a3b1941ec77f567acffdaac6176905b","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.3.tgz","fileCount":321,"integrity":"sha512-wBNZEHvQvt9aAawvx/V+FGDWB5AQ96uzcXETvDzQFGXYiGmPYecrvR/SoP+KnpfhK6E4sGXffFiC/apfhsL7tQ==","signatures":[{"sig":"MEUCIGKkkIPblPa4LPtjSEDlrcgRQor5rvlTnltH2A47Qw+cAiEA+cLVeMAQuL54rlSfG1y/9NqdMxQGVDUGaxj0Vt6MH9U=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":838728},"main":"./dist/core/index.js","type":"module","types":"./dist/core/index.d.ts","engines":{"node":">=20"},"exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"gitHead":"b3562684d46928a98b164092bf8510ae0b1db547","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","demo:movement":"npm run build && node scripts/movement-demo.mjs","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"repository":{"url":"git+https://github.com/AhmedNaguib20/Driftwatch.git","type":"git"},"_npmVersion":"10.8.2","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","directories":{},"_nodeVersion":"20.20.0","dependencies":{"yaml":"^2.6.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","chrome-launcher":"^1.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.17.0","vitest":"^2.1.8","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^20.17.10","typescript-eslint":"^8.18.1"},"_npmOperationalInternal":{"tmp":"tmp/driftwatch_0.6.3_1787716415730_0.0883237626897686","host":"s3://npm-registry-packages-npm-production"}},"0.6.4":{"name":"@ahmednaguib/driftwatch","version":"0.6.4","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"license":"Apache-2.0","_id":"@ahmednaguib/driftwatch@0.6.4","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"bin":{"driftwatch":"dist/cli/index.js","driftwatch-action":"dist/adapters/github/action-bin.js"},"dist":{"shasum":"28ead8dc7421d6f75ffa6031a0de4a21b895fa71","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.4.tgz","fileCount":325,"integrity":"sha512-VWp2fPAvcFZnKsQQWILAAUBsV1tEKRVxCnD/zF2koLMIy6CHUosI8OJ1c2iaTWL89/Op6JE4S6geAn4uvUH5Sg==","signatures":[{"sig":"MEUCIGVItPX9WNFBHkdIAXGh9/Gk47mXfqQ1K3dgrHvQotYpAiEAu68hfwi0mJdq8IsjqqnIuBunlmQ3rPRvgALi1G1ggI0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":850850},"main":"./dist/core/index.js","type":"module","types":"./dist/core/index.d.ts","engines":{"node":">=20"},"exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"gitHead":"95b7befafaf636b265fbacc5e0395098b2a87841","scripts":{"dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","typecheck":"tsc -p tsconfig.json --noEmit","test:watch":"vitest","demo:movement":"npm run build && node scripts/movement-demo.mjs","prepublishOnly":"npm run build && npm test"},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"repository":{"url":"git+https://github.com/AhmedNaguib20/Driftwatch.git","type":"git"},"_npmVersion":"10.8.2","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","directories":{},"_nodeVersion":"20.20.0","dependencies":{"yaml":"^2.6.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","chrome-launcher":"^1.2.1"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.17.0","vitest":"^2.1.8","@eslint/js":"^9.17.0","typescript":"^5.7.2","@types/node":"^20.17.10","typescript-eslint":"^8.18.1"},"_npmOperationalInternal":{"tmp":"tmp/driftwatch_0.6.4_1787719196602_0.24122337411403483","host":"s3://npm-registry-packages-npm-production"}},"0.6.5":{"name":"@ahmednaguib/driftwatch","version":"0.6.5","description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"type":"module","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/AhmedNaguib20/Driftwatch.git"},"homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"engines":{"node":">=20"},"bin":{"driftwatch":"dist/cli/index.js","driftwatch-action":"dist/adapters/github/action-bin.js"},"main":"./dist/core/index.js","exports":{".":"./dist/core/index.js","./report":"./dist/core/report/index.js","./package.json":"./package.json"},"publishConfig":{"access":"public"},"scripts":{"build":"tsc -p tsconfig.build.json","dev":"tsc -p tsconfig.build.json --watch","lint":"eslint .","typecheck":"tsc -p tsconfig.json --noEmit","test":"vitest run","test:watch":"vitest","check":"npm run lint && npm run typecheck && npm run test","prepare":"npm run build","prepublishOnly":"npm run build && npm test","demo:movement":"npm run build && node scripts/movement-demo.mjs"},"dependencies":{"chrome-launcher":"^1.2.1","commander":"^12.1.0","lighthouse":"^12.8.2","picocolors":"^1.1.1","yaml":"^2.6.1"},"devDependencies":{"@eslint/js":"^9.17.0","@types/node":"^20.17.10","eslint":"^9.17.0","typescript":"^5.7.2","typescript-eslint":"^8.18.1","vitest":"^2.1.8"},"_id":"@ahmednaguib/driftwatch@0.6.5","gitHead":"bd5c1f26b25001e0b91a29e8eb7788d65871eee7","types":"./dist/core/index.d.ts","_nodeVersion":"20.20.0","_npmVersion":"10.8.2","dist":{"integrity":"sha512-KvbNsH5EviB8LX5GCYGpdkLfno+bhE7WPr9L/VU1D9eFbeYeMqVHpGEqIjEELAarGSVWUPTPuY1vlk0T/kWxcQ==","shasum":"d67cb71f1d560e8c69371407da4346e680677e24","tarball":"https://registry.npmjs.org/@ahmednaguib/driftwatch/-/driftwatch-0.6.5.tgz","fileCount":325,"unpackedSize":851586,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQDojeoBMp1GPyzewSl1SklQL3cQuE5YLPFsZKpT7et7nQIgLob1G2PmHbH0CMeNZl1dHlkFwUg2OZlYr5MzByU4F6A="}]},"_npmUser":{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"},"directories":{},"maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/driftwatch_0.6.5_1787720032598_0.08451607257049809"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-25T14:51:53.919Z","modified":"2026-08-26T04:53:52.904Z","0.6.0":"2026-08-25T14:51:54.243Z","0.6.1":"2026-08-25T17:35:41.609Z","0.6.2":"2026-08-25T17:52:52.611Z","0.6.3":"2026-08-26T03:53:35.889Z","0.6.4":"2026-08-26T04:39:56.759Z","0.6.5":"2026-08-26T04:53:52.766Z"},"bugs":{"url":"https://github.com/AhmedNaguib20/Driftwatch/issues"},"license":"Apache-2.0","homepage":"https://github.com/AhmedNaguib20/Driftwatch#readme","keywords":["performance","performance-testing","regression-testing","continuous-integration","github-action","benchmark","bundle-size","lighthouse","nextjs","perf-budget"],"repository":{"type":"git","url":"git+https://github.com/AhmedNaguib20/Driftwatch.git"},"description":"Performance regression testing for pull requests — both sides measured under an identical protocol, with optional AI explanation on your own key.","maintainers":[{"name":"ahmednaguib","email":"ahmednaguib76@gmail.com"}],"readme":"# Driftwatch\n\nMeasures your app's performance on every pull request, compares it against the base commit under\nan identical protocol, and — optionally, on your own API key — explains what caused a regression\nand proposes a fix it has already measured.\n\nFree and fully local without any API key. Node 20+, git, and a Next.js project.\n\n---\n\n## 1. What lands on your pull request\n\nThis is a real comment, rendered from\n[`tests/golden/comment-regression-analysed.md`](tests/golden/comment-regression-analysed.md) — the\ngolden file that is the adapter's contract. Its three collapsed `<details>` sections and the\nfooter are omitted here for length; nothing else is changed.\n\n> ### ⚠️ Performance regression detected\n>\n> **build time (cold)** is up +7.2% against baseline `main@c0ffee0`. Threshold is 5%.\n>\n> | Metric | Base | This PR | Change |\n> |---|---|---|---|\n> | build time (cold) | 8.72 s | 9.35 s | **+7.2%** ⬆️ |\n> | client bundle size | 921.0 kB | 921.0 kB | no change |\n> | build output size | 2.20 MB | 2.20 MB | no change |\n> | install time | — | — | skipped — dependencies provided by cloning the existing node_modules |\n> | 4 rows excluded by policy | — | — | prerendered (SSG) |\n> | route /blog/[slug] | — | — | skipped — dynamic segment — no concrete URL to measure |\n>\n> ### Likely cause  `confidence 90% (high)`\n>\n> lib/posts.ts adds a 300-entry archive consumed by generateStaticParams, adding ~300 statically\n> generated pages to the build.\n>\n> **Evidence**\n> - build time (cold) regressed 8724ms → 9350ms (+626ms, +7.18%); samples [11143, 8629, 8724] vs [11810, 9350, 9349]\n> - lib/posts.ts (+25/-1) introduces the archive array that /blog/[slug] statically generates\n>\n> **Suggested fix** (ready diff)\n>\n> ```diff\n> --- a/lib/posts.ts\n> +++ b/lib/posts.ts\n> @@ -1 +1 @@\n> -const ARCHIVE_SIZE = 300\n> +const ARCHIVE_SIZE = 30\n> ```\n\nFour things in that comment are worth noticing, because they are the whole design:\n\n- **The samples are shown**, not just the medians: `[11143, 8629, 8724]` vs `[11810, 9350, 9349]`.\n  You can judge the spread yourself. These are the three timed builds; a warm-up build ran before\n  them and was discarded.\n- **`install time` and four routes were not measured**, and the comment says so instead of leaving\n  gaps. A metric that failed is never silently omitted.\n- **The cause cites its evidence** — a specific file, a specific line count, tied to the specific\n  metric that moved.\n- **The comment updates itself.** One comment per PR, edited in place on every push, never a new\n  one per commit.\n\nEverything above the \"Likely cause\" heading is free and runs with no API key. Everything below it\nis the optional AI tier, on your own key.\n\n---\n\n## 2. Try it in three commands\n\n```bash\ncd your-nextjs-project\nnpx @ahmednaguib/driftwatch run\n```\n\nThat measures your working tree, checks out the base commit into a temporary `git worktree`,\nmeasures that the same way, and prints a table. No key, no signup, no configuration file.\n\nOr install it, after which **the command is just `driftwatch`** — the package is scoped, the binary\nis not. (The unscoped name `driftwatch` is blocked by npm's similarity filter, which rejects it as\ntoo close to an unrelated package called `drift-watch`. The scope is the package's address, not a\ndifferent tool.)\n\n```bash\nnpm i -D @ahmednaguib/driftwatch\ndriftwatch run --json      # the full result JSON — the contract every surface consumes\ndriftwatch init --github   # writes .github/workflows/driftwatch.yml\n```\n\nWhat it does to your repository on that first run: **nothing**. It writes to `.perf/` and nowhere\nelse, never checks out a branch in your working tree, never stashes, and never creates a config\nfile you did not ask for. That is enforced by a test that asserts `git status --porcelain` is\nbyte-identical before and after a run ([`tests/rule2.test.ts`](tests/rule2.test.ts)).\n\n---\n\n## 3. Why most performance CI cannot be trusted, and what this does instead\n\nThe problem is not that measuring is hard. It is that the same code, measured twice, gives\ndifferent numbers — so a tool that reports a delta without controlling for that is reporting\nnoise with a percentage sign on it.\n\nHere is the measurement that set the design, taken on the fixture in this repository over 5 runs\n(spec §5.1, 2026-08-18):\n\n| Mode | Times | Spread |\n|---|---|---|\n| Warm (`.next` kept) | 6.81s, 6.80s, 6.88s | 1.2% |\n| Cold (`.next` removed) | 8.75s, 8.75s | 0.0% |\n| **Warm vs cold** | | **22% apart** |\n\nEach mode is stable. The difference between them is not. And a fresh `git worktree` checkout of\nyour base commit can never be warm, while your working tree usually is — so the naive comparison\nreports **a 22% regression on identical code**, stably and repeatably, which is the worst kind of\nwrong. It looks like a finding.\n\nDriftwatch clears the build cache on **both** sides before every timed build, because cold is the\nonly state achievable on both, and labels the metric `build time (cold)` so the number is never\nmistaken for the build time you experience day to day.\n\nThat example generalises into the rule the tool is built around:\n\n> **Both sides of a comparison must be measured under an identical, recorded protocol. Where they\n> cannot be identical, both are forced to the state achievable on both. Where even that is\n> impossible, the delta is refused rather than reported.**\n\nIn practice that means:\n\n- **Both sides in the same invocation, minutes apart, on the same machine.** A cached baseline is\n  used only as a *screening* tool: if a cached comparison crosses the noise floor, both sides are\n  re-measured fresh in the same run and only the confirmed result is reported.\n- **Cold builds, median of 3, after a discarded warm-up.** Every run records how many samples, how\n  they were collected, and the raw values.\n- **The protocol is recorded and compared** — Node version, platform, architecture, browser build,\n  CI runner labels, driftwatch's own version. If two sides disagree on any field that could have\n  *caused* the number, there is no delta.\n\n### The tool declining to make a claim\n\nThis is the other half, and it is the part most tools skip. Measuring successfully is not the same\nas being entitled to blame the change in front of you. Rendered from\n[`src/core/report/context.ts`](src/core/report/context.ts), for a base 143 commits and two months\nbehind with a differing lockfile:\n\n> ### 〰️ Measured — but not attributable to this change\n>\n> > **Why this is not called a regression**\n> >\n> > - the base `main` is far behind this branch (143 commits ahead of it; the base commit is\n> >   61 day(s) old), so a delta measures months of other people's work as much as this change.\n> >   This project's branches appear to integrate into `staging` — compare against that instead:\n> >\n> >   `driftwatch run --base staging`\n> >\n> > - the lockfile differs between the two sides, so the two builds resolved different dependency\n> >   trees — any delta includes whatever those packages changed.\n> >\n> > _The measurements above are real. What is withheld is the claim that this change caused them —\n> > the same doctrine as movement vs drift._\n\nThe numbers are still there, in full. What is withheld is the attribution — because against a base\nthat stale, \"your PR caused this\" is a claim the measurement does not support. The same doctrine\nruns through the whole tool: per-commit attribution is licensed only where the instrument earns it\n(deterministic byte counts), and everything else is labelled as a tendency rather than a cause.\n\nThe reasoning behind every one of these numbers is in the repository, not summarised here.\n[`specs/perf-tool-spec.md`](specs/perf-tool-spec.md) is ~1,500 lines of it: the measurements that\nset each threshold, the approaches that were tried and rejected, the six separate occasions this\none rule had to be applied to a situation nobody had anticipated, and the decisions that were\nreversed when the data contradicted them. Read it before you trust a number this tool prints.\n\n---\n\n## 4. What it measures\n\nFour fixed metrics, plus five per-route classes. On this repository's fixture that comes to 17\nmetrics per run; on your project it depends how many routes you have.\n\n| Metric | Unit | Notes |\n|---|---|---|\n| `build_time` | ms | Cold build, median of 3 |\n| `install_time` | ms | Only when dependencies changed; its delta is refused by design (see below) |\n| `client_bundle_size` | bytes | What ships to browsers — the headline byte metric |\n| `build_output_size` | bytes | Everything the build emitted, server code included |\n| `route_latency:<route>` | ms | Request-level, against the booted app. No browser involved |\n| `lcp:<route>` `fcp:<route>` `tbt:<route>` | ms | Lighthouse, against a pinned Chrome |\n| `transfer_size:<route>` | bytes | Lighthouse — bytes a browser actually downloads |\n\nTwo kinds of number, judged differently — the distinction the rest of the tool is built on:\n\n**Byte counts are deterministic.** Measured twice on identical code they differ by a couple of\nbytes. Across the 39 recorded points on this repository's own history, every byte-class run drifts\n**≤0.01% cumulative**. So bytes are exempt from the 2% relative noise floor and gated only by a\n1 KB resolution: a 140 KB regression on a 9.6 MB bundle is 1.42%, and hiding it behind a noise\nrule would make the tool blind to the exact case it was built for.\n\n**Wall-clock is not deterministic**, and no amount of care makes it so. Each timing class carries\nits own resolution, measured rather than guessed:\n\n| Class | Quantum | Why that number |\n|---|---|---|\n| `build_time` | 100 ms | Process spawn jitters 5–10 ms; package managers add tens more |\n| `route_latency` | 5 ms | Observed ±1 ms sequential-fetch noise, ×5 |\n| `lcp` / `fcp` | 25 ms local, 200 ms CI | ≤7 ms across local boots; shared runners swung −9.7%…+17.8% on byte-identical trees |\n| `tbt` | 50 ms local, 100 ms CI | ±2 ms locally; +83% observed on identical code on a runner |\n| byte classes | 1 KB | ±2 bytes observed; ≥1 KB is a real asset change |\n\nThe CI values are larger because the machine is part of the instrument, and a shared runner is a\ncoarser one. Both numbers come from measurements taken during acceptance, not from intuition.\n\n---\n\n## 5. Four things it refuses to do\n\nThese are enforced by tests, not by intention. Each one costs something: refusal 3 is why you\nwill never see an install-time delta, and refusal 4 is why the tool will not tell you which commit\nslowed your build. The cost is deliberate in each case, and named below.\n\n**1. It never touches your working directory.** No `git stash`, no checkout of your branch, no\ndeleting your `.next`, no writing outside `.perf/`. Both sides of a comparison are measured in\ntemporary copies: the base via `git worktree`, your current tree via a filtered copy. An audit\nduring development found three places that violated this — including creating a `perf.yml` in a\nproject that had not asked for one — and all three were removed rather than argued for.\n\n**2. It never reports a number it did not measure.** There are no estimates presented as\nmeasurements and no interpolated values. A metric that failed to collect is marked `skipped` with\nthe reason and, where a remedy is knowable, the exact command to fix it. When nothing could be\nmeasured at all, the comment says exactly that rather than rendering an empty table.\n\n**3. It refuses to compare across mismatched protocols.** If the two sides ran under different\nNode versions, different platforms, different Chrome builds, or different CI runner classes, you\nget the values and no delta. This is the rule that costs the most output — cached-base comparisons\nescalate to a full re-measure, install deltas are permanently `not_comparable` because the base\nside installs cold and the current side warm — and it is the rule that makes the deltas that *do*\nappear worth reading.\n\n**4. It never attributes what it cannot attribute.** Naming a specific commit as the cause of a\nchange is the strongest claim the tool makes, so it is licensed only for deterministic byte\nclasses, in every environment. Wall-clock movements across a time gap are reported and explicitly\nlabelled *not judged* — because local runs drift with thermals and CI runs land on different\nphysical machines. Validated on a 10-commit history with three planted bundle regressions among\nseven innocent commits: **3 of 3 found, 0 of 7 falsely accused** — and you can reproduce that in\nunder ten seconds:\n\n```bash\nnpm run demo:movement\n```\n\nIt builds the history in a temporary directory, replays it, prints the movement report, and scores\nitself against what was planted. If the doctrine ever breaks, it prints `MISMATCH` rather than a\nnumber you would have to take on trust. ([`scripts/movement-demo.mjs`](scripts/movement-demo.mjs))\n\n### What it cannot do\n\nStated here rather than in a footnote:\n\n- **No production monitoring.** This measures your repository in CI and on your machine. It knows\n  nothing about your real users.\n- **No cross-machine comparison.** Numbers measured on your laptop and numbers measured on a CI\n  runner are never compared to each other. They are separate segments, by design.\n- **Timing attribution is not available**, and will not be until instruction counting replaces\n  wall-clock timing (Layer 3 in the spec). Today, \"build time moved at this commit\" is a claim the\n  tool declines to make.\n- **Next.js is what is proven.** The detection layer is framework-agnostic in shape, but Next.js\n  is the only framework with acceptance runs behind it. Anything else is untested.\n\n---\n\n## 6. AI analysis is optional\n\nMeasurement never needs a key. Explaining a regression does, and it runs on your own key with your\nown provider account — driftwatch has no server and no account of its own.\n\n<!-- feature-matrix: generated from src/core/tier.ts; a test fails if these drift apart -->\n\n| Needs no key | Needs your own key |\n| --- | --- |\n| measurement, comparison, verdicts, thresholds | analysis (cause, confidence, evidence, suggested fix) |\n| PR comment, CI check, step summary | verified auto-fix PRs |\n| record, replay, movement report | `driftwatch eval` |\n| trends, dashboard, drift alerting |  |\n\n<!-- /feature-matrix -->\n\nWhen a regression is confirmed and a key is present, two calls happen. Triage reads the diffstat\nand ranks suspects; deep analysis reads the patches of those suspects and names a cause with a\ncalibrated confidence, its evidence, and a fix. Triage never stops the pipeline — a confirmed\nregression always reaches deep analysis, because a model declining to look is not evidence that\nnothing is wrong.\n\n```bash\nexport DRIFTWATCH_API_KEY=<your DeepSeek or OpenAI key>\ndriftwatch doctor          # is the key valid, which model actually serves it, what a run costs\n```\n\nThe key is read from `DRIFTWATCH_API_KEY`, or from a `key_command` in `perf.yml` so a password\nmanager can supply it (`key_command: op read op://vault/ai/key`), or from `DEEPSEEK_API_KEY` /\n`OPENAI_API_KEY` / `ANTHROPIC_API_KEY` if you already have one set. A literal key written into\n`perf.yml` is **refused, not warned about**: that file is committed, so a key in it is already\nshared with everyone who can read the repository.\n\n---\n\n## 7. Fix PRs that were measured before they were proposed\n\nWith `auto_fix: propose` in `perf.yml`, a confirmed regression with a machine-applicable diff gets\none more step: the diff is applied in a fresh temporary copy and **measured**, same protocol as\neverything else, in the same invocation. That produces a three-way comparison — base, your PR, and\nyour PR with the fix — and one of four outcomes:\n\n| Outcome | Meaning | Opens a PR? |\n|---|---|---|\n| `restored` | The metrics came back within the noise radius of base | yes |\n| `partial` | Measurably better, not all the way back | yes |\n| `no-recovery` | The fix did not move the metric | no |\n| `build-broken` | The fix does not build | no |\n\nOnly `restored` and `partial` may open a PR, and the PR body carries the measured numbers. The\nother two are reported in the comment and go no further. The model's own confidence is shown\nbeside the outcome but is never the gate: a 0.7-confidence fix that measurably restores the metric\nis better evidence than a 0.9-confidence fix nobody measured.\n\n---\n\n## 8. Trends, drift alerting, and history replay\n\nPush-time runs append a point to a `perf-data` branch in your own repository — plain JSON, no\nbackend — together with a self-contained dashboard you can serve from GitHub Pages. This\nrepository's own is live at **<https://ahmednaguib20.github.io/Driftwatch/>**: one HTML file, no\nexecutable JavaScript, no network requests, and byte-identical to what `driftwatch dashboard`\nrenders locally from the same branch state.\n\n```bash\ndriftwatch trend            # where has main been going?\ndriftwatch trend --moves    # which commits moved a metric beyond noise\ndriftwatch alerts           # what the recorded history would interrupt someone about\ndriftwatch replay --last 20 # measure the last 20 mainline commits retroactively\n```\n\n**Trends never draw a line across a protocol change.** A Node upgrade, a Chrome bump on the\nrunner, or a driftwatch version change starts a new segment, and no delta is computed across the\nbreak. Which fields count as a break is decided per metric class by what could have *caused* the\nnumber: a Chrome upgrade breaks Lighthouse metrics and leaves build-output byte counts alone,\nbecause Chrome is not an input to your build.\n\n**Drift alerting exists for what pull requests structurally cannot see** — accumulation where\nevery individual step stayed under the threshold. It fires at 10% cumulative within one protocol\nsegment, over at least 5 measured points, only for byte classes, and only when no single commit\ncrossed the PR threshold on its own. That is twice the PR threshold, deliberately: an alert spends\nsomeone's attention, and it exists only for the case a review could not have caught. It fires once\nper condition and then stays quiet until the drift widens by another 10 points, retreats, or the\nprotocol segment breaks.\n\n**Replay** measures the last N mainline commits as they were, so a project gets history on day one\ninstead of after a month of pushes. It estimates the cost and asks before spending it, marks every\nreplayed point as replayed (measurement time is not commit time), and skips commits that no longer\nbuild rather than aborting.\n\n---\n\n## 9. What leaves your machine\n\n<!-- disclosure: generated from src/ai/disclosure.ts — run `UPDATE_README=1 npx vitest run tests/readme.test.ts` -->\n\n**Nothing leaves your machine without an API key.** Measurement, comparison, verdicts, trends,\nthe dashboard and drift alerting are entirely local and always will be — that is the free tier,\nand it does not phone anywhere.\n\nWith a key configured, exactly one thing sends data: **AI analysis of a confirmed regression.**\nIt runs only when a regression was measured, analysis is enabled (no `--no-ai` /\n`DRIFTWATCH_NO_AI=1`), and DRIFTWATCH_API_KEY resolves to a key. `--no-ai` is enforced at the module\nlevel — the AI code is never even loaded — which the test suite proves rather than promises.\n\n### Where it goes\n\n- `provider: deepseek` → DeepSeek (Hangzhou DeepSeek Artificial Intelligence Co., a Chinese company)\n- `provider: openai` → OpenAI (a US company)\n- `provider: anthropic` → Anthropic (a US company)\n\nYour key, your account, your provider's terms. Driftwatch adds no server of its own: there is no\ndriftwatch backend, and no copy of your data is kept anywhere by this tool.\n\n### What is sent\n\nOn an analysed regression, the request contains:\n\n- the verdict this run reached\n- metric values for both sides, with their deltas\n- the raw samples behind every median, so the model can judge the spread\n- both measurement protocols (node, platform, browser, host labels)\n- the detection evidence trail — which file told the tool what\n- a package-level summary of lockfile changes (added/removed/bumped, with versions)\n- a diffstat of every changed file between the base commit and your working tree\n- full patches for the most-relevant changed files, within a fixed token budget\n\n### What is withheld, always\n\nFiles whose **basename** matches any of these have their content withheld. They still appear in\nthe diffstat — the model may know the file changed; it may never see what is in it:\n\n- `.env` and `.env.*`\n- `*.pem`, `*.key`, `*.p12`, `*.pfx`, `*.jks`, `*.keystore`\n- `id_rsa*`, `id_dsa*`, `id_ecdsa*`, `id_ed25519*`\n- `.netrc`\n- `.npmrc` (it often carries auth tokens)\n- any basename containing `credential`\n- `secret` / `secrets` and their extensions\n\nAlso never sent, under any setting or budget:\n\n- your API key, the `key_command`, and that command's output\n- binary file content (detected from content, not extension)\n- raw lockfile patches — only the package-level summary travels\n- absolute paths, and anything outside the diff between the base commit and your working tree\n\n### The receipt\n\nThis section says what driftwatch does. Every run also records a `contextManifest` in the result\nJSON (`--json`) listing each file's fate — `full`, `truncated`, `diffstat-only`, `withheld` or\n`binary` — with reasons and token counts. That is what happened on **your** run, and it is the\nauthoritative answer.\n\n<!-- /disclosure -->\n\n---\n\n## 10. Configuration\n\n`perf.yml` at the repository root is optional. Without it, driftwatch detects the framework, the\npackage manager and the routes, and uses defaults for everything else.\n\n```yaml\ndetect: nextjs              # framework; detected, override only if detection is wrong\napp: null                   # which workspace package to measure (monorepos)\npackage_manager: null       # override when detection has no evidence to go on\nmeasure: []                 # metric ids that count as KEY; empty = build_time + client_bundle_size\nserve: true                 # boot the built app and measure route latency\nbrowser: true               # Lighthouse metrics (needs Chrome)\nverify: true                # measure the AI's suggested fix before showing it\nauto_fix: off               # 'propose' opens a fix PR when a fix measurably restores the metric\nthreshold: 5%               # the line at which a reported delta becomes a verdict\nblock_merge: false          # warn only; set true once you trust the numbers\nbase: main                  # default ref to compare against; --base overrides per run\nprovider: deepseek          # deepseek | openai\nmodel: deepseek-chat\nkey_command: null           # a command whose stdout is the key; output is used, never stored\nmax_cost_per_run: null      # e.g. 0.05 — refuses the analysis rather than exceeding it\n```\n\nThe noise floor (2%) is deliberately **not** configurable: it is a property of what the instrument\ncan resolve, not a preference. The threshold is, because where a real delta becomes a verdict is a\nteam judgement.\n\n---\n\n## 11. CI setup\n\n```bash\ndriftwatch init --github    # writes .github/workflows/driftwatch.yml\n```\n\nThe generated workflow runs on pull requests (compare), on pushes to `main` (record a trend\npoint), and weekly on a schedule (drift alerting, which measures nothing). It needs:\n\n| Permission | Why |\n|---|---|\n| `pull-requests: write` | the self-updating PR comment, and fix PRs when `auto_fix` is on |\n| `contents: write` | pushing trend points to the `perf-data` branch |\n| `checks: write` | the non-blocking check run |\n| `issues: write` | drift alerts open one issue per condition |\n\nTwo things the generated file explains inline, because both cost real debugging time otherwise:\n**Chrome is pinned** to a specific build, since a runner's Chrome moving mid-day is a protocol\nbreak that splits your trend; and `auto_fix: propose` additionally needs a repository setting\n(*Settings → Actions → General → Workflow permissions → \"Allow GitHub Actions to create and\napprove pull requests\"*) that driftwatch documents and never flips for you.\n\nThe check is **neutral, not failing**, when a regression is found, unless you set\n`block_merge: true`. A newly installed tool that blocks merges gets uninstalled rather than fixed.\n\n---\n\n## 12. Cost\n\nMeasurement is free and has no external dependency. Analysis costs whatever your provider charges,\nand driftwatch reports it two ways: a ceiling before you spend anything (`driftwatch doctor`), and\nprojected-beside-actual on every analysed run, so the estimate is audited by reality rather than\ntrusted.\n\nSet a hard limit per run if you want one:\n\n```yaml\nmax_cost_per_run: 0.05\n```\n\nOver that projection, the analysis is **refused** — not truncated, not quietly switched to a\ncheaper model. The measurement and verdict are unaffected; only the explanation is withheld, and\nthe comment says so with both numbers.\n\n**Cumulative spend is not tracked.** Driftwatch does not know your provider bill and will not\npretend to; a running total it never measured belongs in the same category as an estimate\npresented as a measurement.\n\n---\n\n## 13. How this was built\n\nThe design history is in the repository, and it is unusually complete:\n[`specs/perf-tool-spec.md`](specs/perf-tool-spec.md) records every decision, the measurement behind\nit, and the ones that were reversed when data contradicted them. Eleven milestones, each closed\nonly on evidence — a live proof on a real pull request, an eval suite against a live provider,\nor a measured acceptance run — and this launch is the twelfth.\n\nTwo practices from that history are worth borrowing regardless of whether you use this tool:\n\n- **The decision audit.** Every so often, walk the decisions recorded in your spec and check each\n  one still exists in the code. Two had quietly stopped being true — one was never implemented,\n  and one was implemented correctly and then silently voided by an unrelated rename. Both had been\n  \"done\" for months.\n- **Never assert which branch a timed run took.** A test that depends on a timing measurement\n  staying under a threshold is flaky by construction: when it fails, the tool was right and the\n  test was wrong. Assert the policy as a pure function over known inputs, and the plumbing\n  separately.\n\n---\n\n## 14. Requirements and status\n\nNode 20+, git, and a Next.js project. Lighthouse metrics additionally need Chrome; in CI the\ngenerated workflow installs and pins one.\n\n**Status: complete, and running on this repository.** Driftwatch measures its own fixture on every\npush; the trend points are on the [`perf-data`](../../tree/perf-data) branch of this repo, which is\nwhere the \"39 recorded points\" in §4 comes from. Every capability described here has an acceptance\nrun behind it, and the suite is 465 tests including end-to-end runs against real builds.\n\nWhat it has not had is many users. If you hit something this README says does not happen, that is\nworth an issue — and `--json` plus `driftwatch doctor` will usually contain the answer.\n\nLicensed under [Apache-2.0](LICENSE).\n","readmeFilename":"README.md"}