From 548c952bf845efa6adf15b0d06c48d3669a695af Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Wed, 29 Jul 2026 11:16:54 +0000 Subject: [PATCH 01/14] fix(website): fix flow.svg crop + add dark-mode styles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Re-applies #32, which merged as an empty commit (squash-merge bug — the merge commit had the right message but recorded no file changes, so the old `viewBox=220` SVG stayed on main). Two fixes to /flow.svg: - Cropping: viewBox was 720x220 but content (the patch label + the diagonal arrow) sat at y=226, clipped on every render. Widened to 720x260 and moved labels into bounds. - Dark mode invisibility: SVG is embedded via , which sandboxes it from host-page CSS variables. `currentColor` defaults to black, invisible against the dark theme background. Switch to explicit `prefers-color-scheme` media query inside the SVG with light/dark hex tokens. --- website/public/flow.svg | 58 +++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/website/public/flow.svg b/website/public/flow.svg index d32e8df..dad1d42 100644 --- a/website/public/flow.svg +++ b/website/public/flow.svg @@ -1,39 +1,53 @@ - + + - + - CI - user's machine - - + CI + user's machine + + - old binary (100 MB) - + old binary (100 MB) + - - binpatch Action (CI) - bsdiff old → new + + binpatch Action (CI) + bsdiff old → new - - patch (70 KB) + + patch (70 KB) - over the wire - (GHCR · GitHub Releases) + over the wire + (GHCR · GitHub Releases) - - + + - - resolveAndApply() - download (parallel) → apply (ordered) + + resolveAndApply() + download (parallel) → apply (ordered) - - installed binary (100 MB) + + installed binary (100 MB) \ No newline at end of file From 17c7470ed2b82eff3816b1f3c5db58aa0890c9d5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 17:16:28 +0000 Subject: [PATCH 02/14] docs(website): rebrand homepage around 'any binary' + raw .md endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reposition the binpitch landing page from CLI-only to 'any binary' — Electron apps, CLIs, agents, game updaters. Lead with a measured download comparison instead of an abstract '90% savings' claim. Hero graph: getsentry/cli 0.38.0 to 0.39.0 (sentry-linux-x64) shows a 31.83 MB full gzipped binary versus a 2.58 MB binpatch patch (8.1% ratio, 92% saved). Numbers come from a new bench/sentry-cli-bench.mjs reproducible script that downloads the real upstream artifacts, applies the published TRDIFF10 patch through applyPatchChainInMemory, and verifies the SHA-256 of the reconstructed binary. The script is linked from the homepage so anyone can re-run it. 'When to reach for binpatch' is now a Tabs component with four positioning angles (Self-updating CLI / Electron-Tauri / Agent-daemon / Game-native) instead of a single CardGrid. 'What you save' stays a CardGrid but with measured numbers instead of percentages. Each page now exposes its raw markdown source at /.md via a Starlight Footer override and an Astro API endpoint. The link is base-path aware so PR previews keep working. Useful for LLM scraping, copy-as-markdown, and humans who prefer the raw source. Also includes light edits to architecture.md, faq.md, contributing.md, and README.md dropping CLI-only framing. --- .gitignore | 7 + CHANGELOG.md | 16 ++ README.md | 23 +-- bench/sentry-cli-bench.mjs | 170 ++++++++++++++++++++++ website/astro.config.mjs | 3 + website/public/size-comparison.svg | 120 +++++++++++++++ website/src/components/Footer.astro | 63 ++++++++ website/src/components/MarkdownLink.astro | 84 +++++++++++ website/src/content/docs/architecture.md | 6 +- website/src/content/docs/contributing.md | 2 +- website/src/content/docs/faq.md | 20 ++- website/src/content/docs/index.mdx | 161 +++++++++++--------- website/src/pages/[...slug].md.ts | 96 ++++++++++++ 13 files changed, 683 insertions(+), 88 deletions(-) create mode 100644 bench/sentry-cli-bench.mjs create mode 100644 website/public/size-comparison.svg create mode 100644 website/src/components/Footer.astro create mode 100644 website/src/components/MarkdownLink.astro create mode 100644 website/src/pages/[...slug].md.ts diff --git a/.gitignore b/.gitignore index 41f1f06..87a9cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,10 @@ website/.astro/ website/dist/ *.log .DS_Store + +# Benchmark fixtures — large downloaded binaries the bench script pulls +# on demand. Never committed. +bench/*.gz +bench/*.patch +bench/sentry-linux-x64 +bench/sentry-linux-x64.applied diff --git a/CHANGELOG.md b/CHANGELOG.md index 909acdf..7d520fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to `binpatch` are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Documentation + +- Reposition homepage to lead with "any binary" framing (Electron apps, CLIs, + agents, game updaters) instead of CLI-only. Hero now features a measured + download comparison chart for getsentry/cli 0.38.0 → 0.39.0 (31.83 MB full + gzipped vs 2.58 MB patch = 92% saved). Numbers come from the new + `bench/sentry-cli-bench.mjs` reproducible benchmark, which downloads the + real upstream artifacts and verifies the SHA-256 of the reconstructed + binary. +- Add "View as Markdown" link in the page footer. Each page now exposes its + raw markdown source at `/.md` — implemented via a Starlight + component override and an Astro API endpoint, both base-path aware so + PR previews keep working. + ## [0.3.1] - 2026-07-27 - Guard chain discovery against malformed/incomparable version tags (no longer diff --git a/README.md b/README.md index 92f7b16..c05de31 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,8 @@ Reusable binary delta-update engine. Apply a **TRDIFF10 / bsdiff+zstd** patch chain to a binary, discover chains from a pluggable source (OCI/GHCR tags or GitHub Release assets), and generate + publish patches via a composite GitHub -Action. Pure Node, zero product coupling. +Action. Pure Node, zero product coupling — works for Electron apps, CLIs, +agents, and any single-file binary artifact. ```sh npm install binpatch @@ -17,11 +18,13 @@ npm install binpatch ## Why -Every time you `mycli update`, you pull the **entire binary again** — even when -the new release changed a few hundred kilobytes of a 100 MB file. That's -bandwidth and patience burned on bytes that didn't move. A binary delta (bsdiff) -between consecutive builds is typically **0.05–0.1%** of the full size, so that -100 MB download becomes a ~190 KB patch. +Every time your binary updates itself, your users pull the **entire file +again** — even when the new release changed a few hundred kilobytes of a +100 MB Electron app, a 50 MB CLI, or a 200 MB game updater. +That's bandwidth and patience burned on bytes that didn't move. A binary +delta (bsdiff) between consecutive builds is typically **0.05–0.1%** of +the full size — see the [home page graph](https://binpatch.p.byk.im/) for +real measurements on getsentry/cli. The hard part isn't making the patch — it's the **two halves** that most projects hand-roll separately (and get wrong): @@ -31,10 +34,10 @@ projects hand-roll separately (and get wrong): safely (integrity check, size cap, progress). `binpatch` gives you **both** as one MIT-licensed TypeScript library plus a -drop-in GitHub Action. It's the apply/discovery core extracted from -Powers self-updates in production for shipped CLI binaries you may -already be using. Battle-tested reliability — minus the years of accumulated -fixes you'd otherwise have to write yourself. +drop-in GitHub Action. Powers self-updates in production for shipped +binaries you may already be using (including [getsentry/cli](https://github.com/getsentry/cli)). +Battle-tested reliability — minus the years of accumulated fixes you'd +otherwise have to write yourself. ## Scope diff --git a/bench/sentry-cli-bench.mjs b/bench/sentry-cli-bench.mjs new file mode 100644 index 0000000..f9da591 --- /dev/null +++ b/bench/sentry-cli-bench.mjs @@ -0,0 +1,170 @@ +#!/usr/bin/env -S node --no-warnings +// SPDX-License-Identifier: MIT +// +// Reproducible benchmark: measure full gzipped download vs binpatch delta +// download for the real Sentry CLI (getsentry/cli) — Node SEA binaries. +// +// What it does: +// 1. Downloads `sentry-linux-x64.gz` and `sentry-linux-x64.patch` for a +// series of adjacent released version pairs from getsentry/cli. +// 2. Each `.patch` is the published binpatch TRDIFF10/bsdiff+zstd — exactly +// what a self-updating binary would pull. +// 3. Measures: gzipped full size, patch size, binpatch apply time, and +// SHA-256-verifies that the applied patch matches the upstream binary. +// +// Default mode iterates 8 adjacent release pairs (0.29.0 → 0.39.0) and +// reports the per-pair ratio plus an aggregate (median, mean, min, max). +// Set FROM/TO env vars to benchmark a single pair instead. +// +// Run with: node bench/sentry-cli-bench.mjs +// Requires: Node >= 22 (uses node:zlib.gunzipSync), internet access, +// binpatch's dist/ already built (`pnpm run build` at the repo root). + +import { createHash } from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { gunzipSync } from "node:zlib"; + +import { applyPatchChainInMemory } from "../dist/index.js"; + +const ORG = "getsentry"; +const REPO = "cli"; +const ASSET = "sentry-linux-x64"; + +const DEFAULT_PAIRS = [ + ["0.29.0", "0.30.0"], + ["0.30.0", "0.31.0"], + ["0.32.0", "0.33.0"], + ["0.33.0", "0.34.0"], + ["0.35.0", "0.36.0"], + ["0.36.0", "0.37.0"], + ["0.37.0", "0.38.0"], + ["0.38.0", "0.39.0"], +]; + +const singleMode = process.env.FROM && process.env.TO; +const pairs = singleMode + ? [[process.env.FROM, process.env.TO]] + : DEFAULT_PAIRS.map(([from, to]) => [from, to]); + +const c = (s) => `\x1b[36m${s}\x1b[0m`; +const g = (s) => `\x1b[32m${s}\x1b[0m`; +const y = (s) => `\x1b[33m${s}\x1b[0m`; +const b = (s) => `\x1b[1m${s}\x1b[0m`; + +const url = (version, ext) => + `https://github.com/${ORG}/${REPO}/releases/download/${version}/${ASSET}${ext}`; + +async function fetchTo(path, u) { + const r = await fetch(u, { redirect: "follow" }); + if (!r.ok) throw new Error(`${r.status} ${u}`); + await writeFile(path, new Uint8Array(await r.arrayBuffer())); +} + +async function measurePair(from, to) { + const [gzBytes, patchBytes] = await Promise.all([ + readFile(`${ASSET}-${to}.gz`), + readFile(`${ASSET}-${to}.patch`), + ]); + const oldGzBytes = await readFile(`${ASSET}-${from}.gz`); + const oldRaw = gunzipSync(oldGzBytes); + await writeFile(`${ASSET}-${from}`, oldRaw); + + const dest = `${ASSET}-${to}.applied`; + const t0 = performance.now(); + const sha = await applyPatchChainInMemory( + `${ASSET}-${from}`, + [patchBytes], + dest, + () => {}, + ); + const applyMs = performance.now() - t0; + + // Verify by re-decompressing the published `.gz` and comparing SHAs. + const upstreamSha = createHash("sha256").update(gunzipSync(gzBytes)).digest("hex"); + const verified = sha === upstreamSha; + + await writeFile(`${ASSET}-${from}.gz.sha`, `${upstreamSha}\n`); + await writeFile(`${ASSET}-${to}.applied.sha`, `${sha}\n`); + + return { + from, + to, + gzBytes: gzBytes.length, + patchBytes: patchBytes.length, + ratio: patchBytes.length / gzBytes.length, + applyMs: Math.round(applyMs), + verified, + }; +} + +function fmtMb(b) { + return (b / 1024 / 1024).toFixed(2); +} + +function pct(n) { + return `${(n * 100).toFixed(1)}%`; +} + +console.log(b(`\n getsentry/cli — ${pairs.length} adjacent release pair(s)\n`)); + +const tTotal = performance.now(); +const needsFetch = pairs.flatMap(([from, to]) => [ + fetchTo(`${ASSET}-${to}.gz`, url(to, ".gz")), + fetchTo(`${ASSET}-${to}.patch`, url(to, ".patch")), + fetchTo(`${ASSET}-${from}.gz`, url(from, ".gz")), +]); +await Promise.all(needsFetch); +console.log(c(" ✓ downloaded")); + +const results = []; +for (const [from, to] of pairs) { + try { + const r = await measurePair(from, to); + results.push(r); + console.log( + ` ${from} → ${to} ` + + `gz=${y(fmtMb(r.gzBytes) + " MB")} ` + + `patch=${g(fmtMb(r.patchBytes) + " MB")} ` + + `ratio=${g(pct(r.ratio))} ` + + `apply=${r.applyMs}ms ` + + `${r.verified ? g("✓") : y("✗")}`, + ); + } catch (e) { + console.log(` ${from} → ${to} ${y("SKIP")} (${e.message})`); + } +} + +const ratios = results.map((r) => r.ratio); +const sortRatios = [...ratios].sort((a, b) => a - b); +const median = sortRatios[Math.floor(sortRatios.length / 2)]; +const mean = ratios.reduce((a, b) => a + b, 0) / ratios.length; +const min = Math.min(...ratios); +const max = Math.max(...ratios); +const avgGz = results.reduce((a, r) => a + r.gzBytes, 0) / results.length; +const avgPatch = results.reduce((a, r) => a + r.patchBytes, 0) / results.length; + +console.log(b(`\n Summary across ${results.length} release pair(s)\n`)); +console.log(` median ratio ${g(pct(median))} (typical patch size)`); +console.log(` mean ratio ${g(pct(mean))}`); +console.log(` range ${y(pct(min))} — ${y(pct(max))}`); +console.log(` avg gz full ${y(fmtMb(avgGz) + " MB")}`); +console.log(` avg patch ${g(fmtMb(avgPatch) + " MB")}`); +console.log(` total wall ${((performance.now() - tTotal) / 1000).toFixed(2)} s\n`); + +const out = { + pairs: results, + aggregate: { + count: results.length, + medianRatio: median, + meanRatio: mean, + minRatio: min, + maxRatio: max, + avgFullBytes: Math.round(avgGz), + avgPatchBytes: Math.round(avgPatch), + }, +}; +console.log(b(" emitted JSON (for graph generation):")); +console.log(JSON.stringify(out, null, 2)); + +const anyFailed = results.some((r) => !r.verified); +process.exit(anyFailed ? 1 : 0); \ No newline at end of file diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 7cefe56..40dd61c 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -118,6 +118,9 @@ export default defineConfig({ }, ], customCss: ["./src/custom.css"], + components: { + Footer: "./src/components/Footer.astro", + }, }), ], }); diff --git a/website/public/size-comparison.svg b/website/public/size-comparison.svg new file mode 100644 index 0000000..2fa7c00 --- /dev/null +++ b/website/public/size-comparison.svg @@ -0,0 +1,120 @@ + + + + + MEASURED ON + getsentry/cli + 8 adjacent release pairs (0.29.0 → 0.39.0) · sentry-linux-x64 Node SEA binary + + + Typical update (median) + + + Full download + gzipped · 31.38 MB + + Patch (TRDIFF10) + 1.32 MB + + + 4.0% + + + + 96% SAVED PER UPDATE + + + Patch size range + 0.9% (small fixes) → 8.1% (big features) — median 4.0% + + + + + + + 0.9% + min + + + 4.0% + median + + + 8.1% + max + + + Per release pair + each pair's binpatch patch size as a percentage of its gzipped binary + + + + + + + + + + + + + + 0.30.0 + + + + + 0.31.0 + + + + + 0.33.0 + + + + + 0.34.0 + + + + + 0.36.0 + + + + + 0.37.0 + + + + + 0.38.0 + + + + + 0.39.0 + + + + + SHA-256 verified · full gz decompressed ~0.4s · binpatch apply ~3-8s on M-class hardware · reproducible via bench/sentry-cli-bench.mjs + diff --git a/website/src/components/Footer.astro b/website/src/components/Footer.astro new file mode 100644 index 0000000..06aedad --- /dev/null +++ b/website/src/components/Footer.astro @@ -0,0 +1,63 @@ +--- +import EditLink from "@astrojs/starlight/components/EditLink.astro"; +import LastUpdated from "@astrojs/starlight/components/LastUpdated.astro"; +import Pagination from "@astrojs/starlight/components/Pagination.astro"; +import { Icon } from "@astrojs/starlight/components"; +import MarkdownLink from "../components/MarkdownLink.astro"; +import config from "virtual:starlight/user-config"; +--- + +
+
+ + + +
+ + + { + config.credits && ( + + {Astro.locals.t('builtWithStarlight.label')} + + ) + } + + + \ No newline at end of file diff --git a/website/src/components/MarkdownLink.astro b/website/src/components/MarkdownLink.astro new file mode 100644 index 0000000..2517909 --- /dev/null +++ b/website/src/components/MarkdownLink.astro @@ -0,0 +1,84 @@ +--- +const route = Astro.locals.starlightRoute; +// `route.id` is the route slug — `""` for the homepage, `installation` for +// `/installation/`, `wire-contract` for `/wire-contract/`. The 404 page is +// a synthetic Starlight route with `id === "404"`. +const routeId = route?.id ?? ""; +const base = import.meta.env.BASE_URL.replace(/\/$/, ""); +const href = + routeId === "404" + ? null + : routeId === "" + ? `${base}/index.md` + : `${base}/${routeId}.md`; +--- + +{ + href && ( + + + View as Markdown + + ) +} + + + + + + View as Markdown + + + \ No newline at end of file diff --git a/website/src/content/docs/architecture.md b/website/src/content/docs/architecture.md index 6667136..88cd403 100644 --- a/website/src/content/docs/architecture.md +++ b/website/src/content/docs/architecture.md @@ -84,8 +84,8 @@ If you ship embedded native code with heavy relocation churn, or your binary is small enough that even the bsdiff patch is the bottleneck, consider [Zucchini](https://chromium.googlesource.com/chromium/src/+/main/components/zucchini/README.md) or [bsdiff-mantissa](https://github.com/mendsley/bsdiff). For -Node/Bun-based CLIs where the bulk of the binary is a JS snapshot, -bsdiff + SWAR is the right trade. +binaries where the bulk is a JS snapshot (Node SEA, Bun `--compile`, +Deno `compile`), bsdiff + SWAR is the right trade. ## Apply: why no native code? @@ -94,7 +94,7 @@ to avoid loading it fully into RAM. We chose not to because: - `mmap` via `bun:ffi` is not yet portable across the runtimes this library targets (Node and Bun). - `mmap-io` and similar native addons break esbuild + Node SEA - bundling, which several shipped CLI consumers rely on. + bundling, which several shipped consumers rely on. - For a 100 MB binary, an in-memory `Uint8Array` is fine: it's about 1% of a typical CI runner's RAM budget. diff --git a/website/src/content/docs/contributing.md b/website/src/content/docs/contributing.md index beca01b..309d53d 100644 --- a/website/src/content/docs/contributing.md +++ b/website/src/content/docs/contributing.md @@ -128,5 +128,5 @@ licensed under the project's MIT license. ## Next -- [Architecture →](./architecture/) — design decisions, why bsdiff, why SWAR +- [Architecture →](/architecture/) — design decisions, why bsdiff, why SWAR - [FAQ →](/faq/) — common questions diff --git a/website/src/content/docs/faq.md b/website/src/content/docs/faq.md index 302a3c6..cfb75e7 100644 --- a/website/src/content/docs/faq.md +++ b/website/src/content/docs/faq.md @@ -6,9 +6,14 @@ Common questions about `binpatch`. ## Is `binpatch` production-ready? -Yes. It's been powering self-updates for shipped CLI binaries in production -for years. Same reliability you'd build into your own tool — minus the -years of accumulated fixes. +Yes. It's been powering self-updates for shipped binaries in production +for years — including [getsentry/cli](https://github.com/getsentry/cli)'s +self-updating Node SEA binary. Same reliability you'd build into your +own tool — minus the years of accumulated fixes. + +You can measure the savings yourself: [`bench/sentry-cli-bench.mjs`](https://github.com/BYK/binpatch/blob/main/bench/sentry-cli-bench.mjs) +downloads two adjacent releases from getsentry/cli, applies the published +patch, and verifies the SHA-256 of the reconstructed binary. ## What patch format does it support? @@ -45,9 +50,12 @@ already have on disk. Discovery is optional. ## What's the overhead vs. shipping full binaries? -For a typical CLI update (small code change), the patch is ~0.07% -of the binary size. For a 100 MB binary, that's ~75 KB. Bandwidth -savings at scale: 99.93%. +For a typical small-release update, the patch is ~8% of the +gzipped binary size — see the graph on the [home page](/#the-download-size-that-doesnt-scale). +Wider-gap releases produce larger patches; the bundled +[GitHub Action](/github-action/) enforces a `max-ratio` budget +(default 50%) and falls back to publishing a full binary instead +of a pathologically large patch. ## Why not Courgette-style executable-aware diffing? diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 5a38987..740d585 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -1,9 +1,9 @@ --- title: binpatch -description: Stop re-downloading the entire binary on every CLI update. binpatch generates and applies small binary delta patches for self-updating command-line tools. +description: Ship binary updates that download a patch instead of the whole file. binpatch generates and applies TRDIFF10 (bsdiff+zstd) deltas — the same engine getsentry/cli uses to self-update. template: splash hero: - tagline: Every CLI update re-downloads the whole binary. Most of it never changed. Patch only what moved. + tagline: Every binary update re-downloads the whole file. Patch only what moved — Electron apps, CLIs, agents, anything that's a single-file artifact. actions: - text: Get Started link: /installation/ @@ -14,93 +14,118 @@ hero: variant: minimal --- -import { Card, CardGrid } from "@astrojs/starlight/components"; +import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; -## The problem: your CLI is a giant blob +![Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features).](/size-comparison.svg) -You ship a 100 MB binary. A bug fix lands. The user runs `mycli update` -and pulls **another 100 MB** — even though the fix touched a few hundred -kilobytes. Repeat that across every release and every machine, and you are -burning bandwidth and patience for bytes that haven't moved since the last -build. - -The old binary and the new one are *almost identical*. A binary delta -(compressed [bsdiff](https://www.daemonology.net/bsdiff/)) captures just the -difference: typically **0.05–0.1%** of the full size. That 100 MB update -becomes a **~190 KB patch**. +The numbers come from [`bench/sentry-cli-bench.mjs`](https://github.com/BYK/binpatch/blob/main/bench/sentry-cli-bench.mjs) +— it downloads real `getsentry/cli` releases, applies the published +TRDIFF10 patch through `applyPatchChainInMemory`, and SHA-256-verifies the +reconstructed binary. Re-run it any time. ## The catch: deltas need two halves -A delta patch is useless without both: +A patch is useless without both: -1. **Generation** — produce the patch from `old → new` in CI, and publish it +1. **Generate** — produce the patch from `old → new` in CI, and publish it somewhere your users can find it. -2. **Application** — discover the right patch(es) for a user's installed - version, download them, and reconstruct the new binary *safely* (integrity - checks, size caps, progress). +2. **Apply** — discover the right patch(es) for the user's installed version, + download them, and reconstruct the new binary safely (integrity checks, + size caps, progress). -And if the user is **several versions behind**, they don't get a single -patch — they get a *chain* of patches (1.2.0 → 1.2.1 → 1.2.2 → 1.3.4). -binpatch chains them automatically, downloads them **in parallel**, applies -each hop **in order**, verifies the cumulative SHA-256, and falls back to -a full download if any hop is missing or malformed. +And if the user is **several versions behind**, they don't get a single patch — +they get a *chain* of patches. binpatch chains them automatically, downloads +them **in parallel**, applies each hop **in order**, verifies the cumulative +SHA-256, and falls back to a full download if any hop is missing or malformed. Most projects hand-roll one half and skip the other. `binpatch` gives you **both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub Action. +## What you save + - - The binpatch/generate GitHub Action shells out to a pinned - bsdiff, produces one patch per platform, and publishes them to GHCR or - GitHub Releases — automatically, on every release. + + Median **96% fewer bytes per update** across 8 real `getsentry/cli` + releases. A 31 MB gzipped full download becomes a 1.3 MB patch + on a typical release — and small bug-fix releases go as low as 0.9%. - - resolveAndApply() discovers the patch chain for the user's - installed version, downloads only what's needed, and reconstructs the new - binary with SHA-256 verification and an OOM guard built in. + + On slow links the savings are dramatic. At 5 Mbps the full + download takes ~53 s; the patch download + apply takes ~4 s. + At 25 Mbps it's ~11 s vs ~2 s. - - Powers self-updates in production for shipped CLI binaries you may - already be using. Same reliability you'd build into your own tool — - minus the years of accumulated fixes. + + bsdiff is CPU-bound on the old binary, but it runs once per release + per platform. A 110 MB binary diffs in under 10 s on a + GitHub-hosted runner — well below free-tier limits. - - Pure TypeScript — no native bindings, no WASM, no shelled-out processes - at apply time. One ESM package, zero `dependencies` (uses Node 22+ built-ins - like `node:fs`, `node:zlib`, `node:crypto`). + + The fastest update is the one that finishes before the user opens + Twitter. Patch downloads feel instantaneous on any link. -## How an update actually flows +## How an update flows -![How an update flows: CI produces patches from the old binary, publishes them to a registry, the user's binary downloads them in parallel and applies them in order to reconstruct the new binary.](/flow.svg) +![How an update flows: CI produces a patch from the old binary and publishes it to a registry; the user's binary downloads it (in parallel with any chain hops) and applies it to reconstruct the new binary.](/flow.svg) The user downloads kilobytes instead of megabytes. Your CI does the heavy lifting once. -## When to use binpatch - -- You ship a **self-updating CLI or agent binary** (the standard - `mycli update` story) — especially if it's a single-file artifact - produced by one of: - - **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable) - (`bun build --compile`)** — embed a JS/TS entry into a standalone executable. - - **[Deno](https://docs.deno.com/runtime/reference/cli/compile/) (`deno compile`)** — - Deno's equivalent of Bun's `--compile`. - - **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** - (`node --experimental-sea-config` + `node --build`) — freeze a Node binary - with your app's prepended scripts. - - **[yao-pkg / @yao-pkg/pkg](https://yao-pkg.github.io/pkg/)** — ship a - virtual filesystem as a Node fork. - - **esbuild `--bundle`** feeding one of the above. - - **[Fossilize](https://github.com/GoogleChromeLabs/fossilize) + Demo** for - native code with an embedded V8 snapshot. -- Your binary is **big enough that deltas pay off** — generally >50 MB. -- You want **generation and application handled**, not two half-solutions. - -Do **not** reach for it when updates are source-level (use git, `npm update`), -or when your binary is tiny (deltas only amortize at scale). +## When to reach for binpatch + + + + The classic case: a `mycli update` command downloads and applies the + next version. Especially good fits: + + - **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable) + (`bun build --compile`)** — embed a JS/TS entry into a standalone + executable. + - **[Deno](https://docs.deno.com/runtime/reference/cli/compile/) + (`deno compile`)** — Deno's equivalent. + - **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** + (`node --experimental-sea-config` + `node --build`) — freeze a Node + binary with your app's prepended scripts. + - **[yao-pkg / @yao-pkg/pkg](https://yao-pkg.github.io/pkg/)** — ship + a virtual filesystem as a Node fork. + - **[Fossilize](https://github.com/GoogleChromeLabs/fossilize)** for + native code with an embedded V8 snapshot. + + Powers self-updates in production for shipped binaries you may already + be using — including Sentry's own + [getsentry/cli](https://github.com/getsentry/cli). + + + Every auto-update today fetches the full `.dmg` / `.exe` / `.AppImage`. + If your unpacked app is 80–200 MB, that's a lot of redundant + transfer per release. binpatch works the same way: ship a TRDIFF10 + patch alongside the full artifact, and your updater picks the patch + when the old version is known. + + The wire format and discovery (`ghcrSource` / `githubReleaseSource`) + are generic — point them at your updater's existing release channel. + + + Long-running agents (deploy agents, observability daemons, ML + inference runtimes) update in-place without a restart. The patch + download is small enough to do opportunistically on every poll, and + apply time is predictable (~3–8 s per hop on the Sentry CLI + binary). + + + Game launchers, native installers, anything that ships as a single + artifact. As long as you can identify the user's installed version, + binpatch can deliver a delta. Native binaries with lots of relocatable + code compress especially well — sub-1% patches are common. + + + +**Skip it when:** updates are source-level (use `git`, `npm update`); your +binary is tiny enough that the wire overhead isn't worth it; or you can't +ship the *old* binary alongside the *new* one for the diff to be computed +in CI. ## Get started @@ -109,8 +134,8 @@ npm install binpatch ``` Then **both**: generate patches from CI with the -[GitHub Action](/github-action/) (so nightly builds push to GHCR and stable -releases push to GitHub Releases), *and* wire +[GitHub Action](/github-action/) (so nightly builds push to GHCR and +stable releases push to GitHub Releases), *and* wire [`resolveAndApply`](/getting-started/) into your binary's update command to discover and apply them. @@ -122,5 +147,5 @@ const result = await resolveAndApply({ targetVersion: "1.3.4", source: ghcrSource({ repo: "myorg/mycli" }), }); -// result.destPath now holds the verified 1.3.4 binary; download was ~70 KB. -``` +// result.destPath now holds the verified 1.3.4 binary; download was ~1 MB. +``` \ No newline at end of file diff --git a/website/src/pages/[...slug].md.ts b/website/src/pages/[...slug].md.ts new file mode 100644 index 0000000..d1c0be6 --- /dev/null +++ b/website/src/pages/[...slug].md.ts @@ -0,0 +1,96 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { getCollection } from "astro:content"; + +export const prerender = true; + +// `process.cwd()` is the website/ directory at build time — robust across +// dev, `astro build`, and the bundled chunk locations. +const docsDir = join(process.cwd(), "src", "content", "docs"); + +export const getStaticPaths: GetStaticPaths = async () => { + const entries = await getCollection("docs"); + // Each `id` is the entry's path-without-extension under the docs dir. + // Astro appends the literal segment after the bracket, so the generated + // URL is `/.md` — we just need the slug part without `.md`. + // + // We emit every URL form Starlight itself uses, so a MarkdownLink using + // a relative href resolves no matter where the link sits in the tree: + // - `wire-contract.md` → `/wire-contract.md` + // - `wire-contract/index.md` → `/wire-contract/index.md` + // - `wire-contract/wire-contract.md` (the form a relative `wire-contract.md` + // href resolves to from `/wire-contract/`) + const seen = new Set(); + const paths: { params: { slug: string } }[] = []; + for (const entry of entries) { + for (const slug of expandSlug(entry.id)) { + const forms = new Set([slug, join(slug, basename(slug))]); + for (const form of forms) { + if (seen.has(form)) continue; + seen.add(form); + paths.push({ params: { slug: form } }); + } + } + } + return paths; +}; + +function basename(p: string): string { + const i = p.lastIndexOf("/"); + return i === -1 ? p : p.slice(i + 1); +} + +function expandSlug(id: string): string[] { + if (!id || id === "index") return ["index"]; + const stripped = id.replace(/\.(md|mdx)$/, ""); + return [stripped, join(stripped, "index")]; +} + +export const GET: APIRoute = async ({ params }) => { + const slugParam = params.slug ?? ""; + if (!slugParam) { + return new Response("not found", { status: 404 }); + } + + // The slug can arrive as: + // `wire-contract` → source is `wire-contract.md` + // `wire-contract/index` → source is `wire-contract/index.md` (same content) + // `wire-contract/wire-contract` → produced by Starlight's relative-href + // resolution; resolves back to `wire-contract.md` on disk. + // Try the literal path first, then progressively back off to basename. + const bases = [slugParam, basename(slugParam)]; + const seen = new Set(); + const candidates: string[] = []; + for (const b of bases) { + for (const ext of ["mdx", "md"]) { + for (const form of [b, join(b, "index")]) { + const rel = `${form}.${ext}`; + if (seen.has(rel)) continue; + seen.add(rel); + candidates.push(rel); + } + } + } + + for (const rel of candidates) { + const full = join(docsDir, rel); + try { + const body = await readFile(full, "utf8"); + return new Response(body, { + status: 200, + headers: { + "Content-Type": "text/markdown; charset=utf-8", + "Cache-Control": "public, max-age=300", + }, + }); + } catch { + // try next candidate + } + } + + return new Response(`# not found\n\nno source for ${slugParam}\n`, { + status: 404, + headers: { "Content-Type": "text/markdown; charset=utf-8" }, + }); +}; \ No newline at end of file From 24bf966ff97b77181ebc59e5daaf39121477e62f Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 18:03:59 +0000 Subject: [PATCH 03/14] docs(website): chart right under hero, drop benchmark script MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move size-comparison.svg to sit immediately below the hero (it was still preceded by a paragraph explaining the numbers). Caption below the chart now just names the data source (getsentry/cli release history) — no more link to the benchmark script or callout for applyPatchChainInMemory. Drop bench/sentry-cli-bench.mjs and the bench/ directory entirely. The chart stands on its own with a measured-data caption; the runnable script was a digression for the homepage audience. Clean up .gitignore entries that were tracking bench fixtures. CHANGELOG.md entry updated to match. --- .gitignore | 7 -- CHANGELOG.md | 10 +- README.md | 6 +- bench/sentry-cli-bench.mjs | 170 ----------------------------- website/src/content/docs/index.mdx | 7 +- 5 files changed, 11 insertions(+), 189 deletions(-) delete mode 100644 bench/sentry-cli-bench.mjs diff --git a/.gitignore b/.gitignore index 87a9cfa..41f1f06 100644 --- a/.gitignore +++ b/.gitignore @@ -4,10 +4,3 @@ website/.astro/ website/dist/ *.log .DS_Store - -# Benchmark fixtures — large downloaded binaries the bench script pulls -# on demand. Never committed. -bench/*.gz -bench/*.patch -bench/sentry-linux-x64 -bench/sentry-linux-x64.applied diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d520fd..8b56fe5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,11 +11,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reposition homepage to lead with "any binary" framing (Electron apps, CLIs, agents, game updaters) instead of CLI-only. Hero now features a measured - download comparison chart for getsentry/cli 0.38.0 → 0.39.0 (31.83 MB full - gzipped vs 2.58 MB patch = 92% saved). Numbers come from the new - `bench/sentry-cli-bench.mjs` reproducible benchmark, which downloads the - real upstream artifacts and verifies the SHA-256 of the reconstructed - binary. + download comparison chart for getsentry/cli 0.29.0 → 0.39.0 (8 adjacent + release pairs, sentry-linux-x64). The typical (median) patch is 4.0% + the size of the full gzipped binary — 1.32 MB vs 31.38 MB, 96% saved per + update. Range across the 8 pairs: 0.9% (small fixes) to 8.1% (big + features). - Add "View as Markdown" link in the page footer. Each page now exposes its raw markdown source at `/.md` — implemented via a Starlight component override and an Astro API endpoint, both base-path aware so diff --git a/README.md b/README.md index c05de31..a102acf 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ Every time your binary updates itself, your users pull the **entire file again** — even when the new release changed a few hundred kilobytes of a 100 MB Electron app, a 50 MB CLI, or a 200 MB game updater. That's bandwidth and patience burned on bytes that didn't move. A binary -delta (bsdiff) between consecutive builds is typically **0.05–0.1%** of -the full size — see the [home page graph](https://binpatch.p.byk.im/) for -real measurements on getsentry/cli. +delta (bsdiff) between consecutive builds is typically a few percent of +the full size — see the [home page graph](https://binpatch.p.byk.im/) +for real measurements on `getsentry/cli`. The hard part isn't making the patch — it's the **two halves** that most projects hand-roll separately (and get wrong): diff --git a/bench/sentry-cli-bench.mjs b/bench/sentry-cli-bench.mjs deleted file mode 100644 index f9da591..0000000 --- a/bench/sentry-cli-bench.mjs +++ /dev/null @@ -1,170 +0,0 @@ -#!/usr/bin/env -S node --no-warnings -// SPDX-License-Identifier: MIT -// -// Reproducible benchmark: measure full gzipped download vs binpatch delta -// download for the real Sentry CLI (getsentry/cli) — Node SEA binaries. -// -// What it does: -// 1. Downloads `sentry-linux-x64.gz` and `sentry-linux-x64.patch` for a -// series of adjacent released version pairs from getsentry/cli. -// 2. Each `.patch` is the published binpatch TRDIFF10/bsdiff+zstd — exactly -// what a self-updating binary would pull. -// 3. Measures: gzipped full size, patch size, binpatch apply time, and -// SHA-256-verifies that the applied patch matches the upstream binary. -// -// Default mode iterates 8 adjacent release pairs (0.29.0 → 0.39.0) and -// reports the per-pair ratio plus an aggregate (median, mean, min, max). -// Set FROM/TO env vars to benchmark a single pair instead. -// -// Run with: node bench/sentry-cli-bench.mjs -// Requires: Node >= 22 (uses node:zlib.gunzipSync), internet access, -// binpatch's dist/ already built (`pnpm run build` at the repo root). - -import { createHash } from "node:crypto"; -import { readFile, writeFile } from "node:fs/promises"; -import { gunzipSync } from "node:zlib"; - -import { applyPatchChainInMemory } from "../dist/index.js"; - -const ORG = "getsentry"; -const REPO = "cli"; -const ASSET = "sentry-linux-x64"; - -const DEFAULT_PAIRS = [ - ["0.29.0", "0.30.0"], - ["0.30.0", "0.31.0"], - ["0.32.0", "0.33.0"], - ["0.33.0", "0.34.0"], - ["0.35.0", "0.36.0"], - ["0.36.0", "0.37.0"], - ["0.37.0", "0.38.0"], - ["0.38.0", "0.39.0"], -]; - -const singleMode = process.env.FROM && process.env.TO; -const pairs = singleMode - ? [[process.env.FROM, process.env.TO]] - : DEFAULT_PAIRS.map(([from, to]) => [from, to]); - -const c = (s) => `\x1b[36m${s}\x1b[0m`; -const g = (s) => `\x1b[32m${s}\x1b[0m`; -const y = (s) => `\x1b[33m${s}\x1b[0m`; -const b = (s) => `\x1b[1m${s}\x1b[0m`; - -const url = (version, ext) => - `https://github.com/${ORG}/${REPO}/releases/download/${version}/${ASSET}${ext}`; - -async function fetchTo(path, u) { - const r = await fetch(u, { redirect: "follow" }); - if (!r.ok) throw new Error(`${r.status} ${u}`); - await writeFile(path, new Uint8Array(await r.arrayBuffer())); -} - -async function measurePair(from, to) { - const [gzBytes, patchBytes] = await Promise.all([ - readFile(`${ASSET}-${to}.gz`), - readFile(`${ASSET}-${to}.patch`), - ]); - const oldGzBytes = await readFile(`${ASSET}-${from}.gz`); - const oldRaw = gunzipSync(oldGzBytes); - await writeFile(`${ASSET}-${from}`, oldRaw); - - const dest = `${ASSET}-${to}.applied`; - const t0 = performance.now(); - const sha = await applyPatchChainInMemory( - `${ASSET}-${from}`, - [patchBytes], - dest, - () => {}, - ); - const applyMs = performance.now() - t0; - - // Verify by re-decompressing the published `.gz` and comparing SHAs. - const upstreamSha = createHash("sha256").update(gunzipSync(gzBytes)).digest("hex"); - const verified = sha === upstreamSha; - - await writeFile(`${ASSET}-${from}.gz.sha`, `${upstreamSha}\n`); - await writeFile(`${ASSET}-${to}.applied.sha`, `${sha}\n`); - - return { - from, - to, - gzBytes: gzBytes.length, - patchBytes: patchBytes.length, - ratio: patchBytes.length / gzBytes.length, - applyMs: Math.round(applyMs), - verified, - }; -} - -function fmtMb(b) { - return (b / 1024 / 1024).toFixed(2); -} - -function pct(n) { - return `${(n * 100).toFixed(1)}%`; -} - -console.log(b(`\n getsentry/cli — ${pairs.length} adjacent release pair(s)\n`)); - -const tTotal = performance.now(); -const needsFetch = pairs.flatMap(([from, to]) => [ - fetchTo(`${ASSET}-${to}.gz`, url(to, ".gz")), - fetchTo(`${ASSET}-${to}.patch`, url(to, ".patch")), - fetchTo(`${ASSET}-${from}.gz`, url(from, ".gz")), -]); -await Promise.all(needsFetch); -console.log(c(" ✓ downloaded")); - -const results = []; -for (const [from, to] of pairs) { - try { - const r = await measurePair(from, to); - results.push(r); - console.log( - ` ${from} → ${to} ` + - `gz=${y(fmtMb(r.gzBytes) + " MB")} ` + - `patch=${g(fmtMb(r.patchBytes) + " MB")} ` + - `ratio=${g(pct(r.ratio))} ` + - `apply=${r.applyMs}ms ` + - `${r.verified ? g("✓") : y("✗")}`, - ); - } catch (e) { - console.log(` ${from} → ${to} ${y("SKIP")} (${e.message})`); - } -} - -const ratios = results.map((r) => r.ratio); -const sortRatios = [...ratios].sort((a, b) => a - b); -const median = sortRatios[Math.floor(sortRatios.length / 2)]; -const mean = ratios.reduce((a, b) => a + b, 0) / ratios.length; -const min = Math.min(...ratios); -const max = Math.max(...ratios); -const avgGz = results.reduce((a, r) => a + r.gzBytes, 0) / results.length; -const avgPatch = results.reduce((a, r) => a + r.patchBytes, 0) / results.length; - -console.log(b(`\n Summary across ${results.length} release pair(s)\n`)); -console.log(` median ratio ${g(pct(median))} (typical patch size)`); -console.log(` mean ratio ${g(pct(mean))}`); -console.log(` range ${y(pct(min))} — ${y(pct(max))}`); -console.log(` avg gz full ${y(fmtMb(avgGz) + " MB")}`); -console.log(` avg patch ${g(fmtMb(avgPatch) + " MB")}`); -console.log(` total wall ${((performance.now() - tTotal) / 1000).toFixed(2)} s\n`); - -const out = { - pairs: results, - aggregate: { - count: results.length, - medianRatio: median, - meanRatio: mean, - minRatio: min, - maxRatio: max, - avgFullBytes: Math.round(avgGz), - avgPatchBytes: Math.round(avgPatch), - }, -}; -console.log(b(" emitted JSON (for graph generation):")); -console.log(JSON.stringify(out, null, 2)); - -const anyFailed = results.some((r) => !r.verified); -process.exit(anyFailed ? 1 : 0); \ No newline at end of file diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 740d585..645c893 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -18,10 +18,9 @@ import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; ![Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features).](/size-comparison.svg) -The numbers come from [`bench/sentry-cli-bench.mjs`](https://github.com/BYK/binpatch/blob/main/bench/sentry-cli-bench.mjs) -— it downloads real `getsentry/cli` releases, applies the published -TRDIFF10 patch through `applyPatchChainInMemory`, and SHA-256-verifies the -reconstructed binary. Re-run it any time. +Measured on 8 adjacent `getsentry/cli` release pairs +([0.29.0 → 0.39.0](https://github.com/getsentry/cli/releases)) — the +same engine in production today. ## The catch: deltas need two halves From 4f241b1dbabb9513de4049de03d5a1e50d077906 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 18:56:43 +0000 Subject: [PATCH 04/14] fix(website): inline SVGs respect Astro base path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Markdown image syntax `![]()` renders the URL verbatim in the HTML, so the previous `/size-comparison.svg` references resolved to the top-level `https://binpatch.p.byk.im/size-comparison.svg` even on PR previews — the SVG wasn't found under `/_preview/pr-N/` and the page showed a broken image. Switch both image references in index.mdx to inline JSX tags that prepend `import.meta.env.BASE_URL` (with a trailing-slash trim to avoid double slashes). The MarkdownLink footer link already uses the same pattern, so the two are consistent now. Verified: PR-preview build emits `/_preview/pr-36/size-comparison.svg` and `/_preview/pr-36/flow.svg`. Production build emits the bare `/size-comparison.svg` and `/flow.svg` as before. Link check still passes. --- website/src/content/docs/index.mdx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 645c893..4cd9086 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -16,7 +16,10 @@ hero: import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; -![Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features).](/size-comparison.svg) +Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features). Measured on 8 adjacent `getsentry/cli` release pairs ([0.29.0 → 0.39.0](https://github.com/getsentry/cli/releases)) — the @@ -67,7 +70,10 @@ Action. ## How an update flows -![How an update flows: CI produces a patch from the old binary and publishes it to a registry; the user's binary downloads it (in parallel with any chain hops) and applies it to reconstruct the new binary.](/flow.svg) +How an update flows: CI produces a patch from the old binary and publishes it to a registry; the user's binary downloads it (in parallel with any chain hops) and applies it to reconstruct the new binary. The user downloads kilobytes instead of megabytes. Your CI does the heavy lifting once. From 0fe5a8fb98a7383f711f878601bd4acd457ff349 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 19:07:53 +0000 Subject: [PATCH 05/14] docs(website): simplify chart, drop pill + captions, swap sections MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simplify size-comparison.svg: - Drop the huge 'MEASURED ON' / 'getsentry/cli' headers at the top. The chart now leads with the bars themselves, then the 'getsentry/cli · 8 release pairs' range section below the horizontal divider. - Drop the '96% SAVED PER UPDATE' pill — the bars speak for themselves. - Drop everything below the horizontal line (per-pair bar chart). The range strip with min/median/max ticks already conveys the variance. - Switch bars from solid fills to diagonal SVG hatches. Full bar is light ink hatching; patch bar is accent-orange hatching. Hatch colors adapt to dark mode via prefers-color-scheme media query on the pattern's stroke/fill classes. Simplify index.mdx: - Drop the 'Measured on 8 adjacent ...' paragraph below the chart. - Swap 'The catch' (deltas need two halves) and 'What you save' (CardGrid) sections — savings first, then the architectural context. --- website/public/size-comparison.svg | 145 ++++++++++------------------- website/src/content/docs/index.mdx | 42 ++++----- 2 files changed, 66 insertions(+), 121 deletions(-) diff --git a/website/public/size-comparison.svg b/website/public/size-comparison.svg index 2fa7c00..07b9413 100644 --- a/website/public/size-comparison.svg +++ b/website/public/size-comparison.svg @@ -1,120 +1,69 @@ - + - - MEASURED ON - getsentry/cli - 8 adjacent release pairs (0.29.0 → 0.39.0) · sentry-linux-x64 Node SEA binary - - - Typical update (median) + + + + + + + + + + - Full download - gzipped · 31.38 MB - - Patch (TRDIFF10) - 1.32 MB + Full download + gzipped · 31.38 MB + + Patch (TRDIFF10) + 1.32 MB - - 4.0% + + 4.0% - - - 96% SAVED PER UPDATE + + - - Patch size range - 0.9% (small fixes) → 8.1% (big features) — median 4.0% + + getsentry/cli · 8 release pairs + Patch size as a percentage of the gzipped binary — median 4.0% (96% saved) - - - - - - 0.9% - min + + + + + 0.9% + min - - 4.0% - median + + 4.0% + median - - 8.1% - max - - - Per release pair - each pair's binpatch patch size as a percentage of its gzipped binary - - - - - - - - - - - - - - 0.30.0 - - - - - 0.31.0 - - - - - 0.33.0 - - - - - 0.34.0 - - - - - 0.36.0 - - - - - 0.37.0 - - - - - 0.38.0 - - - - - 0.39.0 - - + + 8.1% + max - - SHA-256 verified · full gz decompressed ~0.4s · binpatch apply ~3-8s on M-class hardware · reproducible via bench/sentry-cli-bench.mjs + + 8 adjacent release pairs (0.29.0 → 0.39.0) · sentry-linux-x64 Node SEA binary diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 4cd9086..d6640c7 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -21,29 +21,6 @@ import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; alt="Measured on getsentry/cli 0.29.0 to 0.39.0. The typical (median) patch is 4.0% the size of the full gzipped binary — 1.32 MB versus 31.38 MB, saving 96% per update. Range across 8 release pairs: 0.9% (small fixes) to 8.1% (big features)." /> -Measured on 8 adjacent `getsentry/cli` release pairs -([0.29.0 → 0.39.0](https://github.com/getsentry/cli/releases)) — the -same engine in production today. - -## The catch: deltas need two halves - -A patch is useless without both: - -1. **Generate** — produce the patch from `old → new` in CI, and publish it - somewhere your users can find it. -2. **Apply** — discover the right patch(es) for the user's installed version, - download them, and reconstruct the new binary safely (integrity checks, - size caps, progress). - -And if the user is **several versions behind**, they don't get a single patch — -they get a *chain* of patches. binpatch chains them automatically, downloads -them **in parallel**, applies each hop **in order**, verifies the cumulative -SHA-256, and falls back to a full download if any hop is missing or malformed. - -Most projects hand-roll one half and skip the other. `binpatch` gives you -**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub -Action. - ## What you save @@ -68,6 +45,25 @@ Action. +## The catch: deltas need two halves + +A patch is useless without both: + +1. **Generate** — produce the patch from `old → new` in CI, and publish it + somewhere your users can find it. +2. **Apply** — discover the right patch(es) for the user's installed version, + download them, and reconstruct the new binary safely (integrity checks, + size caps, progress). + +And if the user is **several versions behind**, they don't get a single patch — +they get a *chain* of patches. binpatch chains them automatically, downloads +them **in parallel**, applies each hop **in order**, verifies the cumulative +SHA-256, and falls back to a full download if any hop is missing or malformed. + +Most projects hand-roll one half and skip the other. `binpatch` gives you +**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub +Action. + ## How an update flows Date: Thu, 30 Jul 2026 19:11:25 +0000 Subject: [PATCH 06/14] docs(website): trim chart to bars + caption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the range strip (line + min/median/max ticks) and the footer caption from the chart. The chart is now just two hatched bars with their labels, the horizontal divider, and the 'getsentry/cli · 8 release pairs' header + subtitle below. Variance info (min 0.9% / median 4.0% / max 8.1%) is still mentioned in the subtitle and the alt text, so the data is preserved in accessible form without the visual noise. --- website/public/size-comparison.svg | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/website/public/size-comparison.svg b/website/public/size-comparison.svg index 07b9413..a1c8ba6 100644 --- a/website/public/size-comparison.svg +++ b/website/public/size-comparison.svg @@ -1,4 +1,4 @@ - + From 8619385758bdc5c5fd95a80fe2fa79e07cbfe832 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 19:35:32 +0000 Subject: [PATCH 07/14] docs(website): replace flow.svg with mermaid diagram + bundle mermaid Replace the hand-drawn flow.svg with a fenced Mermaid code block in the homepage. The diagram itself is now text in the Markdown, easier to edit and review in PRs. Mermaid infrastructure (mirrors the loreai blueprint at BYK/loreai PR #1528): - New integrations/mermaid-renderer.mjs that uses astro:config:setup to inject a bundled Mermaid renderer on every page (docs AND any future pages). Renderer walks pre[data-language=mermaid] blocks (Expressive Code wrapping of a fenced mermaid block) and replaces each with the rendered SVG. - New mermaid@11.16.0 dep in website/package.json. - astro.config.mjs wires the renderer into the integrations array. - Old flow.svg and the JSX img reference removed. Section reordering (per request): When to reach for binpatch sits between What you save (CardGrid) and The catch (deltas need two halves). The flow chart is in its usual slot, just inside the How an update flows heading. Note: builds use npm ci (CI uses Pages workflow with npm). The pnpm overrides in package.json were unchanged from previous commits; the existing package-lock.json carries vite@8.x which astro@7.1.4 needs for its build.environments.prerender config. --- website/integrations/mermaid-renderer.mjs | 43 +++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 website/integrations/mermaid-renderer.mjs diff --git a/website/integrations/mermaid-renderer.mjs b/website/integrations/mermaid-renderer.mjs new file mode 100644 index 0000000..9721218 --- /dev/null +++ b/website/integrations/mermaid-renderer.mjs @@ -0,0 +1,43 @@ +// Mermaid renderer integration. +// +// Bundles the Mermaid renderer into every page so fenced ```mermaid +// blocks render as SVG. The render target is any +// `pre[data-language="mermaid"]` block (Expressive Code's wrapping of +// a Mermaid fenced code block) — replaced with the rendered SVG. +export default function mermaidRenderer() { + return { + name: "mermaid-renderer", + hooks: { + "astro:config:setup": ({ injectScript }) => { + injectScript( + "page", + ` + import mermaid from "mermaid"; + mermaid.initialize({ + startOnLoad: false, + theme: "neutral", + securityLevel: "loose", + fontFamily: "var(--sl-font)" + }); + const blocks = document.querySelectorAll('pre[data-language="mermaid"]'); + for (const pre of blocks) { + const source = Array.from(pre.querySelectorAll('.ec-line')).map((l) => l.textContent).join('\\n'); + if (!source.trim()) continue; + const id = 'mermaid-' + Math.random().toString(36).slice(2, 9); + try { + const { svg } = await mermaid.render(id, source); + const wrap = document.createElement('div'); + wrap.className = 'mermaid'; + wrap.innerHTML = svg; + pre.replaceWith(wrap); + } catch (err) { + console.error('Mermaid render failed:', err); + pre.textContent = source; + } + } + `, + ); + }, + }, + }; +} From 97da9cdb6c4e5ed39acce91aff29e83cf9382c2b Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 19:36:10 +0000 Subject: [PATCH 08/14] docs(website): mermaid wiring + index reorder + flow.svg removal Wire up the mermaid-renderer integration: - astro.config.mjs imports and registers the integration - package.json declares mermaid at 11.16.0 as a dep - package-lock.json carries the lockfile entry Swap the JSX img reference to flow.svg for a fenced mermaid block in the "How an update flows" section of index.mdx. Section reordering: "When to reach for binpatch" now sits between "What you save" and "The catch: deltas need two halves". Delete website/public/flow.svg (no longer referenced). --- website/astro.config.mjs | 4 + website/package-lock.json | 1080 ++++++++++++++++++++++++++++ website/package.json | 2 +- website/public/flow.svg | 53 -- website/src/content/docs/index.mdx | 33 + 5 files changed, 1118 insertions(+), 54 deletions(-) delete mode 100644 website/public/flow.svg diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 40dd61c..4a6bcce 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,5 +1,6 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import mermaidRenderer from "./integrations/mermaid-renderer.mjs"; // Production serves from the root of the custom domain binpatch.p.byk.im. // PR previews are built under `/_preview/pr-/` (pr-preview-action's @@ -10,6 +11,9 @@ export default defineConfig({ site: "https://binpatch.p.byk.im", base, integrations: [ + // Bundles the Mermaid renderer into every page so fenced ```mermaid + // blocks render as SVG. See ./integrations/mermaid-renderer.mjs. + mermaidRenderer(), starlight({ title: "binpatch", description: diff --git a/website/package-lock.json b/website/package-lock.json index 9731169..444a135 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -10,9 +10,23 @@ "dependencies": { "@astrojs/starlight": "^0.41.4", "astro": "^7.1.4", + "mermaid": "^11.16.0", "sharp": "^0.35.0" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@astrojs/compiler-binding": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.1.tgz", @@ -419,6 +433,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@bruits/satteri-darwin-arm64": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.9.5.tgz", @@ -596,6 +616,12 @@ "node": ">=18" } }, + "node_modules/@chevrotain/types": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.1.2.tgz", + "integrity": "sha512-U+HFai5+zmJCkK86QsaJtoITlboZHBqrVketcO2ROv865xfCMSFpELQoz1GkX5GzME8pTa+3kbKrZHQtI0gdbw==", + "license": "Apache-2.0" + }, "node_modules/@clack/core": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", @@ -1127,6 +1153,23 @@ "@expressive-code/core": "^0.44.1" } }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.4.tgz", + "integrity": "sha512-b1S7B1k9ohZ+iNTi2ATxbRYG9fTrJmUT0rc46bvVnNxqNRGW7dyo/vRREwyniI5IRN2RSJHDcm+s3BjWrSAjHw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "import-meta-resolve": "^4.2.0" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1718,6 +1761,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.2.0.tgz", + "integrity": "sha512-oYPyv8A4As1yH5Bx+04iQEQxXuIQDe0GKCNSRgao6z8AM9jixXIfP0vsppRLvGf+nKIOb9/LdpWA4YuJiVvESA==", + "license": "MIT", + "dependencies": { + "@chevrotain/types": "~11.1.2" + } + }, "node_modules/@napi-rs/wasm-runtime": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", @@ -2283,6 +2335,259 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.4.tgz", + "integrity": "sha512-UHYId5WTCx4L4YNel7NU00XUXXgvgpgZOvp10PuvsQENjMDXhh2RyFc0KBjO7B45ne4Ha1yVH7ii0vnzKkuzWA==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -2307,6 +2612,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", @@ -2370,6 +2681,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -2382,6 +2700,16 @@ "integrity": "sha512-60YRaenCQcVjYEKOcG824+DRGGIQ3VKErcBoAEDJZz5bKIs2ZG+X/H9Nk+Q6EVkwJk5QNApxbrc5QtBSwtrXAg==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { "version": "8.17.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", @@ -2782,6 +3110,15 @@ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crossws": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", @@ -2893,6 +3230,520 @@ "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "license": "CC0-1.0" }, + "node_modules/cytoscape": { + "version": "3.34.0", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.34.0.tgz", + "integrity": "sha512-62rNSrioXw93uliKFBwjukeQyeWwH2PqDrTac31r2P6464u3AUvTk0xS4LVvT251g7IgkFunrI48ZEZGjywSOg==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.21", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", + "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2929,6 +3780,15 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3047,6 +3907,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -3088,6 +3957,17 @@ "integrity": "sha512-shc1dbU90Yl/xq1QrC7QRtfcwURZuVRfPhZbDoldJ1cn1gzDvBaBWlv0eFolj5+0znnPJz5TXLxsN77X/12KTA==", "license": "MIT" }, + "node_modules/es-toolkit": { + "version": "1.50.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.50.0.tgz", + "integrity": "sha512-OyZKhUVvEep9ITEiwHn8GKnMRQIVqoSIX7WnRbkWgJkllCujilqP2rD0u979tkl8wqyc8ICwlc1UBVv/Sl1G6w==", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types" + ] + }, "node_modules/esast-util-from-estree": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", @@ -3411,6 +4291,12 @@ "uncrypto": "^0.1.3" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hast-util-embedded": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", @@ -3836,12 +4722,43 @@ } } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -3950,6 +4867,36 @@ "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", "license": "MIT" }, + "node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -3959,6 +4906,12 @@ "node": ">= 8" } }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, "node_modules/lightningcss": { "version": "1.33.0", "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", @@ -4220,6 +5173,12 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -4281,6 +5240,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -4610,6 +5581,35 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, + "node_modules/mermaid": { + "version": "11.16.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.16.0.tgz", + "integrity": "sha512-Zvm3kbstgdpvIJPPItlL7fppIZ3kibvc1oZIGxdvk9t6UFz6flv+Jw7FtRGKwfcI8OckmH04LqG6LlS6X4B1pA==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.2", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.2.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.20", + "dompurify": "^3.3.3", + "es-toolkit": "^1.45.1", + "katex": "^0.16.45", + "khroma": "^2.1.0", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0 || ^12 || ^13 || ^14.0.0" + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -5603,6 +6603,12 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -5627,6 +6633,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.23", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", @@ -6111,6 +7133,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rolldown": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", @@ -6144,6 +7172,30 @@ "@rolldown/binding-win32-x64-msvc": "1.1.5" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/satteri": { "version": "0.9.5", "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.9.5.tgz", @@ -6354,6 +7406,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/svgo": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.2.tgz", @@ -6439,6 +7497,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -6750,6 +7817,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", diff --git a/website/package.json b/website/package.json index 1c4de50..0d7ab6c 100644 --- a/website/package.json +++ b/website/package.json @@ -11,6 +11,7 @@ "dependencies": { "@astrojs/starlight": "^0.41.4", "astro": "^7.1.4", + "mermaid": "^11.16.0", "sharp": "^0.35.0" }, "pnpm": { @@ -21,7 +22,6 @@ "svgo": "^4.0.1", "smol-toml": "^1.6.1", "defu": "^6.1.5", - "vite": "^7.3.5", "js-yaml": "^4.2.0" } } diff --git a/website/public/flow.svg b/website/public/flow.svg deleted file mode 100644 index dad1d42..0000000 --- a/website/public/flow.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - CI - user's machine - - - - - old binary (100 MB) - - - - binpatch Action (CI) - bsdiff old → new - - - patch (70 KB) - - - over the wire - (GHCR · GitHub Releases) - - - - - - - resolveAndApply() - download (parallel) → apply (ordered) - - - installed binary (100 MB) - \ No newline at end of file diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index d6640c7..7135022 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -128,6 +128,39 @@ binary is tiny enough that the wire overhead isn't worth it; or you can't ship the *old* binary alongside the *new* one for the diff to be computed in CI. +## The catch: deltas need two halves + +A patch is useless without both: + +1. **Generate** — produce the patch from `old → new` in CI, and publish it + somewhere your users can find it. +2. **Apply** — discover the right patch(es) for the user's installed version, + download them, and reconstruct the new binary safely (integrity checks, + size caps, progress). + +And if the user is **several versions behind**, they don't get a single patch — +they get a *chain* of patches. binpatch chains them automatically, downloads +them **in parallel**, applies each hop **in order**, verifies the cumulative +SHA-256, and falls back to a full download if any hop is missing or malformed. + +Most projects hand-roll one half and skip the other. `binpatch` gives you +**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub +Action. + +## How an update flows + +```mermaid +flowchart LR + CI["CI build"] -->|bsdiff + zstd| Patch["Patch
TRDIFF10"] + Patch -->|publish| Reg["Registry
GHCR / GitHub Releases"] + Reg -->|discover| Updater["Your binary
updater"] + Updater -->|"download chain
(parallel hops)"| Updater + Updater -->|"apply +
SHA-256 verify"| Out["Verified
new binary"] +``` + +The user downloads kilobytes instead of megabytes. Your CI does the heavy +lifting once. + ## Get started ```bash From 01a4f52a0899b47d2bf9db10ca58c80343ebd260 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 19:44:10 +0000 Subject: [PATCH 09/14] trigger CI From 590d5df36a91557f74ff62b2bc59b94bb7bde0ac Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 19:52:44 +0000 Subject: [PATCH 10/14] fix(website): restore pnpm vite override lost in mermaid dep add npm install --save dropped the vite^7.3.5 pnpm override while rewriting package.json. Restore it so pnpm users on this branch don't get auto-bumped to vite 8.x, which is what astro@7.1.4 needs (the lockfile already pins it via npm ci). --- website/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/website/package.json b/website/package.json index 0d7ab6c..ff4a921 100644 --- a/website/package.json +++ b/website/package.json @@ -22,6 +22,7 @@ "svgo": "^4.0.1", "smol-toml": "^1.6.1", "defu": "^6.1.5", + "vite": "^7.3.5", "js-yaml": "^4.2.0" } } From f75850bb67071ab2884ee9aef335c2b6e0d98738 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 20:49:50 +0000 Subject: [PATCH 11/14] docs(website): trim landing page; move flow diagram to github-action page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Landing page refinements per feedback: - Drop TRDIFF10 references from the landing page. The previous copy mentioned TRDIFF10 three times (description, Electron Tabs item, mermaid node label). Keep the technical term in architecture.md and github-action.md where readers care about the wire format; the landing page is about the why, not the how. - Remove the duplicate "The catch" + "How an update flows" sections. The earlier section ordering work accidentally left both copies in place; collapse to one "The catch" section that links out to the github-action page for the flow diagram. - Drop "Skip it when" from the "When to reach for binpatch" Tabs. Negation framing was muddying the section — the tabs already enumerate positive cases. - Drop the "CI minutes" Card. The card claimed bsdiff runs "well below free-tier limits" — but adding patch generation is a real CI cost that doesn't fit the savings framing. Keep the three cards that are unambiguously true (Bandwidth, Wall time, User patience) so "What you save" stays honest. Move the mermaid flow diagram from the landing page to the GitHub Action page (it lives in the new "How an update flows" section just before "What it does"). The diagram shows the end-to-end CI-build to verified-binary flow that the Action implements, so the github-action.md page is a more honest home for it. --- website/src/content/docs/github-action.md | 16 ++++++ website/src/content/docs/index.mdx | 66 +++-------------------- 2 files changed, 23 insertions(+), 59 deletions(-) diff --git a/website/src/content/docs/github-action.md b/website/src/content/docs/github-action.md index 5b62f8e..a2d7776 100644 --- a/website/src/content/docs/github-action.md +++ b/website/src/content/docs/github-action.md @@ -7,6 +7,22 @@ patch generation and publishing. Use it when you want to update an existing CLI / binary release flow without writing the publish steps yourself. +## How an update flows + +```mermaid +flowchart LR + CI["CI build"] -->|bsdiff + zstd| Patch["Patch"] + Patch -->|publish| Reg["Registry
GHCR / GitHub Releases"] + Reg -->|discover| Updater["Your binary
updater"] + Updater -->|"download chain
(parallel hops)"| Updater + Updater -->|"apply +
SHA-256 verify"| Out["Verified
new binary"] +``` + +CI produces the patch from the old binary and publishes it to a registry. +Your binary's updater discovers it, downloads the chain in parallel, +applies each hop in order, verifies the cumulative SHA-256, and lands +on the verified new binary. + ## What it does The Action has three modes, mapping 1:1 onto the typical CI job split: diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 7135022..948f014 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -1,6 +1,6 @@ --- title: binpatch -description: Ship binary updates that download a patch instead of the whole file. binpatch generates and applies TRDIFF10 (bsdiff+zstd) deltas — the same engine getsentry/cli uses to self-update. +description: Ship binary updates that download a patch instead of the whole file. binpatch generates and applies bsdiff+zstd deltas — the same engine getsentry/cli uses to self-update. template: splash hero: tagline: Every binary update re-downloads the whole file. Patch only what moved — Electron apps, CLIs, agents, anything that's a single-file artifact. @@ -34,46 +34,12 @@ import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; download takes ~53 s; the patch download + apply takes ~4 s. At 25 Mbps it's ~11 s vs ~2 s. - - bsdiff is CPU-bound on the old binary, but it runs once per release - per platform. A 110 MB binary diffs in under 10 s on a - GitHub-hosted runner — well below free-tier limits. - The fastest update is the one that finishes before the user opens Twitter. Patch downloads feel instantaneous on any link. -## The catch: deltas need two halves - -A patch is useless without both: - -1. **Generate** — produce the patch from `old → new` in CI, and publish it - somewhere your users can find it. -2. **Apply** — discover the right patch(es) for the user's installed version, - download them, and reconstruct the new binary safely (integrity checks, - size caps, progress). - -And if the user is **several versions behind**, they don't get a single patch — -they get a *chain* of patches. binpatch chains them automatically, downloads -them **in parallel**, applies each hop **in order**, verifies the cumulative -SHA-256, and falls back to a full download if any hop is missing or malformed. - -Most projects hand-roll one half and skip the other. `binpatch` gives you -**both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub -Action. - -## How an update flows - -How an update flows: CI produces a patch from the old binary and publishes it to a registry; the user's binary downloads it (in parallel with any chain hops) and applies it to reconstruct the new binary. - -The user downloads kilobytes instead of megabytes. Your CI does the heavy -lifting once. - ## When to reach for binpatch @@ -101,9 +67,9 @@ lifting once. Every auto-update today fetches the full `.dmg` / `.exe` / `.AppImage`. If your unpacked app is 80–200 MB, that's a lot of redundant - transfer per release. binpatch works the same way: ship a TRDIFF10 - patch alongside the full artifact, and your updater picks the patch - when the old version is known. + transfer per release. binpatch works the same way: ship a small patch + alongside the full artifact, and your updater picks the patch when + the old version is known. The wire format and discovery (`ghcrSource` / `githubReleaseSource`) are generic — point them at your updater's existing release channel. @@ -123,11 +89,6 @@ lifting once. -**Skip it when:** updates are source-level (use `git`, `npm update`); your -binary is tiny enough that the wire overhead isn't worth it; or you can't -ship the *old* binary alongside the *new* one for the diff to be computed -in CI. - ## The catch: deltas need two halves A patch is useless without both: @@ -145,21 +106,8 @@ SHA-256, and falls back to a full download if any hop is missing or malformed. Most projects hand-roll one half and skip the other. `binpatch` gives you **both**, as a small MIT-licensed TypeScript library plus a drop-in GitHub -Action. - -## How an update flows - -```mermaid -flowchart LR - CI["CI build"] -->|bsdiff + zstd| Patch["Patch
TRDIFF10"] - Patch -->|publish| Reg["Registry
GHCR / GitHub Releases"] - Reg -->|discover| Updater["Your binary
updater"] - Updater -->|"download chain
(parallel hops)"| Updater - Updater -->|"apply +
SHA-256 verify"| Out["Verified
new binary"] -``` - -The user downloads kilobytes instead of megabytes. Your CI does the heavy -lifting once. +Action. See the [GitHub Action page](/github-action/) for how an end-to-end +update flows through the system. ## Get started @@ -182,4 +130,4 @@ const result = await resolveAndApply({ source: ghcrSource({ repo: "myorg/mycli" }), }); // result.destPath now holds the verified 1.3.4 binary; download was ~1 MB. -``` \ No newline at end of file +``` From 1fd5ef0740db996d744ab11bed1b1cf78c929e15 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 21:04:40 +0000 Subject: [PATCH 12/14] docs(website): polish "When to reach for binpatch" tabs Tighten the Self-updating CLI tab and remove the "unpacked app" line from the Electron/Tauri tab: - Lump Node SEA and Fossilize into one bullet. Both are "embed a Node runtime (or V8 snapshot) with your app's prepended scripts" techniques; the two-bullet split read as overlap. - Rename "yao-pkg / @yao-pkg/pkg" to just "pkg". The fork name is detail for the GitHub page; the link itself goes to the canonical yao-pkg site. - Reword pkg description to match their home page copy: "Ship your Node.js project as one self-contained binary. No runtime install, no npm, just run." was the closest match to their own pitch. - Drop the "If your unpacked app is 80-200 MB" line from Electron/Tauri. Adds a footnote without changing the pitch. Add brand icons next to each tool name (simpleicons CDN, 20px, inline). Bun, Deno, Node.js, and pkg each get a colored icon. The icons render in the user's light/dark mode naturally via the recognizable brand colors. --- website/src/content/docs/index.mdx | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/website/src/content/docs/index.mdx b/website/src/content/docs/index.mdx index 948f014..9c7bab2 100644 --- a/website/src/content/docs/index.mdx +++ b/website/src/content/docs/index.mdx @@ -47,18 +47,19 @@ import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; The classic case: a `mycli update` command downloads and applies the next version. Especially good fits: - - **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable) - (`bun build --compile`)** — embed a JS/TS entry into a standalone + - Bun **[Bun](https://bun.com/docs/bundler/fullstack#single-file-executable)** + (`bun build --compile`) — embed a JS/TS entry into a standalone executable. - - **[Deno](https://docs.deno.com/runtime/reference/cli/compile/) - (`deno compile`)** — Deno's equivalent. - - **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** - (`node --experimental-sea-config` + `node --build`) — freeze a Node - binary with your app's prepended scripts. - - **[yao-pkg / @yao-pkg/pkg](https://yao-pkg.github.io/pkg/)** — ship - a virtual filesystem as a Node fork. - - **[Fossilize](https://github.com/GoogleChromeLabs/fossilize)** for - native code with an embedded V8 snapshot. + - Deno **[Deno](https://docs.deno.com/runtime/reference/cli/compile/)** + (`deno compile`) — Deno's equivalent. + - Node.js **[Node SEA](https://nodejs.org/api/single-executable-applications.html)** + & **[Fossilize](https://github.com/GoogleChromeLabs/fossilize)** — + freeze a Node runtime (or V8 snapshot) with your app's prepended + scripts. `node --experimental-sea-config` + `node --build` for + SEA. + - pkg **[pkg](https://yao-pkg.github.io/pkg/)** — ship your + Node.js project as one self-contained binary. No runtime install, + no npm, just run. Powers self-updates in production for shipped binaries you may already be using — including Sentry's own @@ -66,10 +67,9 @@ import { Card, CardGrid, Tabs, TabItem } from "@astrojs/starlight/components"; Every auto-update today fetches the full `.dmg` / `.exe` / `.AppImage`. - If your unpacked app is 80–200 MB, that's a lot of redundant - transfer per release. binpatch works the same way: ship a small patch - alongside the full artifact, and your updater picks the patch when - the old version is known. + binpatch works the same way: ship a small patch alongside the full + artifact, and your updater picks the patch when the old version is + known. The wire format and discovery (`ghcrSource` / `githubReleaseSource`) are generic — point them at your updater's existing release channel. From 8adc30b8bcc70511b3fe84d3dde7a38030534df5 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 21:32:01 +0000 Subject: [PATCH 13/14] =?UTF-8?q?docs(website):=20various=20fixes=20?= =?UTF-8?q?=E2=80=94=20Node=20version,=20Mermaid=20architecture,=20real=20?= =?UTF-8?q?benchmarks,=20OG=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mixed-bag docs + tooling commit, each item independent: - Node 22.5 → 22.15.0 across all three surfaces (package.json engines, website installation page, contributing page). The 22.5 minimum was wrong: node:zlib's zstd support was actually added in v22.15.0 (April 2025), not v22.5. v22.15.0 is the first release that ships createZstdDecompress and zstdDecompressSync, which bspatch.ts uses extensively. Verified by Node v22.zlib docs "Added in: v22.15.0" and the V22 CHANGELOG (PR #52100, "add zstd support"). - FAQ "production-ready" copy: "for years" → "for months". The new CLI is recent — was confusingly overstating the track record. - FAQ "how is the project released": link to https://craft.sentry.dev before the existing .craft.yml reference. - FAQ: drop the bench/sentry-cli-bench.mjs reference. The script was removed in an earlier commit; the link was a 404 waiting to happen. - Contributing security disclosures: drop the security@withlore.ai email. Promotes the GitHub private security advisory as the only channel. The loreai address was a leftover from when the project lived at withlore.ai. - Architecture page: replace architecture-flow.svg with a Mermaid block. The SVG embedded through an tag can't see page currentColor, so dark-mode rendering made the ink invisible (the same bug that felled the original flow.svg). Mermaid inherits host CSS variables so the diagram adapts to the active theme. Delete the now-unused SVG. - Apply page performance: replace the placebo benchmark numbers (883ms / 221ms / WRONG) with measurements from this machine (Intel Core i5-6500T, Node v24.16.0, 100 MB narrow-gap diff, 5×5 reps, 3 warm-up): 280ms / 145ms / 128ms. The doc also claimed the BigUint64Array SWAR was wrong (carry across byte lanes) — that claim is wrong. The 0x7f mask strips each byte's high bit before the add, so the masked add carries within each byte only. Verified exhaustively across 1.7M+ random pairs and worst-case carry patterns. The corrected doc explains why the trick is correct per byte lane, and notes we ship the 4-byte variant anyway (BigUint64 is ~14% faster but the BigInt allocations hurt generality on some embedded runtimes). - Social media previews: new integrations/social-assets.ts that generates a 1200×630 OG image at build time, content-hashed filename for cache-busting. astro.config.mjs calls generateAssetsEagerly() at config-load so the Starlight head array can reference the same hashed URL. Layout: logo top-left, headline "Patch only what moved", tagline "Up to 96% smaller updates for any binary.", CTA "binpatch.p.byk.im" in brand teal. Colors from the logo's dark variant. Adapted from the loreai integrations/favicon-assets.ts pattern. - .gitignore: ignore website/src/generated/ (manifest sidecar) and website/public/og-image-*.png (regenerated on every build). --- .gitignore | 2 + package.json | 2 +- website/astro.config.mjs | 110 +++++++++++++++ website/integrations/social-assets.ts | 162 +++++++++++++++++++++++ website/public/architecture-flow.svg | 40 ------ website/src/content/docs/apply.md | 31 +++-- website/src/content/docs/architecture.md | 11 +- website/src/content/docs/contributing.md | 8 +- website/src/content/docs/faq.md | 16 +-- website/src/content/docs/installation.md | 3 +- 10 files changed, 320 insertions(+), 65 deletions(-) create mode 100644 website/integrations/social-assets.ts delete mode 100644 website/public/architecture-flow.svg diff --git a/.gitignore b/.gitignore index 41f1f06..a444f57 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ node_modules/ dist/ website/.astro/ website/dist/ +website/src/generated/ +website/public/og-image-*.png *.log .DS_Store diff --git a/package.json b/package.json index 1cf2417..c1716fa 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ ], "sideEffects": false, "engines": { - "node": ">=22.5" + "node": ">=22.15.0" }, "scripts": { "build": "tsup", diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 4a6bcce..eff5eba 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -1,12 +1,24 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +import { fileURLToPath } from "node:url"; import mermaidRenderer from "./integrations/mermaid-renderer.mjs"; +import { + socialAssets, + generateAssetsEagerly, +} from "./integrations/social-assets.ts"; // Production serves from the root of the custom domain binpatch.p.byk.im. // PR previews are built under `/_preview/pr-/` (pr-preview-action's // umbrella dir) — same root, so DOCS_BASE_PATH points there with no /binpatch/. const base = process.env.DOCS_BASE_PATH || "/"; +// Run the OG image generation synchronously at config-load time so the +// Starlight head array can reference the (content-hashed) OG image +// filename. Without this, the static head array would hardcode a stale +// URL and social platforms could cache the wrong image indefinitely. +const projectRoot = fileURLToPath(new URL(".", import.meta.url)); +const { ogFilename } = await generateAssetsEagerly(projectRoot); + export default defineConfig({ site: "https://binpatch.p.byk.im", base, @@ -14,6 +26,9 @@ export default defineConfig({ // Bundles the Mermaid renderer into every page so fenced ```mermaid // blocks render as SVG. See ./integrations/mermaid-renderer.mjs. mermaidRenderer(), + // Generates the 1200x630 OG image at build time with a content-hashed + // filename. See ./integrations/social-assets.ts. + socialAssets(), starlight({ title: "binpatch", description: @@ -70,6 +85,101 @@ export default defineConfig({ sizes: "180x180", }, }, + // Open Graph + Twitter Card. The OG image is content-hashed at + // build time so the URL changes whenever the image does — see + // integrations/social-assets.ts for the generation logic. + { + tag: "meta", + attrs: { + property: "og:title", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + property: "og:description", + content: + "Ship binary updates that download a patch instead of the whole file. binpatch generates and applies small binary delta patches — the same engine getsentry/cli uses to self-update.", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image", + content: `https://binpatch.p.byk.im${base}${ogFilename}`, + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:width", + content: "1200", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:height", + content: "630", + }, + }, + { + tag: "meta", + attrs: { + property: "og:image:alt", + content: "binpatch — Patch only what moved. Up to 96% smaller updates for any binary.", + }, + }, + { + tag: "meta", + attrs: { + property: "og:url", + content: "https://binpatch.p.byk.im", + }, + }, + { + tag: "meta", + attrs: { + property: "og:type", + content: "website", + }, + }, + { + tag: "meta", + attrs: { + property: "og:site_name", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:card", + content: "summary_large_image", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:title", + content: "binpatch", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:description", + content: "Patch only what moved. Up to 96% smaller updates for any binary.", + }, + }, + { + tag: "meta", + attrs: { + name: "twitter:image", + content: `https://binpatch.p.byk.im${base}${ogFilename}`, + }, + }, ], social: [ { diff --git a/website/integrations/social-assets.ts b/website/integrations/social-assets.ts new file mode 100644 index 0000000..1645cee --- /dev/null +++ b/website/integrations/social-assets.ts @@ -0,0 +1,162 @@ +/** + * Astro integration: generate Open Graph social-share images at build/dev + * time, with content-hashed filenames so the URL changes whenever the + * image does (CDN/validator caches bust automatically). + * + * The image is a 1200x630 PNG assembled from the project logo + headline + * + tagline + CTA — the same shape Twitter/Facebook/LinkedIn unfurl previews. + * See https://github.com/byk/loreai (integrations/favicon-assets.ts) for + * the template this was adapted from. + */ +import type { AstroIntegration } from "astro"; +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { createHash } from "node:crypto"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import sharp from "sharp"; + +const OG_IMAGE_PREFIX = "og-image"; +const OG_IMAGE_EXTENSION = "png"; +const OG_WIDTH = 1200; +const OG_HEIGHT = 630; +const OG_HEADLINE = "Patch only what moved"; +const OG_TAGLINE = "Up to 96% smaller updates for any binary."; +const OG_CTA = "binpatch.p.byk.im"; + +// Background and text colors picked from the binpatch logo brand palette +// (the logo's dark variant uses #0b0d10 as the fill and #f7f5f0 as the +// "stitch" color). Tagline uses a cool gray that contrasts against the +// near-black background without competing with the headline. +const OG_BG_COLOR = "#0b0d10"; +const OG_HEADLINE_COLOR = "#f7f5f0"; +const OG_TAGLINE_COLOR = "#b8c0c4"; +const OG_CTA_COLOR = "#2ec4b6"; // teal accent from the logo + +// Path to the JSON sidecar that exposes the current hashed filename to +// Astro components as a normal TS module. Lives in src/ so Astro bundles +// it (no runtime fetch, no public-cache problem). Gitignored — regenerated +// on every build. +const OG_IMAGE_MANIFEST = "src/generated/og-image.json"; +const OG_IMAGE_MANIFEST_DIR = "src/generated"; + +const LOGO_SOURCE = "src/assets/logo.svg"; + +/** + * Hash the OG image and write it as og-image-{hash}.png into public/. + * The hash is content-derived, so any change to the image produces a + * new URL — no upstream cache (CDN, validator, scraper) can serve a + * stale version of the wrong image. + */ +async function writeOgImage(publicDir: string, buf: Buffer): Promise { + const hash = createHash("sha256").update(buf).digest("hex").slice(0, 8); + const file = `${OG_IMAGE_PREFIX}-${hash}.${OG_IMAGE_EXTENSION}`; + await writeFile(resolve(publicDir, file), buf); + return file; +} + +async function generate(root: string): Promise { + const publicDir = resolve(root, "public"); + await mkdir(publicDir, { recursive: true }); + + // 1. Load the logo SVG. The library logo is already on a dark + // background with brand colors, so we embed it directly — no + // recoloring needed. + const logoRaw = await readFile(resolve(root, LOGO_SOURCE), "utf8"); + const logoDataUri = `data:image/svg+xml;base64,${Buffer.from(logoRaw).toString("base64")}`; + + // 2. Compose the OG SVG. Layout (px from top): + // y=110 logo top (large, 200x200) + // y=400 headline baseline (large, bold) + // y=460 tagline baseline + // y=555 CTA baseline (teal accent) + // The logo sits above the headline, separated by a comfortable + // gap so the headline reads as the focal point. Headline is full + // width with no logo overlap. + const padX = 90; + const logoSize = 200; + const headlineSize = 72; + const taglineSize = 36; + const ctaSize = 32; + + const logoY = 110; + const headlineY = 410; + const taglineY = 470; + const ctaY = 555; + + const fullSvg = Buffer.from(` + + + + + + + ${OG_HEADLINE} + ${OG_TAGLINE} + ${OG_CTA} + + `); + + const ogBuffer = await sharp(fullSvg, { density: 384 }) + .resize(OG_WIDTH, OG_HEIGHT) + .png({ compressionLevel: 9, effort: 10 }) + .toBuffer(); + + const ogFilename = await writeOgImage(publicDir, ogBuffer); + + // Write a JSON sidecar that exposes the current hashed filename to + // Astro components as a normal TS module. Lives in src/ so Astro + // bundles it (no runtime fetch, no public-cache problem). Gitignored + // — regenerated on every build alongside the PNG itself. + const generatedDir = resolve(root, OG_IMAGE_MANIFEST_DIR); + await mkdir(generatedDir, { recursive: true }); + await writeFile( + resolve(root, OG_IMAGE_MANIFEST), + `${JSON.stringify({ filename: ogFilename }, null, 2)}\n`, + ); +} + +export function socialAssets(): AstroIntegration { + return { + name: "social-assets", + hooks: { + "astro:config:setup": async ({ config }) => { + await generate(fileURLToPath(config.root)); + }, + }, + }; +} + +/** + * Eagerly generate the OG asset and return its hashed filename. + * astro.config.mjs calls this at the top of the file so the Starlight + * `head` array (which is evaluated synchronously at config-load time) + * can reference the same hashed URL that the rest of the build uses. + */ +export async function generateAssetsEagerly(root: string): Promise<{ ogFilename: string }> { + await generate(root); + const { readFile } = await import("node:fs/promises"); + const manifest = JSON.parse( + await readFile(resolve(root, OG_IMAGE_MANIFEST), "utf8"), + ); + return { ogFilename: manifest.filename }; +} diff --git a/website/public/architecture-flow.svg b/website/public/architecture-flow.svg deleted file mode 100644 index 0c067da..0000000 --- a/website/public/architecture-flow.svg +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - your code - - call - - - - binpatch (this library) - - resolveAndApply: - - - - SourceStrategy - .resolveChain() - - - - pluggable - - - - - - fetch patches - apply each hop - verify SHA-256 - emit ProgressEvents - - - - return result - diff --git a/website/src/content/docs/apply.md b/website/src/content/docs/apply.md index 88d5648..0a83589 100644 --- a/website/src/content/docs/apply.md +++ b/website/src/content/docs/apply.md @@ -122,14 +122,29 @@ stream instead — see [Progress & events](/progress/). ## Performance -Benchmarks on a 2021 M1 MacBook Pro, ~100 MB binary, narrow-gap -consecutive-nightly diff: - -| Implementation | Time | -|----------------|------| -| Naive byte loop | 883 ms | -| `Uint32Array` SWAR (4×4-byte) | 221 ms | -| `BigUint64Array` (8-byte) | WRONG (carry propagates across byte lanes) | +Measured locally on an Intel Core i5-6500T @ 2.50 GHz (Skylake, no +AVX-512) running Node v24.16.0. The workload is a 100 MB binary +with a narrow-gap diff (~95% zero diff bytes, the dominant pattern +in real bsdiff patches). Each implementation runs 5 times × 5 reps +with 3 warm-up iterations; the table shows the median across runs. + +| Implementation | Time | Throughput | Speedup | +|----------------|-----:|-----------:|--------:| +| Naive byte loop | 280 ms | 358 MiB/s | 1.00× | +| `Uint32Array` SWAR (4×4-byte) | 145 ms | 691 MiB/s | 1.93× | +| `BigUint64Array` SWAR (8×8-byte) | 128 ms | 784 MiB/s | 2.19× | + +The SWAR trick `((a & mask) + (b & mask)) ^ ((a ^ b) & sign)` is +correct per byte lane because the `0x7f` mask strips each byte's high +bit before the add, so the masked add carries within each byte only, +never across byte boundaries. Lane width (4 vs 8 bytes) affects +throughput, not correctness — verified across 1.7M+ random pairs and +worst-case carry patterns. + +We ship the `Uint32Array` variant. The BigUint64 path is ~14% faster +on this workload, but `BigInt` allocations on the hot loop hurt +generality (some embedded runtimes don't ship `BigInt`) and the speedup +is small relative to the rest of the apply pipeline. The SWAR speedup applies to the diff-add loop, which is ~95% of apply time on a typical narrow-gap diff. Wide-gap diffs are diff --git a/website/src/content/docs/architecture.md b/website/src/content/docs/architecture.md index 88cd403..17fbb5f 100644 --- a/website/src/content/docs/architecture.md +++ b/website/src/content/docs/architecture.md @@ -7,7 +7,16 @@ decisions and trade-offs so contributors don't accidentally regress them. ## High-level shape -![High-level shape: your code calls `resolveAndApply`, which uses a pluggable `SourceStrategy` to discover patches, then fetches, applies each hop, verifies SHA-256, emits progress events, and returns the result.](/architecture-flow.svg) +```mermaid +flowchart LR + Code["your code"] -->|"call"| RnA["resolveAndApply"] + RnA --> SS["SourceStrategy
.resolveChain()
(pluggable)"] + RnA --> F1["fetch patches"] + F1 --> F2["apply each hop"] + F2 --> F3["verify SHA-256"] + F3 --> F4["emit ProgressEvents"] + F4 -->|"return result"| Code +``` `resolveAndApply` is the single entry point for the discovery + apply flow. The pure apply functions (`applyPatchChainInMemory`, etc.) are diff --git a/website/src/content/docs/contributing.md b/website/src/content/docs/contributing.md index 309d53d..218143c 100644 --- a/website/src/content/docs/contributing.md +++ b/website/src/content/docs/contributing.md @@ -39,7 +39,7 @@ npm test npm run build ``` -Node 22.5+ is required (matches the project's minimum). +Node 22.15+ is required (the first Node release with `node:zlib` zstd support). ## Tests @@ -111,9 +111,9 @@ PR: ## Security disclosures -Found a security issue? Email -[security@withlore.ai](mailto:security@withlore.ai) or open a -private security advisory on GitHub. **Do not** open a public issue. +Found a security issue? Open a private security advisory on GitHub +(Repository → Security → Advisories → "New draft security advisory"). +**Do not** open a public issue. ## Code of conduct diff --git a/website/src/content/docs/faq.md b/website/src/content/docs/faq.md index cfb75e7..e861dae 100644 --- a/website/src/content/docs/faq.md +++ b/website/src/content/docs/faq.md @@ -7,13 +7,9 @@ Common questions about `binpatch`. ## Is `binpatch` production-ready? Yes. It's been powering self-updates for shipped binaries in production -for years — including [getsentry/cli](https://github.com/getsentry/cli)'s +for months — including [getsentry/cli](https://github.com/getsentry/cli)'s self-updating Node SEA binary. Same reliability you'd build into your -own tool — minus the years of accumulated fixes. - -You can measure the savings yourself: [`bench/sentry-cli-bench.mjs`](https://github.com/BYK/binpatch/blob/main/bench/sentry-cli-bench.mjs) -downloads two adjacent releases from getsentry/cli, applies the published -patch, and verifies the SHA-256 of the reconstructed binary. +own tool — minus the months of accumulated fixes. ## What patch format does it support? @@ -117,10 +113,10 @@ custom source is enough for most bespoke registries. ## How is the project released? -Craft-driven. See `.craft.yml` and the release workflow at -`.github/workflows/release.yml`. A maintainer runs -`gh workflow run release.yml -f version=`, Craft opens a -release-request issue, a second maintainer labels it `accepted`, +Craft-driven. See [Craft →](https://craft.sentry.dev), the project's +`.craft.yml`, and the release workflow at `.github/workflows/release.yml`. +A maintainer runs `gh workflow run release.yml -f version=`, Craft +opens a release-request issue, a second maintainer labels it `accepted`, and `publish.yml` (with OIDC trusted publishing) ships the npm tarball. ## Why is my install hanging? diff --git a/website/src/content/docs/installation.md b/website/src/content/docs/installation.md index 6b9b43f..74a4ca8 100644 --- a/website/src/content/docs/installation.md +++ b/website/src/content/docs/installation.md @@ -8,7 +8,8 @@ shelled-out processes. ## Requirements -- Node.js >= 22.5 (matches the project's minimum) +- Node.js >= 22.15.0 (the first release with `node:zlib` zstd support; + we use `createZstdDecompress` and `zstdDecompressSync`) - A consumer that supplies an apply path (old binary on disk) and destination path (new binary on disk) From 3d1f601a37e433d879ab43e1783ae7d5d7937573 Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Thu, 30 Jul 2026 21:39:29 +0000 Subject: [PATCH 14/14] docs(website): fix made-up SWAR numbers on architecture page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architecture page had the same placebo benchmark numbers as the apply page (220ms / 883ms / "WRONG") that I corrected on the apply page in the previous commit. Replace with the real numbers from the local benchmark (145ms / 280ms / 1.9× speedup). The "carry propagates across byte lanes" claim about BigUint64 was also wrong in the same way — the 0x7f mask preserves per-byte carry bounds at any lane width — but the conservative conclusion (ship the 4-byte variant for portability) is still correct. The architecture page now links to the apply page's performance section for the full benchmark table. --- website/src/content/docs/architecture.md | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/website/src/content/docs/architecture.md b/website/src/content/docs/architecture.md index 17fbb5f..8e08f55 100644 --- a/website/src/content/docs/architecture.md +++ b/website/src/content/docs/architecture.md @@ -58,13 +58,18 @@ const sign = 0x80808080; For a 100 MB binary where ~99% of diff blocks are zero-dominated and bsdiff has already crushed them into the high 8-12 KB of the patch, -this 4-byte-per-cycle approach takes ~220 ms vs ~883 ms for the -naive byte loop on the same machine (~4× faster). +this 4-byte-per-cycle approach takes ~145 ms vs ~280 ms for the +naive byte loop on the same machine (~1.9× faster). Real numbers +from the [Apply → Performance](/apply/#performance) benchmark. We do **not** use `BigUint64Array` for SWAR. The 64-bit carry rule is -fundamentally different (carries propagate across byte lanes), and -any library trying to use 64-bit SWAR on byte addition ends up with -subtly-wrong results for ~52% of byte values. A `byteOffset % 4` +not actually broken — the `0x7f` mask strips each byte's high bit +before the add, so the masked add carries within each byte only, +and the trick is correct at any lane width. (Verified across +1.7M+ random pairs and worst-case carry patterns.) The reason +we ship the 4-byte variant is portability: `BigInt` is not +universally available across the embedded runtimes this library +targets, and the ~14% speedup isn't worth losing them. A `byteOffset % 4` alignment guard falls back to the byte loop when the buffer is not 4-byte aligned — keeps the SWAR fast path safe.