skilldrop-cli 0.16.36 → 0.16.38

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 (93) hide show
  1. package/README.md +1 -1
  2. package/catalogue.json +3 -1
  3. package/model-routing.json +8 -0
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/judge-audit/SKILL.md +157 -0
  6. package/packs/ai-engineering/skills/judge-audit/evals/eval_queries.json +11 -0
  7. package/packs/ai-engineering/skills/judge-audit/evals/evals.json +136 -0
  8. package/packs/ai-engineering/skills/judge-audit/evals/files/1/judgements.csv +321 -0
  9. package/packs/ai-engineering/skills/judge-audit/evals/files/1/notes.md +28 -0
  10. package/packs/ai-engineering/skills/judge-audit/evals/files/2/pairwise.csv +101 -0
  11. package/packs/ai-engineering/skills/judge-audit/evals/files/2/run-notes.md +16 -0
  12. package/packs/ai-engineering/skills/judge-audit/evals/files/4/grades.csv +97 -0
  13. package/packs/ai-engineering/skills/judge-audit/evals/files/5/pairwise_judgements.jsonl +120 -0
  14. package/packs/ai-engineering/skills/judge-audit/evals/files/6/rag-judge/README.md +36 -0
  15. package/packs/ai-engineering/skills/judge-audit/evals/files/6/rag-judge/judgments.csv +301 -0
  16. package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/gold_labels.csv +101 -0
  17. package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/run_1.jsonl +100 -0
  18. package/packs/ai-engineering/skills/judge-audit/evals/files/7/moderation/run_2.jsonl +100 -0
  19. package/packs/ai-engineering/skills/judge-audit/evals/files/8/patches.csv +241 -0
  20. package/packs/ai-engineering/skills/judge-audit/evals/files/9/adequacy_ratings.csv +151 -0
  21. package/packs/ai-engineering/skills/judge-audit/examples/summary-panel.md +70 -0
  22. package/packs/ai-engineering/skills/judge-audit/manifest.json +44 -0
  23. package/packs/ai-engineering/skills/judge-audit/reference.md +176 -0
  24. package/packs/ai-engineering/skills/judge-audit/scripts/judge_stats.py +1735 -0
  25. package/packs/frontend/pack.json +9 -3
  26. package/packs/frontend/skills/bundle-size-review/SKILL.md +114 -0
  27. package/packs/frontend/skills/bundle-size-review/evals/eval_queries.json +12 -0
  28. package/packs/frontend/skills/bundle-size-review/evals/evals.json +108 -0
  29. package/packs/frontend/skills/bundle-size-review/evals/files/1/package.json +28 -0
  30. package/packs/frontend/skills/bundle-size-review/evals/files/1/src/utils/format.js +26 -0
  31. package/packs/frontend/skills/bundle-size-review/evals/files/1/stats.json +1334 -0
  32. package/packs/frontend/skills/bundle-size-review/evals/files/2/PR.md +21 -0
  33. package/packs/frontend/skills/bundle-size-review/evals/files/2/meta-after.json +440 -0
  34. package/packs/frontend/skills/bundle-size-review/evals/files/2/meta-before.json +360 -0
  35. package/packs/frontend/skills/bundle-size-review/evals/files/3/package.json +29 -0
  36. package/packs/frontend/skills/bundle-size-review/evals/files/3/stats.json +1583 -0
  37. package/packs/frontend/skills/bundle-size-review/evals/files/4/src/components/CommentBox.jsx +24 -0
  38. package/packs/frontend/skills/bundle-size-review/evals/files/4/stats/after.json +299 -0
  39. package/packs/frontend/skills/bundle-size-review/evals/files/4/stats/before.json +275 -0
  40. package/packs/frontend/skills/bundle-size-review/evals/files/5/.browserslistrc +3 -0
  41. package/packs/frontend/skills/bundle-size-review/evals/files/5/build/meta.json +371 -0
  42. package/packs/frontend/skills/bundle-size-review/evals/files/5/build.mjs +18 -0
  43. package/packs/frontend/skills/bundle-size-review/evals/files/5/package-lock.json +39 -0
  44. package/packs/frontend/skills/bundle-size-review/evals/files/5/package.json +22 -0
  45. package/packs/frontend/skills/bundle-size-review/evals/files/5/src/components/Icon.jsx +8 -0
  46. package/packs/frontend/skills/bundle-size-review/evals/files/5/src/polyfills.js +7 -0
  47. package/packs/frontend/skills/bundle-size-review/evals/files/5/src/utils/groupRows.js +4 -0
  48. package/packs/frontend/skills/bundle-size-review/evals/files/6/.browserslistrc +3 -0
  49. package/packs/frontend/skills/bundle-size-review/evals/files/6/package.json +25 -0
  50. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/App.tsx +8 -0
  51. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/chartLocales.ts +16 -0
  52. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/index.ts +8 -0
  53. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/messages/de.ts +5 -0
  54. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/messages/en.ts +5 -0
  55. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/i18n/useLocale.ts +14 -0
  56. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/main.tsx +10 -0
  57. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/pages/Home.tsx +14 -0
  58. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/pages/Reports.tsx +31 -0
  59. package/packs/frontend/skills/bundle-size-review/evals/files/6/src/routes.tsx +17 -0
  60. package/packs/frontend/skills/bundle-size-review/evals/files/6/stats.json +1869 -0
  61. package/packs/frontend/skills/bundle-size-review/evals/files/6/vite.config.ts +13 -0
  62. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/package.json +26 -0
  63. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/App.tsx +27 -0
  64. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/pages/Analytics.tsx +10 -0
  65. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/src/pages/Users.tsx +13 -0
  66. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/stats.json +1421 -0
  67. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/backoffice/webpack.config.js +25 -0
  68. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/package.json +27 -0
  69. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/App.tsx +18 -0
  70. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/index.tsx +4 -0
  71. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/pages/Home.tsx +10 -0
  72. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/src/pages/Product.tsx +13 -0
  73. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/stats.json +1496 -0
  74. package/packs/frontend/skills/bundle-size-review/evals/files/7/apps/storefront/webpack.config.js +26 -0
  75. package/packs/frontend/skills/bundle-size-review/evals/files/7/package.json +9 -0
  76. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Button.css +3 -0
  77. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Button.js +14 -0
  78. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/Chart.js +21 -0
  79. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/dist/index.js +22 -0
  80. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/package.json +20 -0
  81. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Button.css +3 -0
  82. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Button.tsx +9 -0
  83. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/Chart.tsx +14 -0
  84. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/src/index.ts +6 -0
  85. package/packs/frontend/skills/bundle-size-review/evals/files/7/packages/ui/tsconfig.json +14 -0
  86. package/packs/frontend/skills/bundle-size-review/examples/inputs/stats-after.json +1358 -0
  87. package/packs/frontend/skills/bundle-size-review/examples/inputs/stats-before.json +1334 -0
  88. package/packs/frontend/skills/bundle-size-review/examples/larkspur-sparkline.md +52 -0
  89. package/packs/frontend/skills/bundle-size-review/manifest.json +52 -0
  90. package/packs/frontend/skills/bundle-size-review/reference.md +147 -0
  91. package/packs/frontend/skills/bundle-size-review/scripts/bundle_stats.py +1633 -0
  92. package/packs/frontend/skills/web-performance-review/SKILL.md +3 -3
  93. package/packs/frontend/skills/web-performance-review/manifest.json +2 -1
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "For frontend engineers and the people who review their pages: read a Lighthouse report for what actually slows the page for real users, and choose how each route is rendered from what it needs.",
2
+ "description": "For frontend engineers and the people who review their pages: read a Lighthouse report for what actually slows the page, find what makes a JavaScript bundle heavy, and choose how each route is rendered from what it needs.",
3
3
  "display_name": "Frontend",
