Skip to content

chore: add compare-examples-size skill - #1074

Open
redfish4ktc wants to merge 7 commits into
mainfrom
chore/create_claude_skill_compare_examples_size
Open

chore: add compare-examples-size skill#1074
redfish4ktc wants to merge 7 commits into
mainfrom
chore/create_claude_skill_compare_examples_size

Conversation

@redfish4ktc

@redfish4ktc redfish4ktc commented May 21, 2026

Copy link
Copy Markdown
Member

Summary

  • Add a Claude Code skill, compare-examples-size, and the bash script behind it, comparing the size of the maxGraph chunk across all examples between two git references (commit SHA, branch, or tag) and printing a Markdown table with deltas in kB and %.
  • Useful to measure the size impact of a PR, refactor, or release without manually checking out, building, and tabulating sizes for each reference. The table is meant to be pasted into a PR description or release notes.
  • Accept already-measured sizes for one of the two references (--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.
  • Report how the two references are related, so the sign of the delta can be read correctly.
  • Document both entry points (the skill, and the script for contributors who do not use Claude Code) in the website development docs.

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

  • The working tree must be strictly clean. The script refuses to auto-stash, to avoid silent data loss.
  • The original ref is restored by a trap on every graceful exit, including build failure and Ctrl-C.
  • cleanup is bound to EXIT only, while the signal handlers just exit. Binding it to EXIT INT TERM runs it twice on a signal, because the handler's own exit fires the EXIT trap 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.
  • A forced kill (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 ci is used rather than npm install, so package-lock.json is never rewritten across the two checkouts, and it is skipped when the lock file is unchanged between the two refs.

Reading the table

  • Column order is normalized by committer timestamp: earlier commit in column 1, later in column 2, whatever the argument order. The delta is therefore always column 2 − column 1.
  • The relationship between the two refs is classified with git merge-base --is-ancestor as identical, ancestor, descendant, or diverged, 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 kB means "this branch lacks 80 kB of reductions its base already has" rather than "this change added 80 kB". A descendant classification catches the rebase or amended-date case, where the ancestor carries the later timestamp.
  • Annotated tags are dereferenced via ^{commit}, so the short SHAs in the output table are always findable through git log.

Reusing already-measured sizes

  • Both refs are still required, including the one that is not built, since they determine the column labels and the ancestry check. Supplying both sides is refused: at least one ref must be built.
  • Three input formats are accepted and auto-detected, so no conversion step is needed: a table printed by a previous run of the script, the Markdown table printed by scripts/build-all-examples.bash, and its 2-line CSV. Values parse bare (303.69), with the unit (303.69 kB), or as N/A.
  • A size column is any non-leading column that holds at least one parseable number and is not a delta column. That 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 yield a table that looks authoritative and is wrong. Since the script's own tables carry the short SHA in the column header, that SHA is verified against the resolved ref and a mismatch aborts naming both commits. The guard only fires when a SHA is present in the header, so the skill instructions also require confirming the ref with the user.
  • The note below the table records which column was reused and from which file, along with the caveat that reused sizes are only comparable if they were measured with the same toolchain and dependencies.

Output plumbing

  • Build logs go to stderr; stdout carries the table and its note, so it stays redirectable.
  • The examples build is streamed with tee "$raw_out" >&2 rather than tee /dev/stderr > "$raw_out". The latter 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 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.
  • The trailing CSV is located by filtering blank and whitespace-only lines before taking the last two, since a whitespace-only line would otherwise displace a real CSV line out of the window. A header/values column count mismatch aborts instead of being silently truncated by zip.

Skill instructions

  • The skill asks for both refs rather than defaulting to 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, and N/A rows 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 npm and build-all-examples.bash, so the exit paths could be exercised without waiting on real builds:

  • Two-ref mode runs 2 builds; --from-sizes and --to-sizes each run 1, reusing the correct column.
  • All three input formats parse, including the before/now table of build-all-examples.bash with no flag.
  • Ambiguous columns, an unmatched --sizes-column substring, an out-of-range index, --sizes-column against a CSV, and a SHA that disagrees with the ref all abort before any build, with the candidates listed.
  • Argument validation: missing refs, both sides supplied, unknown option, unreadable file, option without a value.
  • All four ancestry classifications, including descendant via a child commit with a backdated committer date.
  • SIGTERM mid-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.
  • With stderr redirected to a file, the log keeps all checkout, npm ci and build messages in order, ends with the restore line, and contains no NUL bytes.
  • Lock refusal path: a pre-existing lock aborts the next run with recovery instructions and is preserved across the refusal.
  • Real end-to-end run on this repository (main vs branch tip), producing a correct table, restoring the branch, leaving the tree clean and no lock behind.

Left for the reviewer:

  • Try the skill end-to-end with two arbitrary refs from a clean tree. Expect several minutes for two full builds, or roughly half that when passing sizes for one side.
  • Confirm the SKILL.md trigger 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

    • Added user-facing docs describing how to run a bundle-size comparison between two revisions, expected CSV/markdown output, prerequisites, and failure-handling guidance.
  • Chores

    • Added a command-line comparison tool that captures example bundle sizes for two revisions and prints a markdown table of absolute and percentage deltas, with workspace preservation, efficient dependency handling, and interrupted-run recovery.

Review Change Stack

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 %
@redfish4ktc redfish4ktc added the chore Build, CI/CD or repository tasks (issues/PR maintenance, environments, ...) label May 21, 2026
@coderabbitai

coderabbitai Bot commented May 21, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds 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.

Changes

compare-examples-size Skill and Implementation

Layer / File(s) Summary
Skill contract and output rules
.claude/skills/compare-examples-size/SKILL.md
Defines comparison inputs, reuse rules, parsing requirements, output format, analysis requirements, prerequisites, and failure handling.
Script validation and ref preparation
.claude/skills/compare-examples-size/scripts/compare-examples-size.bash
Validates options and repository state, resolves refs, orders columns, classifies ancestry, and prepares cleanup and recovery handling.
Per-ref build and size capture
.claude/skills/compare-examples-size/scripts/compare-examples-size.bash
Conditionally installs dependencies, checks out and builds required refs, captures CSV output, and reuses supplied size files when provided.
Size parsing and comparison output
.claude/skills/compare-examples-size/scripts/compare-examples-size.bash
Parses CSV and Markdown inputs, validates commit SHAs and columns, merges example names, computes deltas, and emits comparison notes.
Developer tooling documentation
packages/website/docs/development/tools.md
Documents example builds, size comparisons, revision ordering, ancestry semantics, size-file reuse, supported formats, column selection, and SHA validation.

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
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise, follows Conventional Commits, and accurately identifies the added compare-examples-size skill.
Description check ✅ Passed The description clearly covers motivation, implementation, safety behavior, documentation, testing, and remaining reviewer actions.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
.claude/skills/compare-examples-size/scripts/compare-examples-size.bash (1)

156-169: ⚖️ Poor tradeoff

Update last-two-lines parsing: currently OK, still brittle to whitespace/future output

build-all-examples.bash ends by printing exactly:

  1. echo "$csv_header"
  2. echo "$csv_values"
    with no further non-empty stdout output after that, so grep -v '^$' "$raw_out" | tail -n 2 should 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, the tail -n 2 contract 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 value

Consider 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

📥 Commits

Reviewing files that changed from the base of the PR and between 47421aa and 1590926.

📒 Files selected for processing (2)
  • .claude/skills/compare-examples-size/SKILL.md
  • .claude/skills/compare-examples-size/scripts/compare-examples-size.bash

Comment thread .claude/skills/compare-examples-size/scripts/compare-examples-size.bash Outdated
Comment thread .claude/skills/compare-examples-size/scripts/compare-examples-size.bash Outdated
Comment thread .claude/skills/compare-examples-size/scripts/compare-examples-size.bash Outdated
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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 86d98179-6bd1-416a-8e87-ed9e74c336d4

📥 Commits

Reviewing files that changed from the base of the PR and between 1590926 and fb2f7ba.

📒 Files selected for processing (1)
  • packages/website/docs/development/tools.md

Comment thread packages/website/docs/development/tools.md Outdated
@sonarqubecloud

Copy link
Copy Markdown

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.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between 6379963 and 46439d6.

📒 Files selected for processing (3)
  • .claude/skills/compare-examples-size/SKILL.md
  • .claude/skills/compare-examples-size/scripts/compare-examples-size.bash
  • packages/website/docs/development/tools.md

Comment on lines +270 to +280
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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Suggested change
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"

Comment on lines +353 to +366
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 500

Repository: 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 || true

Repository: 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 || true

Repository: 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.bash

Repository: 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.bash

Repository: 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 %.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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 @@
---

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore Build, CI/CD or repository tasks (issues/PR maintenance, environments, ...)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants