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 %
WalkthroughAdds a Claude skill, a Bash script, and developer documentation for comparing example bundle sizes across two git refs. The script validates inputs, builds required refs, parses size reports, computes deltas, and restores repository state. Changescompare-examples-size Skill and Implementation
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant Maintainer
participant compare_examples_size as compare-examples-size.bash
participant Git
participant npm
participant build_all_examples as build-all-examples.bash
participant Parser as inline Python parser
Maintainer->>compare_examples_size: provide refs and optional size file
compare_examples_size->>Git: resolve refs and order commits
compare_examples_size->>Git: checkout required ref
compare_examples_size->>npm: run npm ci when the lockfile changes
compare_examples_size->>build_all_examples: build examples and capture CSV
compare_examples_size->>Parser: parse size inputs and validate SHAs
Parser->>compare_examples_size: return aligned sizes and deltas
compare_examples_size->>Maintainer: output Markdown comparison table and notes
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
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
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.
|
Address review feedback on the compare-examples-size skill. Signal handling: `cleanup` was bound to EXIT, INT and TERM at once, so a signal ran it twice (the handler's own `exit` fires the EXIT trap in turn) and the run reported status 0, because `$?` inside a signal handler is the status of the last completed command, not the signal. An interrupted comparison was therefore indistinguishable from a successful one for any caller checking the exit code, while printing no table. `cleanup` is now bound to EXIT only and the signal handlers just exit with 130 (INT) and 143 (TERM). Trap ordering: the lock file and the temp dir were created before the trap was installed, so a signal in that window left both behind and made the next run refuse to start with a recovery message, even though nothing had been checked out. The trap is now armed first, and `cleanup` guards against an unset temp dir. The lock-check refusal path still runs before the trap is armed, so a pre-existing lock is never removed by the run that reports it. CSV parsing: `dict(zip(header, values))` truncates to the shorter list, so a header/values length mismatch produced a partial table that still looked trustworthy. Since the whole point of the tool is numbers people paste into a PR, this now fails loudly instead. Blank-line filtering also ignored whitespace-only lines, which would displace a real CSV line out of the `tail -n 2` window. Also renumber the step comments (4 was used twice), document the non-zero exit status on interrupt, describe the lock file as living in the common git dir rather than hardcoding `.git/`, and drop the em dashes from the skill and the website page.
Running the skill for real against main exposed a documentation error. The table reported +82 kB on the
`without-defaults` examples, which reads as a size regression, while the opposite was true: this branch is 19 commits
behind main and simply lacks the tree-shaking reductions main has since gained.
The cause is that column order is normalized by committer timestamp, and the docs promoted that into a chronological
claim ("positive delta = size grew over time"). A timestamp is always defined, but it only implies a before/after
relationship when one ref is an ancestor of the other. For diverged refs, which is the normal case for a feature
branch that has fallen behind its base, the sign says nothing about a progression, so the previous wording invited
exactly the misreading it produced.
Rename the columns from older/newer to earlier/later timestamp, express the delta as `column 2 - column 1`, and add a
"Reading the sign of Delta" section giving the `git merge-base --is-ancestor` check, the rule that diverged refs must
be reported as a difference between two states rather than as growth or a regression, and the merge base as the right
comparison when the intent is to measure a branch's own impact.
Output is unchanged; this is documentation and a code comment only.
Each side of a comparison costs a full core build plus a full examples build, so measuring a ref whose sizes are already known wastes several minutes. Add `--from-sizes <file>` and `--to-sizes <file>`, which take sizes measured earlier for one ref and build only the other one. Both refs are still required, including the one that is not built, because they determine the column labels and the ancestry check. Supplying both sides is refused, since at least one ref must be built. The parser accepts, and auto-detects, the three formats these numbers realistically arrive in: a table printed by a previous run of the script, the markdown table printed by build-all-examples.bash, and its 2-line CSV. Requiring a conversion step would just move the parsing to the caller, where it would be done by hand and inconsistently. A size column is any non-leading column holding at least one parseable number and not being a delta column, which is what lets the deliberately empty `before` column of build-all-examples.bash be skipped without configuration. A previous comparison table holds two size columns, so the script aborts and lists the candidates with their indices rather than guessing; `--sizes-column` then selects one by header substring or by index. Reused numbers are indistinguishable from freshly built ones in the output, so pairing them with the wrong ref would produce a table that looks authoritative and is wrong. Since the script's own tables carry the short SHA in the column header, verify that SHA against the resolved ref and abort naming both commits on a mismatch. The guard only fires when a SHA is present, so the skill still has to confirm the ref with the user; it is a safety net, not a substitute. Classify the two refs with `git merge-base --is-ancestor` in every mode and report the result on stderr before the builds, then again under the table, because it decides whether the sign of the delta means anything chronologically. A `descendant` classification covers the rebase or amended-date case, where the ancestor carries the later timestamp and a positive delta means the bundle shrank as history advanced. The skill instructions gain an analysis step, replacing the previous "just the numbers" rule: lead with the largest absolute change rather than the largest percentage, state what the sign means given the ancestry, and flag provenance and N/A rows. Attributing a delta to a specific commit or dependency stays out of scope unless the diff was actually inspected, since sizes establish that something moved, never why. Relative sizes-file paths are resolved before the `cd` to the repository root, otherwise they would break for every caller not standing at the root.
The examples build was streamed with `tee /dev/stderr > "$raw_out"`, which silently destroys the log whenever stderr is a regular file. /dev/stderr resolves through /proc/self/fd/2 back to the file's own path, so tee reopens it with O_TRUNC and restarts writing at offset 0, discarding everything logged so far, while the shell's own fd 2 keeps its larger offset and leaves NUL padding behind. Redirecting the build log to a file is a documented use of this script, and it lost every checkout message, every `npm ci` line and the whole first ref's build, ending up with the cleanup message stranded in the middle of the file and NUL bytes that make grep treat the log as binary. Swap the two destinations: tee writes the capture file by name and duplicates to the inherited fd 2, which is never reopened, so nothing is truncated. Streaming to a terminal or a pipe is unaffected. The comparison table was never wrong, since it is parsed from the separate capture file, so this only ever corrupted the diagnostics needed to investigate a failed run.
|
There was a problem hiding this comment.
Actionable comments posted: 6
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 0b2fa14c-50dc-4b09-8ad2-0689cd98514d
📒 Files selected for processing (3)
.claude/skills/compare-examples-size/SKILL.md.claude/skills/compare-examples-size/scripts/compare-examples-size.bashpackages/website/docs/development/tools.md
| local raw_out | ||
| raw_out=$(mktemp -t maxgraph-raw-XXXXXX) | ||
| ./scripts/build-all-examples.bash 2>&1 | tee "$raw_out" >&2 | ||
|
|
||
| # Extract the last two non-blank lines, which are the CSV header and CSV values. | ||
| # Whitespace-only lines must be filtered too, otherwise they would displace a real CSV | ||
| # line out of the `tail -n 2` window. | ||
| local last_two | ||
| last_two=$(grep -vE '^[[:space:]]*$' "$raw_out" | tail -n 2) | ||
| echo "$last_two" > "$out_csv" | ||
| rm -f "$raw_out" |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Keep the raw build log in the cleanup directory.
If build-all-examples.bash fails, set -e -o pipefail exits at Line 272 before Line 280 removes raw_out. cleanup removes only TMP_DIR, so the build log remains in the system temporary directory. Create raw_out inside TMP_DIR, or remove it from cleanup.
Proposed fix
- raw_out=$(mktemp -t maxgraph-raw-XXXXXX)
+ raw_out=$(mktemp "$TMP_DIR/raw-XXXXXX")📝 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.
| local raw_out | |
| raw_out=$(mktemp -t maxgraph-raw-XXXXXX) | |
| ./scripts/build-all-examples.bash 2>&1 | tee "$raw_out" >&2 | |
| # Extract the last two non-blank lines, which are the CSV header and CSV values. | |
| # Whitespace-only lines must be filtered too, otherwise they would displace a real CSV | |
| # line out of the `tail -n 2` window. | |
| local last_two | |
| last_two=$(grep -vE '^[[:space:]]*$' "$raw_out" | tail -n 2) | |
| echo "$last_two" > "$out_csv" | |
| rm -f "$raw_out" | |
| local raw_out | |
| raw_out=$(mktemp "$TMP_DIR/raw-XXXXXX") | |
| ./scripts/build-all-examples.bash 2>&1 | tee "$raw_out" >&2 | |
| # Extract the last two non-blank lines, which are the CSV header and CSV values. | |
| # Whitespace-only lines must be filtered too, otherwise they would displace a real CSV | |
| # line out of the `tail -n 2` window. | |
| local last_two | |
| last_two=$(grep -vE '^[[:space:]]*$' "$raw_out" | tail -n 2) | |
| echo "$last_two" > "$out_csv" | |
| rm -f "$raw_out" |
| def load_csv(path, lines): | ||
| """2-line CSV as emitted by build-all-examples.bash: names line, then values line.""" | ||
| if len(lines) < 2: | ||
| die(f"{path}: expected 2 CSV lines (names then values), found {len(lines)}.") | ||
| names = [c.strip() for c in lines[0].split(",")] | ||
| values = [c.strip() for c in lines[1].split(",")] | ||
| # zip() would silently truncate to the shorter list, producing a partial table that | ||
| # still looks trustworthy. Fail loudly instead. | ||
| if len(names) != len(values): | ||
| die( | ||
| f"CSV header/values column count mismatch in {path} " | ||
| f"({len(names)} vs {len(values)})." | ||
| ) | ||
| return {name: parse_size(value) for name, value in zip(names, values)}, None |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject CSV files with extra non-blank rows.
Line 355 accepts any file with at least two rows, but Lines 357-358 parse only the first two rows. A supplied CSV with trailing data is silently accepted and can produce a comparison from incomplete input. Require exactly two non-blank rows for this documented two-line format.
Proposed fix
- if len(lines) < 2:
- die(f"{path}: expected 2 CSV lines (names then values), found {len(lines)}.")
+ if len(lines) != 2:
+ die(f"{path}: expected exactly 2 CSV lines (names then values), found {len(lines)}.")📝 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_csv(path, lines): | |
| """2-line CSV as emitted by build-all-examples.bash: names line, then values line.""" | |
| if len(lines) < 2: | |
| die(f"{path}: expected 2 CSV lines (names then values), found {len(lines)}.") | |
| names = [c.strip() for c in lines[0].split(",")] | |
| values = [c.strip() for c in lines[1].split(",")] | |
| # zip() would silently truncate to the shorter list, producing a partial table that | |
| # still looks trustworthy. Fail loudly instead. | |
| if len(names) != len(values): | |
| die( | |
| f"CSV header/values column count mismatch in {path} " | |
| f"({len(names)} vs {len(values)})." | |
| ) | |
| return {name: parse_size(value) for name, value in zip(names, values)}, None | |
| def load_csv(path, lines): | |
| """2-line CSV as emitted by build-all-examples.bash: names line, then values line.""" | |
| if len(lines) != 2: | |
| die(f"{path}: expected exactly 2 CSV lines (names then values), found {len(lines)}.") | |
| names = [c.strip() for c in lines[0].split(",")] | |
| values = [c.strip() for c in lines[1].split(",")] | |
| # zip() would silently truncate to the shorter list, producing a partial table that | |
| # still looks trustworthy. Fail loudly instead. | |
| if len(names) != len(values): | |
| die( | |
| f"CSV header/values column count mismatch in {path} " | |
| f"({len(names)} vs {len(values)})." | |
| ) | |
| return {name: parse_size(value) for name, value in zip(names, values)}, None |
|
|
||
| The parser auto-detects, so no conversion is needed: | ||
| - the markdown table printed by a previous run of this script (has two size columns, so `--sizes-column` is required); | ||
| - the markdown table printed by `scripts/build-all-examples.bash` (its `before` column is empty by design, so the populated `now` column is detected on its own); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Candidate files ---'
git ls-files \
'.claude/skills/compare-examples-size/SKILL.md' \
'packages/website/docs/development/tools.md' \
'scripts/build-all-examples.bash' \
'*compare*example*' \
'*build*example*'
printf '%s\n' '--- Relevant documentation ---'
for f in .claude/skills/compare-examples-size/SKILL.md packages/website/docs/development/tools.md; do
if [ -f "$f" ]; then
echo "### $f"
sed -n '1,130p' "$f"
fi
done
printf '%s\n' '--- Producer and related parser references ---'
if [ -f scripts/build-all-examples.bash ]; then
nl -ba scripts/build-all-examples.bash | sed -n '1,260p'
fi
rg -n -S -i 'before|now|bundle size|build-all-examples|kB|KB|compare-examples-size' \
.claude packages scripts \
--glob '!**/node_modules/**' \
--glob '!**/dist/**' \
--glob '!**/build/**' \
| head -n 500Repository: maxGraph/maxGraph
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Files matching relevant names ---'
git ls-files | rg '(^|/)(SKILL\.md|tools\.md|build-all-examples\.bash|.*example.*size.*|.*size.*example.*)$' || true
printf '%s\n' '--- Direct file excerpts ---'
for f in .claude/skills/compare-examples-size/SKILL.md packages/website/docs/development/tools.md scripts/build-all-examples.bash; do
echo "### $f"
if [ -f "$f" ]; then
wc -l "$f"
nl -ba "$f" | sed -n '1,260p'
else
echo 'MISSING'
fi
done
printf '%s\n' '--- Relevant references ---'
rg -n -S -i 'before|now|build-all-examples|compare-examples-size|bundle size|kB' \
.claude packages scripts \
--glob '!**/node_modules/**' \
--glob '!**/dist/**' \
--glob '!**/build/**' \
| head -n 800 || trueRepository: maxGraph/maxGraph
Length of output: 50374
🏁 Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
for name in [
".claude/skills/compare-examples-size/SKILL.md",
"packages/website/docs/development/tools.md",
"scripts/build-all-examples.bash",
]:
p = Path(name)
print(f"### {name}")
if not p.exists():
print("MISSING")
continue
lines = p.read_text().splitlines()
for i, line in enumerate(lines, 1):
if name.endswith("SKILL.md") and 65 <= i <= 95:
print(f"{i}: {line}")
elif name.endswith("tools.md") and 25 <= i <= 50:
print(f"{i}: {line}")
elif name.endswith("build-all-examples.bash"):
print(f"{i}: {line}")
PY
printf '%s\n' '--- All exact references ---'
rg -n -S -i 'before|now|build-all-examples|compare-examples-size|bundle size|kB' \
.claude packages scripts \
--glob '!**/node_modules/**' \
--glob '!**/dist/**' \
--glob '!**/build/**' \
|| true
printf '%s\n' '--- Git-tracked generated/example-size artifacts ---'
git ls-files | rg -i '(example|bundle|size|snapshot)' | head -n 500 || trueRepository: maxGraph/maxGraph
Length of output: 50376
🏁 Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- Parser implementation ---'
sed -n '300,415p' .claude/skills/compare-examples-size/scripts/compare-examples-size.bash
printf '%s\n' '--- Parser call sites and table handling ---'
sed -n '240,305p' .claude/skills/compare-examples-size/scripts/compare-examples-size.bash
sed -n '415,525p' .claude/skills/compare-examples-size/scripts/compare-examples-size.bash
printf '%s\n' '--- Focused source facts ---'
grep -nE 'TRAILING_UNIT|def parse_value|def parse_markdown|before|now|parse_value\(' \
.claude/skills/compare-examples-size/scripts/compare-examples-size.bashRepository: maxGraph/maxGraph
Length of output: 12469
🏁 Script executed:
#!/bin/bash
set -eu
sed -n '315,410p' .claude/skills/compare-examples-size/scripts/compare-examples-size.bashRepository: maxGraph/maxGraph
Length of output: 3688
Document the literal kB marker in the before column. The producer emits kB, not an empty cell. The parser treats kB as missing and reads the numeric now value. Update both documentation entries to describe this format.
🧰 Tools
🪛 LanguageTool
[uncategorized] ~80-~80: Did you mean the formatting language “Markdown” (= proper noun)?
Context: ...so --sizes-column is required); - the markdown table printed by `scripts/build-all-exa...
(MARKDOWN_NNP)
📍 Affects 2 files
.claude/skills/compare-examples-size/SKILL.md#L80-L80(this comment)packages/website/docs/development/tools.md#L37-L37
| .claude/skills/compare-examples-size/scripts/compare-examples-size.bash main v0.23.0 | ||
| ``` | ||
|
|
||
| For each of the two references, the script checks out the revision, runs `npm ci`, builds `@maxgraph/core`, and runs `scripts/build-all-examples.bash` to capture the bundle sizes. It then prints a Markdown table to stdout (build logs go to stderr) with one row per example and the following columns: example name, bundle size at the revision in column 1, bundle size at the revision in column 2, delta in kB, and delta in %. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Document conditional dependency installation.
Line [74] says npm ci runs for each ref. .claude/skills/compare-examples-size/SKILL.md Line [51] says it runs for the first built ref and when package-lock.json differs from the previous ref. Update this sentence to match the documented script behavior.
|
|
||
| For each of the two references, the script checks out the revision, runs `npm ci`, builds `@maxgraph/core`, and runs `scripts/build-all-examples.bash` to capture the bundle sizes. It then prints a Markdown table to stdout (build logs go to stderr) with one row per example and the following columns: example name, bundle size at the revision in column 1, bundle size at the revision in column 2, delta in kB, and delta in %. | ||
|
|
||
| The column order is normalized by the commit date so that the revision with the earlier commit date is always in column 1 and the later one in column 2, regardless of the argument order. The delta is therefore always `column 2 − column 1`. |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use the exact timestamp field.
Line [76] says “commit date”. The skill contract uses the committer timestamp. Author and committer dates can differ and can change the normalized column order. Use “committer timestamp”.
|
|
||
| Below the table, the script prints a short note stating how the two revisions are related, as computed by `git merge-base --is-ancestor`. This matters when reading the sign of the delta: it is chronological only when one revision is an ancestor of the other. Two revisions that have diverged, which is the usual case for a feature branch that has fallen behind its base, produce a delta that is merely the difference between two independent states. A branch behind its base shows a positive delta simply because it lacks the size reductions the base has since gained. | ||
|
|
||
| The working tree must be clean before running. The original branch is restored automatically at the end of the run, including on Ctrl-C or build failure. |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Document restoration of the original ref.
Line [80] says “original branch”. The skill also restores the original SHA when the run starts in detached HEAD. Use “original ref” to cover both cases.
| @@ -0,0 +1,105 @@ | |||
| --- | |||
There was a problem hiding this comment.
This is only use for development, this does not explain processes so they are not intended to be read by consumers of the library. So, it should be put out of the public documentation.
So, store this page in the repository (docs folder).







Summary
compare-examples-size, and the bash script behind it, comparing the size of themaxGraphchunk across all examples between two git references (commit SHA, branch, or tag) and printing a Markdown table with deltas in kB and %.--from-sizes/--to-sizes), in which case only the other reference is built. This halves the runtime whenever the numbers for one side are already known.The script is usable on its own from any shell; the skill is a thin wrapper around it.
Design notes
Safety of the checkout dance
trapon every graceful exit, including build failure and Ctrl-C.cleanupis bound toEXITonly, while the signal handlers justexit. Binding it toEXIT INT TERMruns it twice on a signal, because the handler's ownexitfires theEXITtrap in turn, and reports status 0 for an interrupted run, because$?inside a signal handler is the status of the last completed command rather than the signal. An interrupted run now exits 130 (Ctrl-C) or 143 (SIGTERM), so a caller can tell it apart from a successful comparison.SIGKILL) cannot run the trap, so a recovery lock is written in the common git dir before any checkout. The next invocation refuses to start and prints which ref to restore. The lock check runs before the trap is armed, so a pre-existing lock is never removed by the run that reports it.npm ciis used rather thannpm install, sopackage-lock.jsonis never rewritten across the two checkouts, and it is skipped when the lock file is unchanged between the two refs.Reading the table
column 2 − column 1.git merge-base --is-ancestorasidentical,ancestor,descendant, ordiverged, and reported on stderr before the builds start, then again in a note below the table. This is not cosmetic: the timestamp only implies a chronological before/after when one ref is an ancestor of the other. For diverged refs, which is the normal case for a branch that has fallen behind its base, the same+80 kBmeans "this branch lacks 80 kB of reductions its base already has" rather than "this change added 80 kB". Adescendantclassification catches the rebase or amended-date case, where the ancestor carries the later timestamp.^{commit}, so the short SHAs in the output table are always findable throughgit log.Reusing already-measured sizes
scripts/build-all-examples.bash, and its 2-line CSV. Values parse bare (303.69), with the unit (303.69 kB), or asN/A.beforecolumn ofbuild-all-examples.bashbe skipped without configuration. A previous comparison table holds two size columns, so the script aborts and lists the candidates with their indices rather than guessing;--sizes-columnthen selects one by header substring or by index.Output plumbing
tee "$raw_out" >&2rather thantee /dev/stderr > "$raw_out". The latter destroys the log whenever stderr is a regular file:/dev/stderrresolves through/proc/self/fd/2back to the file's own path, soteereopens it withO_TRUNCand restarts at offset 0, discarding everything logged so far and leaving NUL padding where the shell's own descriptor offset had moved past. The table was never affected, since it is parsed from a separate capture file, but the diagnostics needed to investigate a failed run were.zip.Skill instructions
HEAD, confirms which ref supplied sizes belong to before running, and reports an analysis after the table: largest absolute change first (a large percentage on a small bundle is usually noise), what the sign means given the ancestry, provenance of any reused column, andN/Arows meaning an example was added or removed rather than resized. Attributing a delta to a specific commit or dependency stays out of scope unless the diff was actually inspected, since sizes establish that something moved, never why.Test plan
Verified against a throwaway git repository with stubbed
npmandbuild-all-examples.bash, so the exit paths could be exercised without waiting on real builds:--from-sizesand--to-sizeseach run 1, reusing the correct column.before/nowtable ofbuild-all-examples.bashwith no flag.--sizes-columnsubstring, an out-of-range index,--sizes-columnagainst a CSV, and a SHA that disagrees with the ref all abort before any build, with the candidates listed.descendantvia a child commit with a backdated committer date.SIGTERMmid-run exits 143, runs cleanup exactly once, emits no table, restores the branch, and leaves no lock or temp dir. The pre-fix wiring exits 0 with two cleanup passes.npm ciand build messages in order, ends with the restore line, and contains no NUL bytes.mainvs branch tip), producing a correct table, restoring the branch, leaving the tree clean and no lock behind.Left for the reviewer:
SKILL.mdtrigger description picks up natural phrasings such as "compare bundle sizes between main and my-branch" and "here is my previous table, compare it with my branch".Summary by CodeRabbit
Documentation
Chores