chore: add compare-examples-size skill#1074
Conversation
Add a Claude Code skill that compares maxGraph example bundle sizes
between two git references (commit SHA, branch, or tag). Useful to
measure the size impact of a PR, refactor, or release without manually
checking out, building, and tabulating sizes for each ref.
The skill wraps a bash script that:
- refuses to run on a dirty working tree to avoid silent data loss
- writes a recovery lock at .git/compare-examples-size.lock so
SIGKILL-interrupted runs surface explicit restoration instructions
on the next invocation (bash traps cannot run on SIGKILL)
- restores the original ref via a trap on graceful exit, including
on build failure or Ctrl-C
- uses `npm ci` instead of `npm install` so package-lock.json is
never rewritten across the two checkouts
- dereferences annotated tags via `^{commit}` so the short SHAs
shown in the table are findable via `git log` (not tag-object SHAs)
- normalizes column order by committer timestamp: older commit
always in column 1, newer in column 2, regardless of CLI argument
order; Delta is therefore always newer minus older
- builds packages/core then runs scripts/build-all-examples.bash
at each ref and parses its trailing CSV
- emits a markdown table on stdout (build logs go to stderr) with
columns: example name, older kB, newer kB, delta kB, delta %
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
✅ Files skipped from review due to trivial changes (1)
WalkthroughAdds a Claude skill, developer docs, and a new bash script that, given two git refs, checks out each ref, conditionally runs npm install, builds examples to CSV, and aggregates per-example kB and percentage deltas into a markdown table. Changescompare-examples-size Skill
Sequence DiagramsequenceDiagram
participant User
participant Script as compare-examples-size.bash
participant Git
participant npm
participant Builder as build-all-examples.bash
participant Aggregator as Python-formatter
User->>Script: provide two git refs
Script->>Git: resolve refs -> commit SHAs
Script->>Git: checkout older SHA
Script->>npm: conditional npm ci (if package-lock changed)
Script->>Builder: run build-all-examples.bash -> CSV_old
Script->>Git: checkout newer SHA
Script->>npm: conditional npm ci (if package-lock changed)
Script->>Builder: run build-all-examples.bash -> CSV_new
Script->>Aggregator: load CSV_old, CSV_new and compute table
Aggregator->>Script: output markdown table (stdout)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
.claude/skills/compare-examples-size/scripts/compare-examples-size.bash (1)
156-169: ⚖️ Poor tradeoffUpdate last-two-lines parsing: currently OK, still brittle to whitespace/future output
build-all-examples.bashends by printing exactly:
echo "$csv_header"echo "$csv_values"
with no further non-empty stdout output after that, sogrep -v '^$' "$raw_out" | tail -n 2should capture the intended CSV lines today.The remaining fragility is that
grep -v '^$'doesn’t drop whitespace-only lines; if those ever appear after the CSV, thetail -n 2contract breaks. Consider filtering whitespace-only lines (e.g.,grep -vE '^[[:space:]]*$') and/or validating the header line format before writing$out_csv..claude/skills/compare-examples-size/SKILL.md (1)
56-58: 💤 Low valueConsider adding a concrete example row.
The template header clearly shows the column structure, but adding one or two sample data rows would make the format more tangible.
📊 Example enhancement
+| Example | main 8f3b2c1 (kB) | feature/perf a1b2c3d (kB) | Δ kB |Δ % | +|--------------------------|-------------------|----------------------------|--------|---------| +| Hello World | 245.32 | 248.15 | +2.83 | +1.15% | +| Graph Layout | 312.47 | N/A | N/A | N/A | +| Custom Shapes | N/A | 198.76 | N/A | N/A |The existing explanations in lines 60-66 are thorough, so this enhancement is optional.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: af3dca0b-4fd1-4d51-bf2a-1a43e413f880
📒 Files selected for processing (2)
.claude/skills/compare-examples-size/SKILL.md.claude/skills/compare-examples-size/scripts/compare-examples-size.bash
| # 4. Save current ref so we can restore it on exit (branch name preferred, fallback to SHA), | ||
| # then write the lock so a forced kill leaves a recovery breadcrumb (see step 1). | ||
| ORIGINAL_REF=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse HEAD) | ||
| echo "$ORIGINAL_REF" > "$LOCK_FILE" | ||
|
|
||
| # Temp files for CSV captures | ||
| TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX) | ||
| FROM_CSV="$TMP_DIR/from.csv" | ||
| TO_CSV="$TMP_DIR/to.csv" | ||
|
|
||
| cleanup() { | ||
| local status=$? | ||
| echo >&2 | ||
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | ||
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | ||
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | ||
| rm -rf "$TMP_DIR" | ||
| rm -f "$LOCK_FILE" | ||
| exit $status | ||
| } | ||
| trap cleanup EXIT INT TERM |
There was a problem hiding this comment.
Install the cleanup trap before creating side effects.
The lock file (line 104) and TMP_DIR (line 107) are created before trap cleanup EXIT INT TERM is installed on line 121. If SIGINT/SIGTERM arrives in that window (e.g., during mktemp), the shell takes the default action and exits without running cleanup, leaving an orphaned lock file and a temp dir behind — and on next run the user is told a previous run was killed even though nothing was checked out. Move the trap installation to immediately after ORIGINAL_REF is captured, then write the lock and create the temp dir.
🛡️ Proposed reordering
ORIGINAL_REF=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse HEAD)
-echo "$ORIGINAL_REF" > "$LOCK_FILE"
-
-# Temp files for CSV captures
-TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX)
-FROM_CSV="$TMP_DIR/from.csv"
-TO_CSV="$TMP_DIR/to.csv"
+TMP_DIR=""
cleanup() {
local status=$?
+ trap - EXIT INT TERM
echo >&2
echo "Restoring original ref: $ORIGINAL_REF" >&2
git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \
echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2
- rm -rf "$TMP_DIR"
+ [[ -n "$TMP_DIR" ]] && rm -rf "$TMP_DIR"
rm -f "$LOCK_FILE"
exit $status
}
trap cleanup EXIT INT TERM
+
+echo "$ORIGINAL_REF" > "$LOCK_FILE"
+TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX)
+FROM_CSV="$TMP_DIR/from.csv"
+TO_CSV="$TMP_DIR/to.csv"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| # 4. Save current ref so we can restore it on exit (branch name preferred, fallback to SHA), | |
| # then write the lock so a forced kill leaves a recovery breadcrumb (see step 1). | |
| ORIGINAL_REF=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse HEAD) | |
| echo "$ORIGINAL_REF" > "$LOCK_FILE" | |
| # Temp files for CSV captures | |
| TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX) | |
| FROM_CSV="$TMP_DIR/from.csv" | |
| TO_CSV="$TMP_DIR/to.csv" | |
| cleanup() { | |
| local status=$? | |
| echo >&2 | |
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | |
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | |
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | |
| rm -rf "$TMP_DIR" | |
| rm -f "$LOCK_FILE" | |
| exit $status | |
| } | |
| trap cleanup EXIT INT TERM | |
| # 4. Save current ref so we can restore it on exit (branch name preferred, fallback to SHA), | |
| # then write the lock so a forced kill leaves a recovery breadcrumb (see step 1). | |
| ORIGINAL_REF=$(git symbolic-ref --quiet --short HEAD 2>/dev/null || git rev-parse HEAD) | |
| TMP_DIR="" | |
| cleanup() { | |
| local status=$? | |
| trap - EXIT INT TERM | |
| echo >&2 | |
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | |
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | |
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | |
| [[ -n "$TMP_DIR" ]] && rm -rf "$TMP_DIR" | |
| rm -f "$LOCK_FILE" | |
| exit $status | |
| } | |
| trap cleanup EXIT INT TERM | |
| echo "$ORIGINAL_REF" > "$LOCK_FILE" | |
| # Temp files for CSV captures | |
| TMP_DIR=$(mktemp -d -t maxgraph-sizes-XXXXXX) | |
| FROM_CSV="$TMP_DIR/from.csv" | |
| TO_CSV="$TMP_DIR/to.csv" |
| cleanup() { | ||
| local status=$? | ||
| echo >&2 | ||
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | ||
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | ||
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | ||
| rm -rf "$TMP_DIR" | ||
| rm -f "$LOCK_FILE" | ||
| exit $status | ||
| } | ||
| trap cleanup EXIT INT TERM |
There was a problem hiding this comment.
cleanup runs twice on INT/TERM.
With trap cleanup EXIT INT TERM, a SIGINT invokes cleanup, which calls exit $status, which in turn fires the EXIT trap and runs cleanup again. The second pass re-prints "Restoring original ref: …", attempts another checkout (harmless but noisy), and tries to rm -f an already-removed lock. Clear the traps at the top of cleanup to make it idempotent.
♻️ Proposed fix
cleanup() {
local status=$?
+ trap - EXIT INT TERM
echo >&2
echo "Restoring original ref: $ORIGINAL_REF" >&2📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| cleanup() { | |
| local status=$? | |
| echo >&2 | |
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | |
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | |
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | |
| rm -rf "$TMP_DIR" | |
| rm -f "$LOCK_FILE" | |
| exit $status | |
| } | |
| trap cleanup EXIT INT TERM | |
| cleanup() { | |
| local status=$? | |
| trap - EXIT INT TERM | |
| echo >&2 | |
| echo "Restoring original ref: $ORIGINAL_REF" >&2 | |
| git checkout --quiet "$ORIGINAL_REF" 2>/dev/null || \ | |
| echo "Warning: failed to restore $ORIGINAL_REF. Run 'git checkout $ORIGINAL_REF' manually." >&2 | |
| rm -rf "$TMP_DIR" | |
| rm -f "$LOCK_FILE" | |
| exit $status | |
| } | |
| trap cleanup EXIT INT TERM |
| def load(path): | ||
| with open(path) as f: | ||
| lines = [ln.rstrip("\n") for ln in f if ln.strip()] | ||
| header = lines[0].split(",") | ||
| values = lines[1].split(",") | ||
| return dict(zip(header, values)) |
There was a problem hiding this comment.
Silent column mismatch in CSV parsing.
dict(zip(header, values)) silently truncates to the shorter of the two lists. If build-all-examples.bash ever emits a CSV with a header/values length mismatch (or trailing garbage line picked up by tail -n 2), the script will produce a partial table with no warning. A length check before zipping turns this into a loud failure that matches the rest of the script's strict posture.
🛡️ Proposed fix
def load(path):
with open(path) as f:
lines = [ln.rstrip("\n") for ln in f if ln.strip()]
header = lines[0].split(",")
values = lines[1].split(",")
+ if len(header) != len(values):
+ sys.exit(
+ f"Error: CSV header/values column count mismatch in {path} "
+ f"({len(header)} vs {len(values)})."
+ )
return dict(zip(header, values))📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| def load(path): | |
| with open(path) as f: | |
| lines = [ln.rstrip("\n") for ln in f if ln.strip()] | |
| header = lines[0].split(",") | |
| values = lines[1].split(",") | |
| return dict(zip(header, values)) | |
| def load(path): | |
| with open(path) as f: | |
| lines = [ln.rstrip("\n") for ln in f if ln.strip()] | |
| header = lines[0].split(",") | |
| values = lines[1].split(",") | |
| if len(header) != len(values): | |
| sys.exit( | |
| f"Error: CSV header/values column count mismatch in {path} " | |
| f"({len(header)} vs {len(values)})." | |
| ) | |
| return dict(zip(header, values)) |
Document the two internal tools used during development: - `scripts/build-all-examples.bash` to build all examples and report the size of the maxGraph chunk for each one - `compare-examples-size`, available as a Claude Code skill or as a standalone bash script, to compare the maxGraph chunk size between two git revisions (commit SHAs, branches, or tags) The second tool is presented as useful for tracking the evolution of the codebase, enriching pull request descriptions with the bundle-size impact of a change, and supporting release notes by surfacing positive changes or warning about negative impacts.
|



Summary
scripts/build-all-examples.bashat each ref, parses its trailing CSV, and prints a markdown table on stdout (build logs go to stderr).Design notes
trapon graceful exit (build failure, Ctrl-C, etc.)..git/compare-examples-size.lockis written before any checkout. The next invocation refuses to run and prints which ref to restore.npm ci(notnpm install) sopackage-lock.jsonis never rewritten across the two checkouts.^{commit}, so short SHAs in the output table are always findable viagit log.newer − older.Test plan
HEAD vs HEADproduces a table with all Δ = +0.00 kB / +0.00%.main vs v0.23.0produces a table with correct deltas (validated against an ad-hoc reproduction).packages/core+ examples builds).Summary by CodeRabbit
Documentation
Chores