4
4
  "keywords": [
5
5
  "frontend",
@@ -28,9 +28,10 @@
28
28
  "recovery": "If the reply leads with the lab score or a generic checklist, ask for it by name (\"use web-performance-review\") and include the PageSpeed Insights JSON, which carries the field data."
29
29
  },
30
30
  "journey": {
31
- "when": "A page is slow, or you're deciding how a new app's routes should be rendered.",
31
+ "when": "A page is slow, a bundle grew, or you're deciding how a new app's routes should be rendered.",
32
32
  "you_bring": [
33
33
  "a Lighthouse or PageSpeed Insights JSON, with field data if you have it",
34
+ "the bundler's stats file (webpack stats.json, an esbuild metafile or the Vite visualizer JSON)",
34
35
  "the app's routes, how fresh each one's data must be, and the framework"
35
36
  ],
36
37
  "steps": [
@@ -40,6 +41,11 @@
40
41
  "you_get": "A route table: how each page is rendered and cached, what invalidates it, and what it shows when its data source is slow.",
41
42
  "decide": "Which routes change rendering mode first."
42
43
  },
44
+ {
45
+ "skill": "bundle-size-review",
46
+ "you_do": "Hand over the stats file, and the one before the change if a PR made it grow.",
47
+ "you_get": "The compressed JavaScript each entry loads before first render, what pulls in each heavy module, duplicates, and what can safely come out."
48
+ },
43
49
  {
44
50
  "skill": "web-performance-review",
45
51
  "you_do": "Hand over the Lighthouse or PageSpeed Insights JSON, with field data.",
@@ -47,7 +53,7 @@
47
53
  "decide": "Which fix ships first, and how you'll measure it in the field."
48
54
  }
49
55
  ],
50
- "you_end_with": "Pages rendered the way their data needs, and fixes chosen by what real users feel."
56
+ "you_end_with": "Pages rendered the way their data needs, a bundle with nothing heavy in the first load that doesn't need to be, and fixes chosen by what real users feel."
51
57
  },
