Repository navigation
perf(spec): a bundle that never reads the ADR-0087 conversion table stops keeping it, 226 KB gzip off the console first screen (#22044) - #22048
Conversation
…a bundle of ./shared stops keeping every conversion Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
… conversion table only when it reads it Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
… that do not read it Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 139 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 2de71f32cd64d6d1f7f97567ddc15f0f989699bb && git checkout 2de71f32cd64d6d1f7f97567ddc15f0f989699bb
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8caa131e52717ef35fe71742d758f10b8f2b77ee cf854f71dc379b62288390cd779476586644db3b && git checkout -B drift-repro 8caa131e52717ef35fe71742d758f10b8f2b77ee && git merge --no-ff cf854f71dc379b62288390cd779476586644db3b
node scripts/docs-audit/affected-docs.mjs --json 8caa131e52717ef35fe71742d758f10b8f2b77ee |
Contract reviewServed-tier: Inputs read: card #22044 (body; triage ① Derived judgmentsAccept set: none changes. Right. The only Public surface, what reaches a published file. Right, with one precision note. Where each annotation sits, and which call it covers. Right.
Are the two calls free of side effects? Yes. The pin,
Check-runs on the head, as read once, not polled: 32 in all. 14 ② Semver level
③ Boundary flagsDev report deviations, each answered:
Reviewer's own flags:
Implemented-by: VERDICT: PASS Generated by Claude Code |
…ames objectui value-imports from ./shared Claude-Session: https://claude.ai/code/session_01GV6oYwgc1kWiUCb1YaprQ7 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #22044
Clause-②: no
This is the source-side payback for the maintainer's ruling on objectui#11717. The record is comment
6027998226, and the selection was 「A 抬预算+回收卡 (Recommended)」. objectui raised its console first-screen budget to let 17.7.0 in (objectui#11742), and this card brings it back down.Measured on objectui's console at objectui
c0862c1c, which resolves 17.7.0: the first screen's eager closure is 226,238 bytes gzip smaller, and all of the change is in thevendor-objectstackchunk. The 17.7.0 bump added +182,909 bytes gzip to that chunk. No entry's export list changes, and no declaration or runtime value changes. Only the bytes a bundler keeps change.What grew between the tags, module by module
Instrument: source-map attribution of each published ESM entry file. The tarball ships an
.mjs.mapfor each entry, and each generated byte is assigned to the source module of the mapping it falls under. The comparison is npm@objectstack/spec@17.6.0against@objectstack/spec@17.7.0. All figures are raw bytes.dist/shared/index.mjs: 980,505 to 1,503,348 bytes (+522,843), with the same 79 exports../sharedsrc/ui/component.zod.tssrc/ui/view.zod.tssrc/data/field.zod.tssrc/ui/dashboard.zod.tssrc/conversions/registry.tssrc/ui/chart.zod.tssrc/ui/report.zod.tssrc/ui/bulk-action.zod.tsdist/browser/index.mjs(the root a browser bundler loads): 2,348,750 to 2,593,955 bytes (+245,205).src/ui/component.zod.tssrc/conversions/registry.tssrc/automation/approval.zod.tssrc/ai/agent.zod.tssrc/system/job.zod.tssrc/ui/chart.zod.tssrc/ui/page.zod.tsHow
./sharedreaches those modules. This is the esbuild metafile of the 17.7.0 source, shortest path.shared/index.tsimportsshared/metadata-collection.zod.ts, which importsconversions/apply.ts(fornormalizeStackInput). That importsconversions/registry.ts, and from there the schemas the conversions read:ui/view.zod.ts, thenchart.zod.tsandbulk-action.zod.ts;conversions/walk.ts:ui/component.zod.ts, thendashboard.zod.tsandreport.zod.ts.data/field.zod.tsis also reached throughshared/field-type-suggestion.ts. The shared source itself grew very little: its own modules are under 10 KB of the delta.Why the console kept it
Every top-level call stays. Each published entry is one flat file (
tsup.config.ts,splitting: false). A consumer's bundler therefore keeps every top-level call it cannot prove pure, together with everything that call references.Two calls build the conversion table at load:
18: inApplicationOrder(MAJOR_18_CONVERSIONS);Object.keys(CONVERSIONS_BY_MAJOR)chain that flattens intoALL_CONVERSIONS.Unmarked, they kept every conversion, and every schema a conversion reads, in every bundle of every entry whose graph reaches the table. They did so whether or not the bundle reads the table.
The console never reads the table, and it carried two copies. In the control's
vendor-objectstackchunk, each of the 129 conversion ids appears as anid:property exactly 2 times: the root's copy and./shared's copy. In the candidate's chunk the count is 0. That is why the recovery is larger than./shared's growth alone. The table was already in both copies at 17.6.0, and 17.7.0's new conversions made each copy larger.Consumer probe. This probe isolates
./sharedfrom the rest of the chunk. It is not the acceptance instrument. Setup:c0862c1cvalue-imports from that entry;./shared./ui./system(browser)./sharedat this head,src/conversions/registry.tskeeps 0 bytes, down from 201,720 at 17.7.0.defineStack/composeStacks, which@object-ui/typesand@object-ui/corere-export, but nothing the console bundles calls either of them. The console reading above shows the root's copy gone as well.What changed
packages/spec/src/conversions/registry.ts: both load-time calls carry a@__PURE__annotation. TheCONVERSIONS_BY_MAJORdocblock says why, says that the next major'sinApplicationOrder(…)takes the same mark, and names the pin. No conversion, no order and no value changes.Why source annotations, not the
pure-schema-constructionbuild plugin. The plugin marks calls by the name of the callee, everywhere undersrc/. TheALL_CONVERSIONSinitializer is amap/sort/flatMapchain onObject.keys, and marking those names would mark every such call in the package. Two explicit marks on the two calls are the narrowest change.tsup.config.tsis untouched, so the chunk layout and the published file set are unchanged (below).Pin: a new case in
packages/spec/scripts/pure-schema-construction.test.ts. It builds./sharedthrough tsup with the package's own main options, then builds two consumers of that output:./sharedkeep none of the 129 conversion ids;normalizeStackInput, the control, keeps all of them, which proves the instrument sees the table whenever a bundle keeps it.This is a test case, not a gate and not a byte ceiling.
packages/spec/scripts/conversions-major18-merge.test.ts: itsWIRINGstring, a textual pin of the major-18 wiring line, now spells the annotation..changeset/22044-shared-entry-drops-conversion-table.md:@objectstack/specpatch. The published bytes change and the contract does not.Console reading: the acceptance instrument
objectui
c0862c1cin a detached worktree, built the way theBundle Analysisjob builds it:Each reading comes from
apps/console/dist/eager-closure.json, which reports gzip bytes.vendor-objectstackgzipvendor-objectstackrawOBJECTSTACK_SPEC_DISTset tomain9a0401fd, builtOBJECTSTACK_SPEC_DISTset to this headeb2152b86b, builtvendor-objectstack. The other 330 eager chunks moved by +63 gzip net (import-hash churn), and the eager chunk count is 331 in all three builds.OBJECTSTACK_SPEC_DIST: it is objectui's own injection, the one the Console Pin Gate uses. Reading (b) shows it moves the control by only +248 gzip, which ismain's drift since the tag plus the injection path.Export parity
The guard is that the export surface does not change. Every comparison below sets
main9a0401fd(built in its own worktree) against this head (built):pnpm --filter @objectstack/spec check:api-surface: exit 0, "public API surface + factory signatures unchanged ✓".Object.keysof every.mjsand.jsfile the exports map names, browser arms included. 48 entry files and 7,864 names, 0 differing. Control leg: npm 17.6.0 against this head reads 16 differing (exit 1).tsup.config.tsDTS docblock prescribes (sort union members, property-only type-literal members, statements and specifiers; strip chunk hashes; pair chunks by content). 130 files, 38 entries and 62 chunk contents on each side: identical. Control leg: npm 17.6.0 against 17.7.0 reads 52 differences (exit 1).dist/holds 228 files on both sides, with chunk hashes stripped, and the lists are identical. The chunk layout does not move, sopublish-smokeis not owed andneeds:pack-smokeis not needed.dist/shared/index.mjsgoes from 1,510,391 to 1,510,423 bytes.Verification record
The final head is
eb2152b86b. Its last commit adds only the changeset, so readings taken at08392e53a4cover the samesrc/andscripts/tree.Reverse verification of the pin. Both annotations were committed first, then each was removed in turn through
scripts/ablation-replace.mjs, at08392e53a4:ALL_CONVERSIONSmark ("mutation landed", anchor count 1 to 0) turned the pin red, with 129 ids kept against the expected[]. Restore: "blob == HEAD (d707d83fdbad) andgit diff HEADis empty".inApplicationOrdermark turned it red with the 62 major-18 ids kept. Restored the same way.src/through tsup, so nodist/sits between the mutation and the assertion.Tests:
@objectstack/speclocal project, full (pnpm --filter @objectstack/spec test)eb2152b86bscripts/pure-schema-construction.test.ts08392e53a4scripts/conversions-major18-merge.test.ts, repo project08392e53a4The rest of the
repoproject is declared to CI.Typecheck:
pnpm --filter @objectstack/spec typecheck(tsc, scripts and test layer) passed ateb2152b86b.Build:
pnpm --filter @objectstack/spec build(the JS and DTS passes,check-dts-emitted,check-dts-references) exited 0 atd017de4582and ateb2152b86b.Gates, at
eb2152b86b:node scripts/pm/dispatch-gates.mjs --commandsderives 86 families.--ranreconciles them: 86 run, 0 NOT-MEASURED, 0 UNRUN. Every recorded exit code is 0.pnpm turbo run build:check:dual-build-cjs-loads,check:lean-entry-closureand@objectstack/lintcheck:doc-formula-expressions.pnpm --filter @objectstack/spec check:generated("All 15 generated artifacts are up to date").check:authorable-surface,check:yaml-examples, thecheck-governed-queue-guardself-test,check:console-injectionandcheck:console-sha. The last two skipped for want ofpackages/console/dist, so they are NOT MEASURED here.Lint, narrowed and proven, at
eb2152b86b:eslint --no-inline-config --format jsonover the 3 changed.tsfiles, which is every changed file in eslint's population (**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}). The changeset is outside it.eslint.config.mjsenables no type-aware linting (noparserOptions.project, no typed rules), so this diff cannot move any untouched file's verdict. The full-treepnpm lintis CI's.Acceptance notes
BASELINEby the recovered amount is objectui's follow-up, once it resolves a release that carries this change. The reading to lower by is the (b) − (c) line above. No objectui commit, branch or PR was made: the objectui worktree was used for measurement only, and was removed../sharedafter this change (consumer probe: 144,609 raw / 47,491 gzip for the console's 5 names). Most of it is argument expressions of calls the build marks pure. AstrictObject({ … })call is dropped when unused, but arguments that have side effects still run and are kept, for example barez.*chains,.optional().describe(…)on an imported schema, andcomponentSlot(…).src/ui/component.zod.ts34,556,src/ui/view.zod.ts18,973,src/data/filter.zod.tsabout 18,000.src/ui/component.zod.tsis kept at about 157 to 159 KB raw in each of./ui, the root and./system, because each flat entry carries its own copy.tsup.config.tsdocblock asks for that review first.Generated by Claude Code