# Release binary-size policy

> .github/release-binary-size-policy.json is the source of truth for release binary-size admission. It answers two different questions with two different numbers.

Website: https://harnlang.com/dev/release-binary-size-policy.html

This page documents Harn, which is pre-1.0. Language, standard library, and CLI APIs may change. If the intended version is unclear, clarify before using this page.

---

`.github/release-binary-size-policy.json` is the source of truth for release
binary-size admission. It answers two different questions with two different
numbers.

## The distribution fuse

`distribution_fuse_bytes` is a fail-closed absolute ceiling: an artifact above
it is not distributable, whatever produced it. It is an emergency fuse, not a
ratchet, so the contract requires it to sit between 10% and 15% above the
accepted baseline. Tighter and it becomes a release-day byte cliff — the
schema-1 policy this replaced allowed at most 4 MiB of headroom above a ~222 MiB
baseline, which moved the ceiling 212 → 213 → 216 → 218 → 220 MiB inside one
release line, each raise its own PR and its own exact release build after the
useful work was already certified. Looser and it stops being a fuse.

Raising the fuse is a deliberate policy decision, taken rarely, alongside a
baseline refresh.

## The growth signal

`growth.warn_bytes` and `growth.warn_percent_hundredths` are the early
complexity signal. A crossing needs **both**: an absolute floor, so a few
megabytes on a 200+ MiB artifact is not treated as news, and a ratio, so the
same thresholds still mean something on a smaller target.
`warn_percent_hundredths` is an integer per-ten-thousand — 200 is 2.00% — so the
contract carries no float comparison.

A crossing is reported, never blocking. It warns wherever it is measured — in
the release build, in the pull-request comment, and in the pre-tag gate — and
nothing refuses on it. Only the distribution fuse refuses. This is the lesson of
the v0.10.126 cut, which spent two ~40-minute Linux builds being refused by a
baseline last refreshed at 0.10.118 while the artifact sat 28 MiB under the
fuse: a growth number describes code that ships fine, and a stale baseline
should cost a reader's attention rather than a release.

An `accepted_growth` entry suppresses the warning for a known, bounded
increase. An entry names `against_baseline_sha`, the byte allowance, and a short
reason.
It is scoped to the baseline it was written against rather than to the commit
that caused the growth, because a contributor cannot predict the squashed `main`
SHA their PR will become — but they do know which baseline they measured
against. Validation rejects an entry whose `against_baseline_sha` is not the
current baseline, so refreshing the baseline forces every stale acceptance to be
dropped in the same change.

## Comparability

Raw bytes cannot classify causality. Between v0.10.52 candidates with identical
source and toolchain, moving x86_64 release codegen units from 16 to 8 shed
8,407,168 bytes — codegen duplication and layout, not eight megabytes of
product. Subtracting two differently-built artifacts produces a number with no
causal meaning.

The baseline therefore carries a `build_identity`: profile, codegen units, LTO,
strip, rustc, and whether the AOT payload is embedded. The check observes the
same fields for the build in front of it (from `CARGO_PROFILE_RELEASE_*` and
`rustc -vV`, falling back to `[profile.release]` in the workspace manifest). If
any diverge, the report says which, classifies the comparison as
`not-comparable`, and draws no growth conclusion. The fuse still applies.

`aot_embedded` is a boolean, not a byte count, on purpose: whether the payload
is compiled in is a build-identity question, while how large it has become is
product growth this policy exists to report.

## Attribution

`scripts/check_binary_size.harn` writes `binary-size.json` beside the report:
the same verdict in a closed
`harn.release_binary_size_report.v1` record, including a `blocking` field
derived from the failures list. Cross-repo readers — the pre-tag gate deciding
whether a commit has already been measured, and the release tail writing
observed bytes back into the baseline — read that record rather than parsing
prose.

`scripts/check_binary_size.harn` also writes `binary-size.txt` with the fuse state,
the baseline delta in bytes and hundredths of a percent, the comparability
verdict, and both build identities, so a blocked release carries its own
attribution instead of costing a second exact release build. The release job
also emits `elf-sections.txt` (`size -A -d`) and a `cargo bloat --crates`
report.

