Skip to content

fix(packaging): trim the package to 24.99 MB and give size budgets headroom - #3568

Merged
bokelley merged 2 commits into
mainfrom
bokelley/package-size-headroom
Nov 1, 2026
Merged

bokelley merged 2 commits into
mainfrom
bokelley/package-size-headroom

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Why

scripts/check-package-size.mjs pinned the packed-byte cap and the packed file count to almost exactly the current measurement. Two PRs that each added a file or a few KB passed alone and broke main together: #3510 + #3541 went over the byte cap, and #3564 + #3559/#3560 went one file over. Each fix bumped the cap (#3548, #3565). The cap crept from about 26,000,000 to 26,910,000 bytes, and the storage package budget and the focused-client eager-graph count used the same exact-pin pattern.

Budget design

All numbers live in scripts/package-size-budget.json. The policy is in the header of scripts/check-package-size.mjs.

  1. Ceilings with headroom. Each package has packed-byte, unpacked-byte and file-count ceilings set at the measured size plus deliberate headroom:

    Package Metric Measured Ceiling Headroom
    @adcp/sdk packed bytes 24,993,401 26,000,000 4.0%
    @adcp/sdk unpacked bytes 129,047,083 135,000,000 4.6%
    @adcp/sdk files 8,176 8,450 3.4%
    @adcp/reporting-storage packed bytes 447,064 490,000 9.6%
    @adcp/reporting-storage unpacked bytes 2,039,835 2,250,000 10.3%
    @adcp/reporting-storage files 129 150 +21 files

    When less than two PRs' allowance of headroom is left, the check prints a warning, so someone raises the ceiling or trims the package before parallel PRs can collide on it. The storage package gets proportionally more room because a single module is a large share of it.

  2. Per-PR growth against the merge base. On main, library-checks saves each commit's measurement to the Actions cache (package-size-<os>-<sha>). On a pull request, it restores the record for the merge commit's first parent, which is the exact merge base, and checks this PR's delta. Allowances: @adcp/sdk +100,000 packed bytes / +25 files, @adcp/reporting-storage +15,000 / +8. Recent feature PRs added 30 to 60 KB and 4 to 20 files, and the 3.3.0-beta.3 adoption added about 230 KB and 21 files. Growth over the allowance fails unless the same PR raises the matching ceiling, so the reviewer sees and approves the decision in the diff. Two PRs that both raise a ceiling conflict on the same line and get merged one after the other. The delta table goes in the job summary. If there is no record for the merge base (main's run hasn't finished yet, or the cache was evicted), only this comparison is skipped and the step prints a notice; the ceilings still apply. I chose this over a ratchet that CI commits back to main, because that would need write permissions and bot commits.

  3. Structural checks without headroom (unchanged or new): no source maps, no skill snapshots, no expanded bundled schemas, required schema/compliance roots, no compliance domains/ alias, protocols/ present in every compliance root, and exact one-line ESM facades for the four large generated declarations. The bundled-schema archive limit goes from 2,048,000 to 2,200,000 bytes (measured 2,001,269). That leaves room to swap a protocol version and still catches the old expanded bundles, which were tens of MB.

The focused-client eager graph count in test/lib/client-core-public-export.test.js goes from an exact <= 216 to a ceiling of 230. The named heavy-module exclusions remain the real guard: an accidental barrel import adds hundreds of modules (index.mjs reaches 605).

The storage package's budgets moved into the shared budget file, and check:package-size now checks both packages in one process.

Trim: 26,862,556 → 24,993,401 packed bytes (-1,869,155, -7.0%); 8,529 → 8,176 files

I diffed the 17.0.0-beta.11 tarball against 17.0.0-beta.4 (25,749,296 bytes) and 16.6.1 (20,788,430). The growth since beta.4 is real content: the 3.3 schema and compliance bundles (+1.0 MB of schemas, +0.7 MB of test vectors, unpacked), reporting subscriptions, and per-tool types. 16.6.1 → 17 added a fourth protocol version. Nothing stale was riding along. All the waste was duplication:

