build: one ts.Program per DTS pass — patch tsup's bundled rollup-plugin-dts grouping (spec 4997 → 882 MB live heap) - #20499
Merged
Conversation
…s every DTS entry tsup 8.5.1 bundles rollup-plugin-dts 6.1.1. Its createPrograms keys each entry by a directory, and on the tsconfig-override path tsup always takes, a config-cache hit kept the entry's own directory. So every DTS entry got its own ts.Program. That affects 12 packages here; spec's 18 entries built 18 programs. The patch keys every entry by the tsconfig's directory. Written with pnpm patch-commit; patchedDependencies lives in pnpm-workspace.yaml; the lockfile was regenerated by pnpm install. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
The spec DTS-pass docblock now describes the patched grouping, the tsup-bump duty, and today's table. Measured at the 6144 ceiling in an 8192 MB cgroup: 18 -> 1 programs, live heap 4997 -> 882 MB, 153s -> 41s. It also records the wider normalisation under which the patched and unpatched trees are equal. The 6144 ceiling is unchanged. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx
Contributor
📓 Docs Drift Check
What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): |
objectstack-fleet
Bot
deleted the
claude/issue-20419-dts-one-program
branch
September 28, 2026 20:52
This was referenced Sep 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20419
Clause-②: no
What changes
This is round 2 of the card: the root-cause fix. The seat ruled route A, a pnpm patch of tsup's bundled rollup-plugin-dts, and the maintainer's veto window runs until this PR lands. Round 1 (#20483) landed the
noCheckmitigation.patches/tsup@8.5.1.patchchanges 5 lines indist/rollup.js, which is tsup's bundled rollup-plugin-dts 6.1.1. When tsup passes a tsconfig path, every entry is now grouped by the tsconfig's directory. Before, an entry that hit the config cache kept its own directory, so each entry directory got its ownts.Program. The patch was written withpnpm patch/pnpm patch-commit.pnpm-workspace.yamlgetspatchedDependencies: tsup@8.5.1, with a comment on why the patch exists and what a tsup bump owes.patch-commitwrote the key intopackage.json'spnpmfield; it was moved here, andpackage.jsonis unchanged.pnpm-lock.yamlwas regenerated bypnpm install. The diff is thepatchedDependenciesblock plus thepatch_hashon each tsup resolution (+16 / −11). No other resolution moved. The lockfile was byte-identical before and after the key moved frompackage.jsonto the yaml.packages/spec/tsup.config.tschanges only its docblock: the one-program mechanism, the tsup-bump duty, today's table, and the wider equality needed to compare trees. The 6144 ceiling is unchanged.No package source changes.
Census: which packages build more than one program
Static census. All 67 workspace packages whose scripts run tsup were checked. Each config was loaded the way tsup loads it (
bundle-require), with the build script's--configand positional entries applied, and the plugin's grouping simulated. 12 packages had more than one program; the other 55 have one DTS entry.Empirical census. The declaration pass of every package in the census closure (31 packages) was built with a
ts.createProgramprobe preloaded into the DTS worker. It confirms the 12. Programs before → after the patch:@objectstack/spec@objectstack/platform-objects@objectstack/metadata@objectstack/metadata-core(two config items)@objectstack/service-cluster(two config items)@objectstack/core,lint,objectql,plugin-auth,plugin-webhooks,service-datasource,typesRound 1's estimate was off in both directions. It listed
create-objectstack, whose DTS pass has one entry (dts.entry), and it missedplatform-objects, whose object-formentrygave 11 programs.Measurements: spec at the 6144 ceiling in an 8192 MB cgroup
Method.
--trace-gc.8113763026(withnoCheck) against the same tree plus the patch (78cbf5dfbf). Spec's source is identical in both.buildscript, unpatchedbuildscript, patchedThe ceiling now has about 4.5 GB of headroom. It stays at 6144; lowering it is a separate change.
The patch cannot rot silently (measured)
All three cases ran with the repo's pnpm 10.31.0, from
packageManager.pnpm installexits 1 withERR_PNPM_UNUSED_PATCH The following patches were not used: tsup@8.5.1, and--frozen-lockfileexits 1 withERR_PNPM_LOCKFILE_CONFIG_MISMATCH.dist/rollup.jscarries the patch marker once.patchedDependenciesblock removed,pnpm install --frozen-lockfileexits 1 withERR_PNPM_LOCKFILE_CONFIG_MISMATCH, which is what CI's frozen install would hit. The removal was done withscripts/ablation-replace.mjs, and the restore was proven blob-equal to HEAD.The emitted declarations are the same types
Per the seat's Q2 answer: canonical equality with negative controls replaces byte identity.
Trees compared. All 31 packages of the census closure were built twice, in dependency order: unpatched at
8113763026, then patched at78cbf5dfbf. Only tsup differs between the two builds. Everydist/**/*.d.{ts,mts,cts}was saved each time.Canonical form. Each file is re-printed through the TypeScript printer with:
import/exportspecifiers sorted;Shared chunks are paired by chunk-name-agnostic content, and the pairing must be unique on both sides.
Results.
@objectstack/spec: 130 files, 0 differ canonically. One chunk is paired by content:data-engine(unpatched) isanalytics.zod(patched). The files differ only in the ways round 1 recorded.@objectstack/metadata-protocolhas one entry, so the patch does not change its program count. It differs in bytes only, through union order inlined from spec, and is canonically equal even with statement sorting off.Negative controls. In a copy of each patched tree, the first real
stringtype node becamenumber, located by the TypeScript parser and never inside a comment. The control ran on:platform-objects,identity/index.d.ts, because itsindex.d.tsonly re-exports);lint,metadata-core,objectql, and spec's renamedanalytics.zodchunk.Every control was caught, and each one named exactly the mutated file. A mutated chunk also fails the content pairing ("no partner"). One control (
platform-objects) was first pointed at a directory, errored, and was re-run on a real file. That first attempt is not counted.Gates, at the merged head (
57e5191f21= this branch +origin/mainfc0db22bcf)node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commandsderived 60 commands over 4 changed paths.Derived set. All 60 derived gates exited 0.
--ranreconciliation: "60 derived famil(ies) accounted for — 60 run, 0 NOT-MEASURED". That includes:check:override-consistency,check:vendor-export-contract-resolve,check:workspace-manifest-cyclesandcheck:osv-exemptions;check:dts-closure: 72 built packages, 166/166 declared declaration files present;check:dual-build-cjs-loads;check:lean-entry-closure.Builds.
turbo run build --forceover the census closure: 29/29 tasks, 0 cached.turbo run build --filter=!@objectstack/docs, 72/72 tasks.pnpm --filter @objectstack/spec buildinside the 8192 MB cgroup: exit 0, peak RSS 3680 MB, 77.6 s, DTS 38.7 s.check-dts-emitted: 36/36.check-dts-references: 130 files, 394/394.pnpm --filter @objectstack/spec check:generated: exit 0, all 15 artifacts up to date.pnpm install --frozen-lockfile: exit 0.Tests. The 8 spec test files that read
tsup.config.tsas text: 130 passed.Scope. The patch reaches only the DTS worker: tsup's
index.jsloadsdist/rollup.jsin one place,new Worker(… "./rollup.js"). The JS passes are untouched.Changeset. None;
skip-changeset. Per Q2: canonical equality holds for every census package, no JS output or shipped source moves, and 29 of 31 packages are byte-identical.Acceptance notes
entry. A tsup package whosedts.entrynarrows to one file (create-objectstack) has one program, whatever itsentrysays. Object-formentrycounts too (platform-objects).Draft upstream report (for the maintainer to file at rollup-plugin-dts)
Generated by Claude Code