Given a baseline `size -A -d` table via `--baseline-sections`, the report adds
per-section deltas and classifies them as `codegen-layout`, `content`, or
`mixed`: `.text`, `.eh_frame`, `.eh_frame_hdr`, and `.gcc_except_table` move
together on a layout change, while an embedded asset lands in `.rodata` and
friends. The release workflow does not yet carry a stored baseline table across
runs, so the delta is available to an operator comparing two archived reports
rather than automatically.

## Where each level runs

`.github/release-binary-size-policy.jq` is the bootstrap validator, used before
the release job installs or builds Harn. It decodes the same closed contract and
emits the fuse in bytes. `scripts/release_binary_size_policy.harn` owns the
typed contract and the verdict; `scripts/check_binary_size.harn` applies it. The
focused PR gate exercises both validators against unknown fields, invalid
metadata, duplicate targets, topology failures, both fuse bounds, and a stale
acceptance.

In `build-release-binaries.yml` the fuse is a pure-bash arithmetic step, so it
holds even when the binary it just measured cannot run. The growth signal is a
separate step that runs the freshly built `harn` against the policy. Collapsing
them back into one step is the regression this split exists to prevent.

That job uploads its report as `harn-binary-size-<target>-<source-sha>`. The
name is source-qualified because a benchmark dispatch's run head SHA is the
policy ref, not the revision it measured, so run metadata alone cannot answer
"has this exact commit already been measured?". harn-bump-fleet's pre-tag gate
asks exactly that before spending a release build.

## The pull-request signal

`ci.yml`'s `binary-size-signal` job comments on every Rust pull request with
bytes, the change against main, and the remaining fuse headroom. It is advisory
by construction: it is absent from `ci-status`, so a growth number can never
block a merge.

It adds no build. It measures the debug `harn` the workspace-test producer
already publishes as `harn-cli.tar.zst`, and compares it against main's last
measurement of the same binary built the same way. A release-profile build here
would cost about forty minutes per pull request to answer a question that is
nearly always "no change".

The debug binary is a proxy and is treated as one. Only the ratio is compared,
never the absolute bytes against a release baseline, because a debug binary is
several times the size of the stripped artifact that ships. The comment carries
main's last real release measurement alongside it, so a reader gets the proxy
delta and the shipped number in one place.

Both baselines are read from the newest artifact a main-headed run published, so
main is the only writer and a pull request cannot poison what the next one
reads. When nothing is recorded, the comment says nothing is recorded. It never
renders an absent measurement as zero growth.

`--fuse-mb`, `BINARY_SIZE_FUSE_MB`, and `BINARY_SIZE_FUSE_BYTES` are explicit
local or recovery overrides. They move the fuse only: the growth signal always
compares against the recorded baseline, so overriding the ceiling cannot
silently retire the early signal too.

## Refreshing the baseline

The release tail in harn-bump-fleet refreshes the baseline automatically once a
release is proven published: it reads `binary-size.json` from that release's own
build, writes the version, source SHA, byte count, observation time, and build
identity, drops every `accepted_growth` entry, and opens the pull request. This
step does not depend on fleet convergence, because convergence is skipped when
an earlier tail phase fails and a baseline that only refreshes on a perfect
release is a baseline that goes stale.

Refreshing it by hand is the fallback. Record the candidate version, source SHA,
byte count, observation time, and build identity from that Actions run, and drop
any `accepted_growth` entries written against the previous baseline. Do not
estimate from a local binary built for another target.

Only x86_64 Linux is budgeted today. Adding another target entry is data
preparation, not activation: also add the matching workflow gate and report
surface before treating that target as enforced.

---

## Read next

- [Release runner policy](https://harnlang.com/dev/release-runner-policy.md)
- [Reusable bump-harn workflow](https://harnlang.com/dev/reusable-bump-harn-runtime.md)
