rolldown-perf-kit 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/AGENTS.md +345 -0
  2. package/LICENSE +25 -0
  3. package/README.md +621 -2
  4. package/harness.mjs +4860 -0
  5. package/lib/build.mjs +57 -0
  6. package/lib/cdp.mjs +217 -0
  7. package/lib/chunk-discovery.mjs +226 -0
  8. package/lib/coverage.mjs +498 -0
  9. package/lib/field/analysis.mjs +225 -0
  10. package/lib/field/browser.mjs +232 -0
  11. package/lib/field/build-match.mjs +86 -0
  12. package/lib/field/capture.mjs +501 -0
  13. package/lib/field/config.mjs +84 -0
  14. package/lib/field/run.mjs +612 -0
  15. package/lib/gen-app.mjs +399 -0
  16. package/lib/graph-diff.mjs +136 -0
  17. package/lib/interrupt.mjs +25 -0
  18. package/lib/measure.mjs +540 -0
  19. package/lib/min-cut.mjs +218 -0
  20. package/lib/module-graph.mjs +318 -0
  21. package/lib/network-evidence.mjs +84 -0
  22. package/lib/node/launch.mjs +351 -0
  23. package/lib/node/measure.mjs +289 -0
  24. package/lib/paint-diagnostics.mjs +76 -0
  25. package/lib/parity/anchor.mjs +186 -0
  26. package/lib/parity/capture.mjs +527 -0
  27. package/lib/parity/compare.mjs +813 -0
  28. package/lib/parity/png.mjs +166 -0
  29. package/lib/parity/run.mjs +145 -0
  30. package/lib/profile.mjs +281 -0
  31. package/lib/replay/cert.mjs +45 -0
  32. package/lib/replay/model.mjs +656 -0
  33. package/lib/replay/run.mjs +1140 -0
  34. package/lib/replay/server.mjs +499 -0
  35. package/lib/replay/shaper.mjs +158 -0
  36. package/lib/rolldown-support.mjs +161 -0
  37. package/lib/serve.mjs +142 -0
  38. package/lib/worker/attrib.mjs +125 -0
  39. package/lib/worker/config.mjs +116 -0
  40. package/lib/worker/launch.mjs +337 -0
  41. package/lib/worker/measure.mjs +296 -0
  42. package/package.json +38 -4
  43. package/replay.mjs +79 -0
package/AGENTS.md ADDED
@@ -0,0 +1,345 @@
1
+ # perf-kit — agent contract
2
+
3
+ You are here to make a page load faster and prove it with measurements. This package
4
+ provides the measuring and diagnosis commands; you make the code changes and the
5
+ keep/revert decisions.
6
+
7
+ All commands work from any working directory (`node <this dir>/harness.mjs ...`).
8
+ The first `--app <appDir>` is remembered, so afterwards every command can be run
9
+ bare — no paths. Each target app keeps its own state (baseline, history, reports).
10
+
11
+ Installed from npm, the lab runs as `npx <package name> ...` with no install into the
12
+ project; `help` prints the exact spelling and the path of this file. The state then
13
+ goes to `.perf-kit/` under the directory you run it from: keep that directory out of
14
+ commits. No rolldown checkout sits next to an installed copy, so `rolldown-install`
15
+ needs `--from <a built packages/rolldown>` (see the Worker section).
16
+
17
+ ## The two commands that matter
18
+
19
+ - `node harness.mjs scan --app <appDir>` — one browser session that does it all:
20
+ N throttled timed runs (FCP and LCP vs the pinned baseline), first-paint
21
+ coverage, boot CPU profile — then prints the fused verdict. The FIRST scan of a
22
+ target automatically pins the baseline. After a kept change, `scan --pin` re-pins.
23
+ - `node harness.mjs verdict` — re-print the fused OPEN / clear / UNKNOWN checklist
24
+ for the gathered signals, with staleness tracking against the current build.
25
+ **This is the only "done" that counts**: it refuses to conclude while a lead is
26
+ open or a signal is missing/stale, and even its all-clear states what the tools
27
+ cannot see.
28
+
29
+ Individual commands exist too (`measure [--pin]`, `coverage`, `profile`,
30
+ `baseline`, `target [<appDir>] [--demo]`) plus demo-app helpers (README.md).
31
+
32
+ The throttle is part of the measurement identity. The first scan calibrates a bandwidth
33
+ scale (x1 to x8, with RTT 150 ms and a 4x CPU slowdown) and pins it. To judge the page on
34
+ the network of a deployed environment instead, pin that network:
35
+ `scan --bandwidth <Mbit/s> --rtt <ms> --cpu <rate>`, with the values that a calibrated
36
+ `record` measured (`--cpu 1` = this machine's CPU). A change of either makes the pinned
37
+ baseline incomparable: re-pin on the unchanged build before you judge a change.
38
+
39
+ On rolldown devtools-metrics builds (vite ≥ 8:
40
+ `build.rolldownOptions.devtools = { mode: "report" }`) several static queries skip the
41
+ import-chain archaeology entirely: `graph` ranks split candidates by **retained size**
42
+ (the dominator subtree a deferral removes from the initial load, with `via` naming the
43
+ one import chain to cut); `what-if <module> [<module>...] [--keep a,b]` lists the exact
44
+ modules and bytes one deferral frees (sentries in `--keep` stay eager) — pass **two or
45
+ more** modules and it prices them as one combined plan against the summed-individually
46
+ total, so shared internals dominated by no single module (a library split across imports,
47
+ sibling routes) show up as combined-exceeds-the-sum; `cut <module>` returns the **fewest
48
+ import statements** to make dynamic to detach a module reached by several paths (the
49
+ last-hop edges nearest it, the exact files to edit — a min edge cut, which dominator
50
+ retained-size can't answer once several paths reach the module; `--keep` edges are
51
+ protected; `--root a,b` keeps class modules eager with their closure so the cut lands
52
+ below them; on `what-if` and `cut`, `--root` also takes a module that only a dynamic import
53
+ reaches but the app loads on every start (a boot chain of `import()` calls): it joins the
54
+ entries, so both commands price that startup load); and `graph-diff [--against <file>]`
55
+ reports, deterministically and with no
56
+ browser run, what a change did to the initial load (the modules that ENTERED or LEFT the
57
+ eager set with byte deltas) against the pinned baseline graph or any saved sidecar. Rank
58
+ candidates statically with these, then verify the winner's real LCP effect with `scan`.
59
+ `scan`/`verdict` fold the graph in as the **statically retained imports** signal;
60
+ when the app builds with rolldown but the graph was never collected, the verdict
61
+ reports it UNKNOWN with the one config line that enables it — enable it and rebuild
62
+ rather than justifying the gap: it is the cheapest signal in the whole kit (no
63
+ browser run, and it prices every candidate at once).
64
+
65
+ ## If the target is a server, not a page
66
+
67
+ `node-scan --entry <builtServerScript> --ready <spec>` measures a process coming up
68
+ instead of a page loading. The loop below is unchanged — baseline, one change, measure,
69
+ accept or revert — with three differences that decide whether your work is worth anything:
70
+
71
+ - **Declare readiness once and never change it.** `--ready port:3000` /
72
+ `'stdout:listening on'` / `http://127.0.0.1:3000/health` / `exit`. It is pinned with the
73
+ target; two scans that measured different moments are not comparable, the same way two
74
+ browser scans under different net scales are not.
75
+ - **Read the ownership line before doing any work.** `node-scan` reports pre-ready CPU
76
+ split into app modules / runtime+native / engine. When app modules hold a small share,
77
+ the bundle is not the cost — startup is native addon loading, TLS/crypto init, or a
78
+ DB handshake — and deferring imports will not move the number no matter how good the
79
+ deferral is. Say that and stop, rather than shipping edits that cannot help.
80
+ - **Judge small startups by bytes, not milliseconds.** Readiness polling quantizes; when
81
+ `node-scan` prints the resolution note, the millisecond delta is not evidence and the
82
+ cold-at-ready byte count is.
83
+
84
+ `graph`, `what-if`, `cut` and `graph-diff` work exactly as below on a server build —
85
+ a top-level import and a render-blocking import are the same edge in the module graph.
86
+ Bun is not supported (JavaScriptCore, not V8) and edge runtimes can only be analyzed
87
+ statically; do not report a measured number for either.
88
+
89
+ ## If the target is a Cloudflare Worker
90
+
91
+ `worker-scan --app <appDir>` measures the time a fresh isolate spends in the Worker's
92
+ global scope, under the app's own workerd, and attributes it to modules. Build the app
93
+ first with the config `worker-config` writes (the report on every environment, sourcemaps
94
+ on the worker). The loop below is unchanged, with these differences:
95
+
96
+ - **Startup is the global scope.** `worker.app_startup_ms` is the import of the main module
97
+ and its static closure, timed from the host minus an empty-import floor. Isolate boot
98
+ sits outside it. `worker-scan` against the pinned baseline is the keep-or-revert signal.
99
+ Cloudflare's startup limit counts the same scope; the number to quote in a PR comes
100
+ from `wrangler check startup` on the same build, before and after.
101
+ - **A deferral moves work, it does not delete it.** The first request that needs a deferred
102
+ module pays its load, once per isolate. Pin the routes that matter with `--request` and
103
+ read the "cost MOVED here" line next to the startup gain. The lab does not know the
104
+ route mix; an isolate whose first request needs the module gains nothing.
105
+ - **Durable Object and Workflow classes are roots.** The runtime loads them by name, so their
106
+ modules stay eager with their whole closure. `cut <module> --root <class modules>` lands
107
+ the cut below them; without `--root`, every cut points at the entry's class exports,
108
+ which cannot become dynamic. The scan prints the class names from `wrangler.json`.
109
+ - **Three kinds of edge, three kinds of fix.** `why <module>` names the importers. An importer
110
+ under `node_modules` is upstream: when the library is only used in async code, a lazy shim
111
+ behind a Vite alias is the app-side fix. A module reached only from a synchronous
112
+ constructor or getter cannot be deferred by a dynamic import: say so and move on. A mixed
113
+ `import { type X } from 'm'` keeps a side-effect import of `m` under `verbatimModuleSyntax`:
114
+ change it to `import type`, no dynamic import needed.
115
+ - **Library top-level work is charged by the attribution build.** When the profile leaves
116
+ milliseconds in "(unmapped)" or in zod, run `worker-attrib` on the `ROLLDOWN_PERF_KIT_ATTRIB=1`
117
+ build: it names the module whose evaluation runs the library code, with the import chain.
118
+ - **The report needs a rolldown with the report mode.** The npm release has none, so with it
119
+ no build writes `module-graph.json` and `graph`, `what-if`, `cut`, `why` and `graph-diff`
120
+ have nothing to read. `worker-config` names the app's rolldown when it lacks the mode;
121
+ `rolldown-install --app <appDir>` copies a built package over the copy the app's vite
122
+ resolves: the one next to the lab in a rolldown checkout, or `--from <packages/rolldown>`
123
+ of a checkout built with `just build-rolldown-release` (an installed lab has no package
124
+ next to it, so `--from` is required there). The swap is not a config change;
125
+ `<package manager> install --force` restores the release. Rebuild and re-pin the
126
+ baseline after it: the bundler changed.
127
+ - **Read the two delta lines.** `worker-scan` prints `vs pinned baseline` (every change since
128
+ the pin, added up) and `vs previous scan` (the last change alone). Judge a change by the
129
+ second line, or re-pin with `--pin` after each kept change so the first line means the same.
130
+ - **The pinned routes must answer 2xx.** A 404 or 5xx never ran the handler, so no moved load
131
+ can show on it, and the scan says so. Pin routes the app serves. Do not invent a `.dev.vars`
132
+ to get past a 5xx: without the real variables the first-request numbers stay invalid.
133
+ - **The functional check is the app's own test suite, run the way the repo documents it.** When
134
+ the test pool fails for environment reasons (port exhaustion, `EADDRNOTAVAIL`), run it serially
135
+ or in batches. Never reinstall dependencies inside the loop: that undoes `rolldown-install`,
136
+ and the config shim then refuses to build. Never move or symlink packages inside `node_modules`.
137
+ - **Coverage comes from inside the isolate** and needs Wrangler 4.108.0 or later. When the
138
+ scan reports it unavailable, pass `--tools <dir>` with a newer wrangler for that run, or
139
+ work from the profile and the static graph; do not add the compatibility flags to the app.
140
+ - **Never upload, never deploy, never edit the app's config, variables or bindings to satisfy
141
+ the tool.** A 5xx on the first requests usually means the app needs its `.dev.vars`; startup
142
+ is unaffected, the first-request numbers are not valid until it has them.
143
+
144
+ ## If the lab and production disagree
145
+
146
+ `field --config <file>` measures the DEPLOYED site in the user's own logged-in Chrome
147
+ (README.md, "Field mode"). It does not replace the loop above; it checks where the lab
148
+ cannot see:
149
+
150
+ - **Read the critical path first.** When `after gate` is small and `gate ttfb` is most
151
+ of the LCP, the page waits on the server; only `gate start` (client time before the
152
+ request the LCP waits for) can move with a bundle change. Optimize what delays that
153
+ request, not what renders after it.
154
+ - **Judge by the paired deltas and their 90% intervals,** never by one load or by a
155
+ median of unpaired runs. An interval that crosses 0 is "no measured difference".
156
+ - **Compare `gate - doc`, not only `gate start`,** when the variants come from different
157
+ servers (a preview worker can answer the HTML hundreds of ms slower than production).
158
+ - **Module rows need a build match.** Read `buildMatch` before quoting a module; with no
159
+ match only the chunk-level rows are evidence.
160
+ - Field output contains real account traffic. Keep it where the tool writes it; never
161
+ copy it into a repo, a ticket or a merge request.
162
+
163
+ `record` + `replay` give field-like numbers for a build that is NOT deployed (README.md,
164
+ "Replay"):
165
+
166
+ - **Calibrate before any A/B.** `replay --calibrate` on the recorded build must PASS.
167
+ A FAIL means the model misses something real; find it in `timing.json` (the field
168
+ waterfalls) before you quote a replay delta.
169
+ - **Read the unrecorded requests.** A page request that the recording never saw gets a
170
+ 404; a candidate that adds an API call is then not measured as it would run.
171
+ - **Record again when the network changes.** Chrome's network rating decides which
172
+ requests wait; an archive from a fast network says nothing about a slow one.
173
+ - The archive holds real responses. It stays in `$TMPDIR`; never copy it anywhere.
174
+ - **Two agents: hand over the archive path only.** The recording agent runs
175
+ `node replay.mjs record ... --json` and passes the `archive` path. The replaying
176
+ agent runs `node replay.mjs inspect --archive <dir> --json` first, then
177
+ `replay ... --calibrate --json --exit-code`. Exit 2 or `usable: false` means record
178
+ again; do not edit `archive.json` by hand.
179
+ - **Act on the calibration verdict, not on PASS/FAIL alone.** `record-again` (exit 3,
180
+ `gaps` not empty): record again with a longer `--settle`, at most twice.
181
+ `model-error` (exit 1): the archive is complete; do NOT record again, report the rows
182
+ that are off. Recording again cannot fix a model error.
183
+ - **Keep `--storage warm` for a page that caches data in storage.** The field profile
184
+ has visited the page before; an empty context measures a first visit instead.
185
+ - **Find leads in the field runs, decide with a replay A/B.** `inspect` →
186
+ `lateChunksBeforeLcp` lists static chunks that started more than one recorded RTT
187
+ after a static ancestor finished, in the real field runs. Check in the source whether
188
+ paint needs them before you change a chunk boundary or a preload. Never quote a
189
+ discovery delay as a gain.
190
+
191
+ ## The rendered output must not change
192
+
193
+ The first scan of a target saves the build as the ORIGINAL — run it before you touch
194
+ any code, and pin the client-side routes your work can reach with it:
195
+ `scan --app <appDir> --pages /pricing,/settings` (entry HTML files are found
196
+ automatically; a shared chunk or stylesheet you change for `/` can break another page).
197
+
198
+ - **Optimize freely** — nothing is compared per change. Each scan only says whether the
199
+ LCP element is still the original's; when it is not, LCP is timing a cheaper paint
200
+ (a hidden hero, a placeholder) and that gain is probably not real — find out why
201
+ before you build on it.
202
+ - **When you have finished: `scan --parity`.** It takes the final measurement and
203
+ compares every pinned page with the original: first-screen and full-page pixels, page
204
+ height, visible text, accessible names, new runtime errors, the LCP element. The
205
+ verdict holds it as PENDING until then; you are not done without it.
206
+ - **A difference starts RECOVER MODE**: that build's LCP numbers do not count until it
207
+ matches again — often it was faster BECAUSE it broke (a stylesheet split, reordered or
208
+ made non-blocking without the swap; a font deferred until it never loads). The output
209
+ names what changed — the element and the property or font, the missing text, the new
210
+ error — and points at a diff image. Fix the change or revert it, and choose how you
211
+ re-check: `parity strategy each-tweak` (every scan re-checks and records a snapshot)
212
+ or `on-demand` (you run `parity`; `--only /page` re-checks just the broken pages).
213
+ `parity recover` lists the options, including bisecting your kept commits with
214
+ `git bisect run` and `parity --exit-code`; `parity history` shows every check. A fix
215
+ may give back part of the gain — report the number of the build that matches.
216
+ - **Never** run `parity approve`, `parity anchor --replace`, or add a `--mask`, to make a
217
+ difference go away. Those are human decisions and every verdict lists them. If you
218
+ believe a difference is intended, stop and say so, with the evidence path.
219
+ - UNKNOWN there means the original itself renders differently between two loads (a
220
+ human adds masks) or the LCP element was not compared yet (scan).
221
+
222
+ ## The optimization loop
223
+
224
+ 1. Build the app. `scan --app <appDir>` — the first scan is your baseline and saves
225
+ the original build.
226
+ 2. Read EVERY signal class in the scan output; each is a lead with a next-step:
227
+ - **render-blocking CSS gate** — FCP cannot precede the last render-blocking
228
+ stylesheet; when they land together, CSS is the paint gate and no amount of
229
+ JS work will move it. Fix FIRST, alongside render gap: inline the small
230
+ critical CSS, load the rest non-blocking (preload + media swap), split
231
+ styles only later routes need. (On pages loading many scripts, a plain
232
+ `media="print"` link gets fetch-deprioritized behind them — pair it with
233
+ `rel="preload"`.)
234
+ - **late chunk candidates before FCP or LCP** — inspect
235
+ `runtime-metrics.json` → `chunkDiscovery.fcp.groups` and
236
+ `chunkDiscovery.lcp.groups`. Each check follows static imports that start
237
+ more than one RTT after an ancestor finishes. The download must finish before
238
+ that paint. The check uses `chunk-graph.json` or the current Vite manifest.
239
+ Each example includes an import chain, request timings, and the available
240
+ source module and initiator. Request details also record Chrome's initial
241
+ priority and priority changes. Check them when testing preload priorities.
242
+ Repeated URLs and redirects omit ambiguous network evidence.
243
+ Follow the available source locations to the source. Check whether the
244
+ chunk renders the first visible content or the LCP element. The report marks
245
+ this dependency `unverified`; timing alone cannot prove it. If paint needs
246
+ the chunk, test an earlier preload or a different chunk boundary. If paint
247
+ does not need it, test deferring its load and work. Groups and paint windows
248
+ overlap. Never add their bytes or delays. A delay is not a predicted paint gain.
249
+ - **render gap** — LCP after the load event. Downloads can continue after
250
+ load. Inspect concurrent script, fetch/XHR, font, and image evidence together.
251
+ One candidate does not exclude another. Check which responses rendering
252
+ awaits before changing load order. Test moving optional data reads after
253
+ paint. Keep the LCP image request early. Check which fonts visible text needs.
254
+ Compare the candidates with the CPU profile. When these checks do not
255
+ explain the gap, check whether the LCP element **mounts invisible**: an entry
256
+ animation or fade-in wrapper that starts the hero at opacity 0 delays LCP
257
+ until the reveal — LCP counts the first frame the element paints VISIBLE.
258
+ Render the hero visible immediately and animate only decoration.
259
+ - **pre-paint CPU by module** — the profile maps all sampled same-origin
260
+ chunks through their sourcemaps, including dynamic imports. Inspect source
261
+ rows and their chunk filenames in `profile.json`. Missing maps, unmapped
262
+ positions, inline scripts, external scripts and mixed top-level lines
263
+ (`top-level`) stay unattributed. Do not
264
+ classify a generated filename as engine work. Old entry-only profiles must
265
+ be collected again. The profile stops at FCP observation; its polling tail
266
+ can include post-paint work. It does not cover the FCP-to-LCP interval.
267
+ Sampled time does not prove that work blocks paint or can be deferred.
268
+ Check warm-up caches, telemetry, and data-module evaluation. Defer what the first render does not need —
269
+ but only judge deferrals AFTER any render gap is fixed (CPU that overlaps a
270
+ blocked render is free, so deferring it can measure worse until then).
271
+ - **cold bytes at paint** — the unified byte view: weight fetched+parsed before
272
+ first paint but mostly unread by it, coldest first. Its middle band is the one
273
+ that hides everywhere else: a vendor SDK executing 10–50% at boot (firestore,
274
+ an analytics kit) is neither a "candidate" nor "large executed", yet one
275
+ boot-time init call is dragging the whole package. Defer the CALL, not just
276
+ the import. Framework runtimes (react-dom) are annotated — their cold bytes
277
+ rarely move; don't chase them.
278
+ - **defer candidates** — parsed but ~unexecuted at paint: classic lazy-load targets.
279
+ - **pre-paint sibling chunks** — a non-entry chunk fetched AND executed before
280
+ first paint is critical-path transfer, even if every import of it is dynamic.
281
+ Find what runs it at boot (a top-level or render-time `import()`, an eagerly
282
+ mounted component) and move that trigger to the actual interaction.
283
+ - **static pre-paint transfer** — chunks fetched before first paint (static
284
+ script tags, modulepreloads) but executed only after it. Execution timing
285
+ says "deferred"; the download still competed with the paint for bandwidth,
286
+ and when transfer dominates LCP this is the biggest lever. Load them on
287
+ demand: dynamic import, or drop them from the initial tags/preloads.
288
+ - **large modules executed at paint** — "executed" does NOT mean needed; top-level
289
+ data evaluates on import. Verify how much the first render reads; split the rest
290
+ behind a dynamic import.
291
+ - **sibling variant groups** — locales/themes/config families where one variant is
292
+ active per session: keep the default in the entry, load the active one dynamically.
293
+ - **statically retained imports** (rolldown builds only) — this view starts
294
+ at build entries. It does not price dynamic imports that startup immediately
295
+ executes. Check coverage and CPU evidence for those imports, and pass the modules
296
+ they load as `--root` to `what-if` / `cut` to price that startup load.
297
+ The module graph's
298
+ dominator view: any non-framework module retaining ≥100KB on the initial load
299
+ is a priced split candidate; `what-if` shows the exact cut, `cut` the fewest
300
+ import edits when several paths reach it, and `what-if <a> <b>` the combined
301
+ free when a package repeats across rows. Retained is potential, not proof — the
302
+ first render may genuinely need it; if so, say why.
303
+ 3. Read the app source and find why the landing page pays for each lead.
304
+ 4. Change the app, without removing any feature. One small change at a time — render
305
+ gap first, then data/variant splits, then CPU deferrals.
306
+ 5. Rebuild. Run the app's own functional check (it must pass). `scan --no-early-stop` — or
307
+ `scan --quick` (1 run, no profile) as a cheap probe on slow apps; quick results
308
+ are indicative only. Early-stopped scans are probes too. Check both paint
309
+ metrics with a full scan of at least 3 runs before a keep/revert decision.
310
+ 6. Read `paintComparison` and both entries in `baselineDelta`. At least one of
311
+ FCP or LCP must improve beyond noise. Neither may regress beyond noise.
312
+ The threshold for each metric is max(30ms, 2% of its baseline).
313
+ Both metrics must appear in every run. If these checks and the functional check pass,
314
+ keep the change, `scan --pin` (or `baseline`), and commit. Otherwise, revert
315
+ the change and rebuild. Keep the same content and features.
316
+ (A deferral that measures worse while a render gap exists is worth retrying
317
+ after the gap is fixed.)
318
+ 7. Repeat. When you have finished optimizing, run `scan --parity` (above) and work any
319
+ difference in recover mode. Declare done ONLY when the verdict reports every signal
320
+ class clear and fresh — never because a single report looks empty. One tool's
321
+ silence only means that tool sees nothing; the verdict checks them all and tells
322
+ you which signals you haven't gathered yet.
323
+ 8. Report: baseline and final FCP and LCP, their % changes, entry size before/after, and one
324
+ sentence per kept change — **plus the final verdict checklist, copied verbatim.**
325
+ A re-pinned baseline records a gain; it does NOT close the checklist. If you stop
326
+ while leads are OPEN, say "stopping with N lead(s) OPEN" and justify each one:
327
+ a measurement (you tried it, the delta was sub-noise) or a concrete constraint
328
+ (framework dep, the first paint genuinely needs it, outside your declared scope).
329
+ Never describe OPEN leads as "confirmed done" — misquoting the verdict is the
330
+ one way to fail this task even with a good number.
331
+
332
+ ## Rules
333
+
334
+ - Judge speed ONLY by the `vs pinned baseline` verdict. Never by the chain
335
+ "vs previous measure" line, never by intuition.
336
+ - A faster page that breaks a feature, or renders differently from the original, is
337
+ a failure, not an optimization.
338
+ - Never edit this package, the app's build/check scripts, or any thresholds to move
339
+ the numbers.
340
+ - **The functional check is the contract — never modify it.** If it fails after your
341
+ change, your change is wrong: fix the app or revert. A check you edited proves
342
+ nothing.
343
+ - **Never change the app's build layout (outDir, file names) to satisfy this tool.**
344
+ Aim the tool at the app instead: `--app <appRoot>` resolves dist/build/out
345
+ automatically, `--dist <builtDir>` aims at any output dir directly.
package/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024-present VoidZero Inc. & Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ end of terms and conditions
24
+
25
+ The licenses of externally maintained libraries from which parts of the Software is derived are listed [here](https://github.com/rolldown/rolldown/blob/main/THIRD-PARTY-LICENSE).