52
58
  "requires": [
53
59
  "core"
@@ -0,0 +1,114 @@
1
+ ---
2
+ name: bundle-size-review
3
+ description: Find what makes a JavaScript bundle heavy and what can safely come out, from the bundler's stats file (webpack stats.json, an esbuild metafile, or the rollup-plugin-visualizer raw-data JSON that Vite builds use) read together with the repo around it (package.json, .browserslistrc, the lockfile, the bundler config and the source files that import the heavy modules). Gives each entry's first-load JavaScript in the exact bytes the stats record, kept apart from lazy chunks; finds whole-library imports, CommonJS packages that can't be tree-shaken, packages bundled twice and polyfills the browser targets don't need; names the import chain that pulls each module in before suggesting a removal; and, given before and after stats, says what grew, why, whether it lands in the first load and which per-entry budget would have caught it. Use when the user says "why is our bundle so big", "what can we cut from the bundle", "review this stats.json", "why is lodash or moment in our bundle", "the bundle grew in this PR", or "set a bundle size budget".
4
+ ---
5
+
6
+ # bundle-size-review
7
+
8
+ Reads the bundler's own record of what it built, and the repo files that decide what it should have built, and answers two questions: what does the browser download before the first screen, and which of it can come out without breaking something. The stats file and the repo are the evidence; an analyzer treemap and `du -sh node_modules` are not. Page-load performance from a Lighthouse report (LCP, INP, CLS) is `web-performance-review`; choosing how a route is rendered is `rendering-strategy`. Whether to merge a dependency bump is `dependency-upgrade-review`; reviewing the code change itself is `pre-merge-review`.
9
+
10
+ ## How to respond
11
+
12
+ **Four rules come before the steps and outrank them:**
13
+
14
+ - **Answer what was asked, first.** Open with the answer, the decision or the artifact, in plain words. Scores, matrices, frameworks and tags come after it, and anything that doesn't change the answer is cut.
15
+ - **Use only what you were given.** Don't add facts, names, numbers, incidents, history, steps or sections the input doesn't contain. What you need and don't have is left out of the artifact and listed once at the end under "To confirm".
16
+ - **Deliver from what you have.** When the request gives you something to work on, state your assumptions in a line and produce the result. When it gives you nothing to work on, ask for it in one or two plain sentences and say what you will do once you have it.
17
+ - **Write for someone who has never heard of this skill.** No skill names, no paths or scripts from this folder, no internal terms, and nothing about how the run was set up. A next step is one plain sentence at the end that describes the work.
18
+ - **For this skill:** The repo outranks the stats file on what the build is for, and nothing is called missing without having been looked for. Browser targets, installed versions and how a module is imported live in the files next to the stats; read them, cite them, and rank every fix by what the first screen waits for.
19
+
20
+ 1. **Get the stats file, asking once.** One of: webpack `stats.json`, an esbuild metafile, or rollup-plugin-visualizer output with `template: "raw-data"` (Vite). A treemap screenshot is not enough. Ask for the file with the command that makes it:
21
+ ```bash
22
+ npx --no webpack --mode production --json=stats.json
23
+ npx --no esbuild src/main.tsx --bundle --minify --splitting --format=esm --outdir=dist --metafile=meta.json
24
+ ```
25
+ For Vite, the visualizer config is in [`reference.md`](reference.md).
26
+
27
+ 2. **Run the script before reading anything by hand.** It sizes every entry's initial JavaScript and each lazy chunk, lists the largest modules and packages, finds packages installed at more than one path, flags whole-library CommonJS imports, locales and polyfills, prints the chain for each `--why`, and reads the project around the stats file (the stats folder, its parent or grandparent, or `--root`):
28
+ ```bash
29
+ # Claude Code
30
+ python3 "${CLAUDE_SKILL_DIR}/scripts/bundle_stats.py" stats.json --why lodash --budget main=320kB
31
+ # Other IDEs (from the skill folder)
32
+ python3 scripts/bundle_stats.py stats.json --why lodash --budget main=320kB
33
+ ```
34
+ `--compare before.json after.json` diffs two builds; `--dist dist/` gzips the built files so compressed sizes are measured; `--json` for machine output; `-` reads stdin. Exit 1 means a budget failed, 2 bad input. With no script runtime, follow the field-by-field method in [`reference.md`](reference.md) and show the arithmetic.
35
+
36
+ 3. **Read the repo before concluding anything about it.** The script's "Project files" block lists what it found and what is absent: `package.json` (ranges, `browserslist`), `.browserslistrc`, the lockfile, `webpack.config.*` / `vite.config.*` / `build.mjs` (`target`, existing budgets), `tsconfig.json` (`target`), and the source files that import each bundled package. Open the source files named in the chains and the configs it lists, and cite them by name in the reply. A claim that a file does not exist comes only after listing the folder (`ls -a`, a glob), and is worded as what was looked for.
37
+ - ✅ *"`.browserslistrc` targets Safari 15 and Chrome 100, and `vite.config.ts` builds for `safari15`, so four of the five polyfills in `src/setup/polyfills.ts` are dead."*
38
+ - ❌ *"There is no browserslist, so the polyfills can't be judged."* (said without looking; the file was there)
39
+
40
+ 4. **Lead with the exact sizes the stats give, and say which size it is.** Copy the bytes from the stats, never round them into a new figure first. One unit system for the whole reply: bytes, with kB = 1000 bytes for readability (*"288,640 bytes (288.6 kB)"*); no KiB, KB or MB mixed in. Say once what the number is: webpack module `size` is before minification; a webpack asset size is the emitted file, minified and uncompressed; esbuild `bytes` and `bytesInOutput` are post-bundle (minified when `--minify` is on), uncompressed; the visualizer gives rendered length, plus gzip and brotli when enabled. Leave `.map` outputs out of every count.
41
+ - **Compressed sizes are secondary and never invented.** Quote them only when measured (the built files under `--dist`, or the visualizer's gzip/brotli). Otherwise give the command that measures them after a build, in plain words: gzip the entry's files with `gzip -9c` and count the bytes with `wc -c`.
42
+ - ✅ *"The `app` entry is 288,640 bytes of minified JavaScript before compression (esbuild output bytes)."* ❌ *"app is about 92 KB gzipped"* (no built files were given: that number is a guess).
43
+
44
+ 5. **Separate initial from lazy, then rank.** Order: (a) what can come out of or shrink in the initial chunks, (b) duplicates in the initial chunks, (c) lazy chunks last, each named with the route that loads it. ✅ *"monaco-editor is the biggest library (2,104,300 bytes), but only `routes/Editor.tsx`, loaded with `import()`, pulls it in, so it does not delay the first screen."* ❌ *"Replace monaco-editor first, it is the biggest dependency."*
45
+
46
+ 6. **Name why a module is there before suggesting it go.** Give the chain from the entry and whether the app imports it directly or a dependency does. A transitive module does not leave when its line leaves `package.json`. Read the importing source file when it is on disk: `import * as Flags from "worldflags"` read as `Flags[code]` keeps every export, and the fix is explicit imports or a code-to-component map of the ones in use (found with a search for `<Flag code=` when the callers aren't given).
47
+
48
+ 7. **Find what tree-shaking failed to remove.** Patterns, replacements and what to check are in [`reference.md`](reference.md): whole-library CommonJS imports (`import _ from "lodash"`), barrel files and namespace imports, CommonJS packages in initial chunks (a prebuilt `react-dom.production.min.js` is not a finding), side-effectful modules, locale bundles.
49
+ - **Polyfills against the targets:** the script checks each bundled core-js module against the browser targets it found and lists the files that use a still-needed feature. Drop the polyfills every target has natively; keep any feature a target lacks that the code uses, and name the file. ✅ *"Keep `es.array.find-last`: Safari 15 lacks `Array.prototype.findLast` (it arrived in 15.4) and `src/lib/history.ts` calls it; the other four can go (4,210 bytes)."* With no targets in any file, this goes under To confirm.
50
+
51
+ 8. **Make every fix safe for the installed versions.** Read the installed version (lockfile) and declared range (`package.json`) before naming an API, an import path or a version. Never suggest a downgrade, an option or an export the installed major doesn't have, or a flag the tool doesn't take. For a duplicate, name both install paths, each version and who asks for which range (the script reads them from the lockfile; without one, give `npm ls <package>`). Forcing one copy with `overrides` across a major version is a change to test, not a free removal; whether a newer dependent accepts the newer major is checked with network access, never asserted:
52
+ ```bash
53
+ npm view @harbor/calendar versions
54
+ npm view @harbor/calendar@latest dependencies
55
+ ```
56
+ ✅ *"@harbor/calendar 2.3.0 asks for date-fns ^2.29.0 while the app uses 3.6.0, so two copies ship (18,400 + 5,200 bytes). An override to 3.x is a major jump for the calendar; check with `npm view` whether a calendar release accepts date-fns 3 first."* ❌ *"Upgrade the calendar to 3, which uses date-fns 3"* (not in the input) or *"pin date-fns to 2.x"* (downgrades the app).
57
+
58
+ 9. **For before and after, attribute the growth.** Run `--compare`. Lead with the change in each entry's initial JavaScript, then the lazy chunks; name the modules that moved and the chain that brought them; call out anything that moved from lazy to initial. When someone compared totals, show both numbers side by side: the total barely moved because bytes changed place.
59
+
60
+ 10. **Set or check a budget per entry.** The guard is a limit on each entry's initial JavaScript, not on the total. Pick a threshold between the good build and the bad one (the current figure plus about 5%, or the team's target), in the same unit as the tool that enforces it: webpack `performance` with `hints: "error"` and `maxEntrypointSize` counts emitted bytes, minified and uncompressed; size-limit measures compressed size from the built files itself. When the stats carry pre-minification module sizes, say the emitted figure comes from one build before fixing the number. State that a total-size limit misses a lazy-to-initial move and fails on harmless lazy growth.
61
+
62
+ 11. **Reply short, in one message, in this order.**
63
+ - **First line answers the question in the person's words.** ✅ *"Yes, the reviewer is right: echarts moved from the lazy reports chunk into vendors, so the first load grew from 311,282 to 803,674 bytes (+492,392) while the total fell 1,949."* ❌ *"This confirms it."* / *"Let me break this down."*
64
+ - **Then the fixes, numbered by first-load impact:** what it is, the chain, the change, what to check after. Two to four lines each.
65
+ - **One line on lazy chunks** when they matter to the question.
66
+ - **Then the guard:** the per-entry budget that would have caught it, with its threshold.
67
+ - **"To confirm"** for what only the user can answer; omit it when empty. No module tables, no full package lists, no tool output pasted in, no flags or options that don't exist. About 250 words.
68
+
69
+ **Non-interactive runs** (subagent, CI, headless): with no stats file, emit `BLOCKED: need the bundler stats file (webpack --json, esbuild --metafile, or rollup-plugin-visualizer raw-data)`. With only built files or a treemap screenshot, report the per-file sizes and emit `BLOCKED: need the stats file to say what is inside each chunk`. Browser targets absent from every project file (after looking) become `[assumption] browser targets unknown; polyfill findings unconfirmed`; a missing budget becomes `[assumption] budget set at the current initial size plus 5%`.
70
+
71
+ ## Useful references in this skill
72
+
73
+ - [`reference.md`](reference.md): producing stats for each bundler, what each format's sizes mean, the project files read and how, replacements for the usual heavy packages with what to check, duplicates and versions, budgets in CI
74
+ - [`scripts/bundle_stats.py`](scripts/bundle_stats.py): stdlib reader for webpack, esbuild and visualizer stats plus the project around them; bytes throughout, chains, duplicates with lockfile versions, polyfills against targets, `--compare`, `--budget` (exit 1 over budget, 2 bad input)
75
+ - [`examples/larkspur-sparkline.md`](examples/larkspur-sparkline.md): worked example, a PR that moved a chart library from a lazy chunk into the first load while the total stayed flat
76
+
77
+ ## Quality bar
78
+
79
+ - **The first line answers the question** in the person's terms, with the exact byte figures from the stats.
80
+ - **The repo was read:** targets, versions and importing files are cited by file name; nothing is called absent without having been looked for.
81
+ - **One unit system,** bytes with kB = 1000, and the kind of size (module, emitted, post-bundle, compressed) is named once.
82
+ - **Compressed figures are measured or absent,** never estimated into the lead.
83
+ - **Initial and lazy are kept apart,** and a lazy chunk is never ranked above an initial-chunk fix.
84
+ - **Every suggested removal names its import chain** and is valid for the installed version; no downgrades, no APIs the installed major lacks.
85
+ - **Duplicates name both paths, both versions and who asks for which range,** and a major-version override is flagged as a change to test.
86
+ - **A per-entry budget closes the reply,** with a threshold that passes the good build and fails the bad one.
87
+
88
+ ## When to use this skill
89
+
90
+ - ✅ "Why is our bundle so big?" with a webpack, esbuild or Vite stats file
91
+ - ✅ A PR made the bundle grow and someone wants to know what and why, from before and after stats
92
+ - ✅ "Why is lodash / moment / core-js in our bundle?" or "can we drop this package?"
93
+ - ✅ Two versions of a package in the bundle, or one module in several chunks
94
+ - ✅ Setting or checking a bundle size budget per entry in CI
95
+
96
+ ## When NOT to use this skill
97
+
98
+ - ❌ Reading a Lighthouse report for what slows the page (LCP, INP, CLS, render-blocking requests): use `web-performance-review`
99
+ - ❌ Choosing SSR, static or client rendering per route to ship less JavaScript: use `rendering-strategy`
100
+ - ❌ Deciding whether a dependency bump PR is safe to merge: use `dependency-upgrade-review`
101
+ - ❌ Reviewing the application code in a PR for correctness: use `pre-merge-review`
102
+ - ❌ Server or API response time, database queries, or backend memory
103
+
104
+ ## Anti-patterns to avoid
105
+
106
+ - ❌ **"There is no browserslist" without listing the folder.** `.browserslistrc`, `package.json` and the build config's `target` all name targets.
107
+ - ❌ **Leading with estimated gzip numbers.** The stats give exact bytes; lead with those.
108
+ - ❌ **Mixing KB, KiB and MB.** Pick bytes and kB = 1000 and keep them.
109
+ - ❌ **Leading with the biggest package.** The biggest is often behind a route nobody opens on the first screen.
110
+ - ❌ **Adding up every chunk, or counting `.map` files.** The first load is the entry's initial chunks.
111
+ - ❌ **"Remove X" without the chain,** or a fix the installed version can't take.
112
+ - ❌ **Forcing a nested copy to the app's major and calling it safe.**
113
+ - ❌ **A total-size budget as the only guard.** It misses a lazy-to-initial move.
114
+ - ❌ **Openers and leftovers:** "This confirms it", pasted tool output, flags that don't exist.
@@ -0,0 +1,12 @@
1
+ [
2
+ { "query": "why is our webpack bundle so big? stats.json attached", "should_trigger": true },
3
+ { "query": "what can we cut from the Vite bundle, I've got the visualizer raw-data json", "should_trigger": true },
4
+ { "query": "this PR made main.js grow by 80 KB, here's the esbuild metafile from before and after, what grew?", "should_trigger": true },
5
+ { "query": "why is moment in our bundle when nothing imports it?", "should_trigger": true },
6
+ { "query": "we have two versions of date-fns in the bundle, which dependency pulls in the second one?", "should_trigger": true },
7
+ { "query": "set a bundle size budget per entry that CI can check", "should_trigger": true },
8
+ { "query": "here's our Lighthouse report, why is LCP 4.8 seconds on mobile?", "should_trigger": false },
9
+ { "query": "Dependabot wants to bump webpack from 5.88 to 5.94, is the PR safe to merge?", "should_trigger": false },
10
+ { "query": "review my PR that adds the CSV export button before I merge it", "should_trigger": false },
11
+ { "query": "should the product pages be server-rendered or statically generated?", "should_trigger": false }
12
+ ]
@@ -0,0 +1,108 @@
1
+ {
2
+ "skill_name": "bundle-size-review",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "The dispatch dashboard is slow to open on our technicians' phones, and the bundle analyzer treemap shows echarts as the giant block. I ran webpack with --json and attached stats.json, plus package.json and src/utils/format.js. What's making the bundle heavy, and what should we cut first?",
7
+ "assertions": [
8
+ "Opens with the first-load fix, not echarts: lodash is imported whole (import _ from \"lodash\", the CommonJS lodash/lodash.js, 544,098 bytes of webpack module size, before minification) into the initial vendors chunk through src/utils/format.js",
9
+ "Says echarts and zrender sit in the lazy reports chunk that src/routes.js loads with import(), so they do not delay the first screen, and ranks them below the initial-chunk fixes",
10
+ "Measures the main entry's initial JavaScript as the vendors and main chunks, 311,282 bytes of emitted JavaScript (301,412 + 9,870, minified, before compression), in one unit system, and does not present the sum of all chunks as what the first load downloads. It gives no compressed figure as measured: any gzip number is labelled an estimate or replaced by a way to measure it after a build",
11
+ "Finds date-fns installed twice, naming node_modules/date-fns (pulled in by src/utils/format.js) and node_modules/@tidal/daterange-picker/node_modules/date-fns (pulled in by the picker, a CommonJS copy), and proposes aligning the versions so one copy is installed rather than removing date-fns from the app",
12
+ "Gives the lodash fix from the functions format.js actually uses (groupBy, sortBy, round) as per-method imports, lodash-es named imports or native code, and notes from the stats that format.js is the only importer",
13
+ "Does not promise the pre-minification module size as the download saving, and says the saving is confirmed by rebuilding and comparing",
14
+ "Proposes a per-entry budget on the main entry's initial JavaScript, with a threshold in the unit of the tool that enforces it",
15
+ "Does not invent the version of the nested date-fns copy or the picker's required range, which are not in the input, and says how to read them (for example npm ls date-fns)",
16
+ "Keeps the reply lean: the answer first, a short numbered list of fixes, no full module or package table"
17
+ ]
18
+ },
19
+ {
20
+ "id": 2,
21
+ "prompt": "PR #318 on our order console failed the bundle-size check; PR.md has the CI output and the author's comment. meta-before.json and meta-after.json are the esbuild metafiles from main and from the PR branch. Should we raise the limit like the author suggests? What actually grew?",
22
+ "assertions": [
23
+ "Answers first that raising the total limit is the wrong fix, because the change that slows the first load is in the main entry, not the lazy Analytics route",
24
+ "Attributes the first-load growth to papaparse (19,480 bytes) and file-saver (2,690 bytes) imported statically by src/components/ExportButton.tsx through src/components/Toolbar.tsx, 23,344 bytes added to the main entry's initial JavaScript (159,050 to 182,394)",
25
+ "Attributes the larger growth, 195,230 bytes, to @tidal/heatmap in the Analytics chunk that src/App.tsx loads with import(), and says it does not land in the first load",
26
+ "Recommends loading papaparse and file-saver with a dynamic import when the export button is clicked, and says what to check after (the export still downloads a correct CSV)",
27
+ "Proposes a per-entry budget on the main entry's initial JavaScript (plus, optionally, a separate figure for the Analytics route) and says that budget would have caught this PR's first-load growth while a total-size limit only flagged the lazy growth",
28
+ "Gives no invented compressed sizes: the metafile bytes are minified and uncompressed, and with no built files any gzip figure is labelled an estimate or replaced by a way to measure it",
29
+ "Does not invent load times, Lighthouse scores, user counts or sizes that cannot be computed from the metafiles and PR.md",
30
+ "Keeps the reply short and answer-first, without a table of every chunk and module"
31
+ ]
32
+ },
33
+ {
34
+ "id": 3,
35
+ "prompt": "We're on Vite. stats.json is the rollup-plugin-visualizer raw-data output (gzip and brotli sizes on), and package.json is attached. moment is by far the biggest thing in our bundle. Can we just remove it? And what else is making the first load heavy? We'd like the first load under 60 KB gzipped.",
36
+ "assertions": [
37
+ "Answers first that moment cannot just be removed: the app does not import it; @tidal/react-dates does, through src/pages/BookingCalendar.tsx, so deleting it from package.json would not change the bundle",
38
+ "Says moment (moment-with-locales, 488,140 bytes rendered) is in the lazy BookingCalendar chunk loaded by import() from src/routes.tsx, so it is not part of the first load",
39
+ "Names the way to shrink it (replace the date picker with one built on a smaller date library or a native input, or keep the picker and drop the bundled locales) and what to check (formats, locales and time zones the calendar uses)",
40
+ "Identifies the core-js polyfills from import of core-js/stable in src/main.tsx (58,250 bytes rendered, 21,454 bytes gzipped per the visualizer) in the initial index chunk, and checks them against the browserslist in package.json (Chrome and Edge 111, Firefox 115, Safari 16.4), which have these features natively",
41
+ "Measures the first load as 75,974 bytes gzipped across the index and vendor chunks, from the visualizer's figures, says it is over the 60 KB target and that removing core-js brings it to about 54,500 bytes",
42
+ "Does not present react-dom as something to remove",
43
+ "Does not invent which locales the app needs, the picker's internals, or a moment version beyond what package.json says",
44
+ "Keeps the reply short and answer-first"
45
+ ]
46
+ },
47
+ {
48
+ "id": 4,
49
+ "held_out": "2026-10-20",
50
+ "prompt": "Our size bot flagged PR #482 on the support portal (it adds a Reply box under each ticket on the Home page, src/components/CommentBox.jsx) and says main grew a lot. But I diffed the dist folder totals and it's only about +3 KB, so I think the bot is comparing the wrong thing. Before and after webpack stats are in stats/before.json and stats/after.json. Is the bot wrong? And what should we have had set up to catch this properly?",
51
+ "assertions": [
52
+ "Answers the question directly up front: the bot is right. The initial `main` chunk grew from 163,000 to 342,900 bytes (+179,900, roughly 176-180 KB), while the all-chunks total grew only 3,400 bytes (347,100 to 350,500), which is the +3 KB the user saw.",
53
+ "Explains why the total barely moved: the editor code (src/components/RichText.jsx 3,900 + @inkwell/editor 141,200 + inkwell-model 31,400 = 176,500 bytes) moved out of the lazy `compose` chunk (184,100 down to 7,600) into the initial `main` chunk. The bytes went from lazy-loaded to first load; little new code was added.",
54
+ "Names the import chain from the `reasons` in after.json: src/routes/Home.jsx -> src/components/CommentBox.jsx -> src/components/RichText.jsx -> @inkwell/editor -> inkwell-model. Notes that src/routes/Compose.jsx is still loaded through import(), so the static import from CommentBox is what drags the editor in.",
55
+ "Recommends a fix that keeps the editor out of the first load: load RichText (or the editor) lazily from CommentBox with dynamic import()/React.lazy, for example when Reply is clicked, which CommentBox.jsx's `open` state already gates. Does not suggest removing the editor, and does not suggest lazy-loading the Compose route, which is already lazy.",
56
+ "Proposes a per-entry budget on the `main` entry's initial JavaScript (for example webpack `performance.maxEntrypointSize` with `hints: 'error'`, or a size-limit style CI check on main), not a budget on total dist size. The threshold it gives passes 163,000 and fails 342,900, and it says explicitly that a total-size budget would have missed this PR.",
57
+ "Is accurate about what the numbers are: the stats `size` values are webpack module sizes, not gzip or brotli bytes. Any compressed figure is either labelled an estimate or comes with a command to measure it from the built files (e.g. `gzip -c dist/main.*.js | wc -c`). It does not present invented compressed numbers as measured.",
58
+ "Invents nothing that isn't in the input: no modules, packages, asset sizes, config files or CI tools described as already present beyond what stats/before.json, stats/after.json and CommentBox.jsx show. For example, it does not claim react-dom or client.js changed.",
59
+ "Stays proportionate to a 'was the bot wrong, how do we guard this' question. It stays on this regression and the budget and does not turn into a full bundle audit (e.g. recommending replacing react or react-dom)."
60
+ ]
61
+ },
62
+ {
63
+ "id": 5,
64
+ "held_out": "2026-10-20",
65
+ "prompt": "ops dashboard is really slow to open on the warehouse tablets and someone said the bundle is huge. what can we actually cut without breaking anything? esbuild metafile is build/meta.json, rest of the repo is here",
66
+ "assertions": [
67
+ "Answers the question directly with a ranked list of what can come out of the first load. It treats first-load JS as the entry output dist/assets/main-7QX2KD.js (421,400 bytes), leaves the .js.map outputs out of the count, and keeps the lazily loaded dist/assets/Exports-M4PZ1A.js (215,500 bytes) separate.",
68
+ "Ranks glyphset as the biggest first-load cut (198,400 bytesInOutput, about 47% of main) and traces it to src/components/Icon.jsx, where `import * as Icons from 'glyphset'` plus the `Icons[name]` dynamic lookup stops tree-shaking. The fix it gives is explicit imports (or an explicit name-to-component map) of only the icons actually used, plus a way to list the names in use (e.g. grep for `<Icon name=`), since the route sources aren't provided.",
69
+ "Finds tempora bundled twice: v3.1.0 at node_modules/tempora (ESM, 6,900 bytes in main) and v2.9.1 at node_modules/plotline/node_modules/tempora (CJS, 23,800 bytes, not tree-shaken). It gives the chain src/routes/Overview.jsx -> plotline -> plotline's nested tempora, and notes from package-lock.json that plotline 4.2.0 requires tempora ^2.4.0.",
70
+ "Treats the tempora dedupe with care: forcing v3 on plotline (npm overrides/resolutions) is a major-version jump and not a safe removal without checking. The response gives a command to run with network access (e.g. `npm view plotline versions` / `npm view plotline@latest dependencies`) and does not claim that a plotline version supporting tempora 3 exists.",
71
+ "Checks polyfills against .browserslistrc (chrome >= 110, edge >= 110): es.promise, es.array.flat, es.object.from-entries, es.string.replace-all and es.array.at can come out of src/polyfills.js (about 5,950 bytes). es.object.group-by (610 bytes) must stay, because Object.groupBy only shipped in Chrome/Edge 117 and src/utils/groupRows.js uses it. The alternative is to rewrite groupRows and then drop it.",
72
+ "Does not present sheetsmith (214,300 bytes, the biggest single library) as a first-load cut, because it is only reached through the dynamic import of src/routes/Exports.jsx and sits in the lazy Exports chunk. If it mentions sheetsmith, it says it doesn't affect opening the dashboard.",
73
+ "Is accurate that the metafile bytes are minified, uncompressed sizes, not gzip or brotli. Any compressed figure is labelled an estimate or comes with a command to measure it after a build. It does not suggest cutting code needed at runtime (react, react-dom, plotline itself).",
74
+ "Invents nothing that isn't in the input: no package versions, icon names, sizes, files or browser targets beyond what meta.json, package.json, package-lock.json, .browserslistrc and the src files contain. It delivers the cut list instead of stopping to ask clarifying questions first."
75
+ ]
76
+ },
77
+ {
78
+ "id": 6,
79
+ "held_out": "2026-10-20",
80
+ "prompt": "lighthouse keeps dinging our landing page for unused JS. we moved the charts onto a lazy /reports route back in march so i dont get why index-*.js is still ~460k. someone on the team reckons its the echarts locale files? vite build w/ the visualizer plugin, raw data is in stats.json. what can we cut - the german charts have to keep working tho",
81
+ "assertions": [
82
+ "Names the import chain that keeps ECharts in the entry chunk assets/index-C7qK2mPa.js despite the lazy route: src/main.tsx -> src/i18n/index.ts -> src/i18n/chartLocales.ts -> echarts/core (imported for registerLocale) -> echarts/lib/core/echarts.js and zrender.",
83
+ "Corrects the team's guess with numbers: the six registered locale files (DE, FR, ES, JA, PT-br, KO) are only about 8.2 KB gzip (8,244 B), while ECharts core + zrender + tslib in the entry total about 67.6 KB gzip (67,604 B), close to half the entry's ~147.4 KB gzip (147,431 B, the sum of per-module gzipLength).",
84
+ "Explains that the ~460k figure is the uncompressed size (the entry's renderedLength sums to 465,132 B), gives first-load JavaScript in compressed terms (~147 KB gzip), and keeps the lazy Reports chunk (assets/Reports-Bf41xQ9e.js, ~40.4 KB gzip / 40,414 B) separate from that figure.",
85
+ "Recommends moving the chartLocales import / registerLocale call off the entry path, into src/pages/Reports.tsx or a module only Reports imports, so echarts/core moves into the lazy chunk. Does not recommend dropping ECharts or undoing the lazy route.",
86
+ "Keeps German working by keeping the DE registration (EN ships with ECharts core via langEN). Says FR, ES, JA, PT-br and KO can be dropped because SUPPORTED_LANGUAGES is only ['en', 'de'], and puts that at about 6.9 KB gzip (6,892 B), a minor saving next to the core move.",
87
+ "Gives the expected outcome as an estimate (entry roughly 80 KB gzip after the move: 147,431 - 67,604 = 79,827 B, with React/react-dom/router at ~73 KB as the remaining floor) and says to confirm it by rebuilding (e.g. `npm run build` / `vite build`) and re-reading stats.json, since per-module gzip sums only approximate the gzip of the whole chunk.",
88
+ "Invents nothing that is not in the input: no module sizes, files, chunk names, ECharts APIs or Lighthouse scores beyond what stats.json and the source files contain.",
89
+ "Answers the actual question directly and at a size that fits a casual ask: leads with what is really heavy and the specific cut(s) to make, rather than a module-by-module audit of the whole bundle."
90
+ ]
91
+ },
92
+ {
93
+ "id": 7,
94
+ "held_out": "2026-10-20",
95
+ "prompt": "I lead frontend platform at Quillmark. Both of our apps in the monorepo (apps/storefront and apps/backoffice, webpack 5) consume our shared component library @quillmark/ui from packages/ui. Storefront only uses Button, PriceTag and Modal, yet its main bundle is enormous. I've committed production stats for both: apps/storefront/stats.json and apps/backoffice/stats.json. I need three things: (1) what @quillmark/ui is costing each app on first load and why, (2) the change to packages/ui that fixes it, plus anything that change could break, and (3) per-entry size budgets we can enforce in CI for both apps so this doesn't creep back.",
96
+ "assertions": [
97
+ "Traces the import chain from stats reasons/issuers, e.g. ./src/index.tsx -> ./src/App.tsx -> ./src/pages/Home.tsx -> @quillmark/ui (../../packages/ui/dist/index.js) -> ./Chart -> chart.js/auto -> chart.js/dist/chart.cjs. Names the root cause: packages/ui is compiled to CommonJS (tsconfig \"module\": \"commonjs\", only a \"main\" field, no ESM entry and no \"sideEffects\"), so the barrel's require() calls pull in every component (the stats show the 'Module is not an ECMAScript module' bailout).",
98
+ "For storefront, gives first-load cost from the CompressionPlugin output: main.8c41d2e7.js is 141,206 B gzip (437,914 B raw). Names the unused weight in it: the DataGrid/DatePicker/Chart wrappers and their dependencies @tanstack/table-core, dayjs, chart.js (with helpers.segment.cjs and @kurkle/color).",
99
+ "Treats webpack module `size` values as pre-minification source bytes, not emitted or gzip bytes. For example, it does not claim the chart.js-related modules' 522,298 B (or the 665,654 B of unused modules) are part of a 437,914-byte asset. Any per-dependency compressed saving is labelled an estimate or comes with a way to measure it after a rebuild. It also notes that backoffice's stats contain no gzip sizes (no CompressionPlugin there).",
100
+ "For backoffice, sees that DataGrid/DatePicker (table-core, dayjs) are really used by Users/Orders in main, but Chart/chart.js is used only by the lazy Analytics route. analytics.e71b4d08.js is just 1,206 B because chart.js already sits in main. Says chart.js will move into the analytics chunk only once main's imports stop going through the CommonJS barrel.",
101
+ "The packages/ui fix ships an ES module build (e.g. ESM output via a \"module\" field or an \"exports\" import condition, optionally per-component entry points) and declares side effects while keeping CSS as one (e.g. \"sideEffects\": [\"**/*.css\"]), because dist/Button.js does require(\"./Button.css\"). It warns that a blanket \"sideEffects\": false would silently drop the Button styles.",
102
+ "Budgets are set per entry for each app separately with an explicit unit (gzip vs raw). Notes that webpack's performance.maxEntrypointSize counts uncompressed emitted bytes, that storefront has performance.hints set to false, and that backoffice's 512,000-byte limit did not fire at 451,088 B. Proposes deriving the final numbers from post-fix stats plus headroom, without stating post-fix sizes as measured fact.",
103
+ "Invents nothing that is not in the input: no component names, package versions, file paths, chunk or asset sizes, or webpack/plugin options beyond what the stats and repo files contain. Any tool that would need installing from the network is given as a command, not presented as already run.",
104
+ "Answers all three numbered questions directly, per app, in the order asked."
105
+ ]
106
+ }
107
+ ]
108
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "larkspur-dispatch-web",
3
+ "version": "2.8.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "webpack --mode production",
7
+ "stats": "webpack --mode production --json=stats.json"
8
+ },
9
+ "dependencies": {
10
+ "@tidal/daterange-picker": "^3.4.0",
11
+ "date-fns": "^2.30.0",
12
+ "echarts": "^5.4.3",
13
+ "lodash": "^4.17.21",
14
+ "react": "^18.2.0",
15
+ "react-dom": "^18.2.0"
16
+ },
17
+ "devDependencies": {
18
+ "@babel/core": "^7.24.0",
19
+ "@babel/preset-env": "^7.24.0",
20
+ "@babel/preset-react": "^7.24.0",
21
+ "babel-loader": "^9.1.3",
22
+ "css-loader": "^6.10.0",
23
+ "mini-css-extract-plugin": "^2.8.1",
24
+ "webpack": "^5.91.0",
25
+ "webpack-cli": "^5.1.4"
26
+ },
27
+ "browserslist": ["defaults"]
28
+ }
@@ -0,0 +1,26 @@
1
+ import _ from "lodash";
2
+ import { format, parseISO, isValid } from "date-fns";
3
+
4
+ // Shared formatting helpers for the dispatch board, the header and the report pages.
5
+
6
+ export function formatDay(iso) {
7
+ const d = parseISO(iso);
8
+ return isValid(d) ? format(d, "EEE d MMM") : "—";
9
+ }
10
+
11
+ export function formatTime(iso) {
12
+ const d = parseISO(iso);
13
+ return isValid(d) ? format(d, "HH:mm") : "";
14
+ }
15
+
16
+ export function byTechnician(jobs) {
17
+ return _.groupBy(jobs, "technicianId");
18
+ }
19
+
20
+ export function sortJobs(jobs) {
21
+ return _.sortBy(jobs, ["priority", "dueAt"]);
22
+ }
23
+
24
+ export function money(amount) {
25
+ return _.round(amount, 2).toFixed(2);
26
+ }