Change Packed bytes saved Files Justification
Exclude compliance/cache/*/domains/** (package.json files) 286,556 -353 In all four bundles (3.0.25, 3.1.31, 3.2.7, 3.3.0-beta.3), domains/ is byte-identical to protocols/ (diff -rq). The storyboard loader (listBundles, resolveStoryboardsForCapabilities) reads only protocols/<id> and never uses index.domains. Nothing in src/ or bin/ reads domains/. Loading every bundle from the extracted tarball gives the same bundle and storyboard counts as the repo for all four versions (63/125/213/237 storyboards).
One-line ESM facades for types/core.generated.d.mts, types/tools.generated.d.mts, registry/types.generated.d.mts 1,174,602 0 These were 5.7 MB of byte-for-byte copies of type-only declarations (interfaces and type aliases, no values, no package imports). They now export * from './<name>.js', the pattern schemas.generated.d.mts already shipped. The generator refuses to emit a facade if one of those files gains a value declaration, a default export, or a package import. attw, publint, check:adopter-types (+narrow), typecheck:examples and the packed-artifact checks pass.
Compact JSON for the MCP tools/list request projections (scripts/copy-schemas-to-dist.ts) 407,997 0 They are self-contained (refs inlined), and indentation was about a quarter of their 26 MB. The loader only JSON.parses them and tools/list serves the parsed object, so the wire output is unchanged.

Things I checked and kept:

  • Test vectors in the compliance caches: storyboards and adopters read them through the documented cache path.
  • The bundled schema archives and per-version schemas-data: the runtime validator uses them.
  • Per-tool types/*.d.ts slices: they are the ./types/* entry points.
  • Old migration guides: shipped README, llms.txt and guides link to them by relative path, and verify-package.mjs asserts MIGRATION-v8.md ships. They are about 130 KB compressed.
  • Minifying the remaining canonical schemas would save another ~318 KB. I left that out because those files are the upstream-authored schema tree. It is an option for a follow-up.

Verification

  • npm run check:package-size: ceilings pass. With a baseline of the beta.11 measurement, the delta table shows -1,869,162 bytes / -353 files. A synthetic +150 KB baseline fails with the remediation message, and a mismatched PACKAGE_SIZE_BASE_SHA skips with a notice.
  • npm run check:package, which runs runtime-compat, the packed-artifact checks, publish-protocol-artifacts, typecheck:examples, publint --strict, and attw for both packages.
  • npm run check:adopter-types and check:adopter-types-narrow.
  • npm run verify:package: a clean-room install with peer floors, Node + Bun, MCP negotiation, the MCP schema smoke, and a browser bundle.
  • Targeted unit tests: MCP modern negotiation, the schema loader, createAdcpServer, conformance schema loader, the d.mts generator (new facade and type-only guard cases), the budget logic (new), client-core, the CI artifact contract, and publish-protocol artifacts.
  • npm run lint:workflows.

A changeset (@adcp/sdk patch) records the user-visible file changes. The domains/ → protocols/ note is the only one an adopter could notice.

🤖 Generated with Claude Code

bokelley and others added 2 commits October 31, 2026 23:49
Trim @adcp/sdk from 26,862,556 to 24,993,401 packed bytes (8,529 to
8,176 files) without changing runtime behavior:

- Exclude compliance/cache/*/domains/ from the package. Every bundle's
  domains/ is a byte-identical copy of protocols/, the only directory the
  storyboard loader reads (-286,556 bytes, -353 files).
- Emit one-line ESM facades for the type-only generated declarations
  (types/core.generated, types/tools.generated, registry/types.generated),
  as types/schemas.generated already did, instead of copying 5.7 MB of
  declarations (-1,174,602 bytes).
- Store the self-contained MCP tools/list request projections as compact
  JSON. They are only ever JSON.parse'd (-407,997 bytes).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The packed-byte and file-count budgets were pinned to the measurement, so
two PRs that each added a file or a few KB passed alone and broke main
together (#3510 + #3541, #3564 + #3559/#3560). Replace the pins with:

- Ceilings with headroom in scripts/package-size-budget.json: @adcp/sdk
  26,000,000 packed bytes / 135,000,000 unpacked / 8,450 files against a
  measured 24,993,401 / 129,047,083 / 8,176 (about 4%), and
  @adcp/reporting-storage 490,000 / 2,250,000 / 150. A warning fires when
  less than two PRs' allowance of headroom is left.
- A per-PR growth check. main caches each commit's measurement; a pull
  request compares itself with the record for its exact merge base and may
  add 100 KB / 25 files to the SDK (15 KB / 8 files to the storage
  package). More needs the same PR to raise the ceiling, so the growth is
  reviewed in the diff. A missing record skips only this comparison.
- The focused-client eager graph count becomes a ceiling (230, measured
  216) instead of an exact pin; the named heavy-module exclusions stay.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

@aao-secretariat aao-secretariat Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Ladon verdict: Approve

Approve — package-size tooling PR with no blocking findings.

Checked:

  • scripts/check-package-size.mjs rewrite: fail-closed on size ceilings (L913), fail-open on stale baselines (L924) — correct posture.
  • Compaction + domains/ removal are wire-neutral and guarded by new structural invariants in checkSdkContents (L792, L802).
  • Storage content-check coverage preserved via the workspace check.
  • CI records on push and compares PRs by commit-matched cache.
  • Changeset present (patch) and appropriate for build-tooling scope; no wire-shape or export impact.
  • Well-tested (new tests for size check, dmts declarations, client-core public export).

Decision table: no critical/high/medium findings; high_risk false; gated_paths false; no no-auto-approve team match. Rows 1–8 do not fire → row 9 approve. (review_decision is REVIEW_REQUIRED, but gated_paths is false, so row 2 does not apply.)

@bokelley
bokelley merged commit 6141030 into main Nov 1, 2026
45 checks passed
@bokelley
bokelley deleted the bokelley/package-size-headroom branch November 1, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant