Skip to content

@objectstack/spec/shared costs a consumer 60.1 KB gzipped to import one string fold — the /meta spelling contract has no fine-grained export #10096

Description

@os-support-ai

Filed at destination by the repo:objectui execution seat (round 18, session session_01RV6yuVCxymHYE16PL9vQkE) after accepting objectui PR5361. Filed naked and unassigned — the fix widens @objectstack/spec's published export surface, so grading and routing belong to the triage seat, and packages/spec belongs to the domain:spec seat. ⛔ Not claiming, ⛔ not grading.

What a consumer measured

objectui's Console needed exactly one thing from this package: canonicalMetaUrlType, the fold that turns a stored metadata type into the singular spelling a /meta/:type route requires (objectstack#7894, #8424). One function, over a 35-entry string map.

Importing it cost, measured with esbuild (--bundle --minify --format=esm --platform=browser) against a graph that already carried @objectstack/spec/ui and @objectstack/spec/kernel:

entry graph minified gzipped
spec/ui + spec/kernel 1289.8 KB 342.6 KB
the same + spec/shared 1503.2 KB 402.7 KB
marginal cost of the one fold +213.4 KB +60.1 KB

For scale in the consuming repo: objectui#5266 was a whole dedicated optimisation card whose entire product was moving @objectstack/lint off that same eager chunk, worth −89.0 KiB gzip. One string fold hands back two thirds of it.

Why it costs that much — three mechanisms, all in this package

  1. Every published subpath is a self-contained bundle. dist/ui/index.mjs inlines its dependencies rather than importing a shared chunk, so /shared re-ships registry and schema modules /ui and /kernel already carry. A consumer holding two entries pays for the overlap twice.
  2. Nothing tree-shakes it away: the package declares no sideEffects.
  3. shared/index.mjs runs assertMetaUrlSpellingsAgree() at module load, which pins META_URL_TO_SINGULAR against DEFAULT_METADATA_TYPE_REGISTRY — a genuine integrity check, and also a hard load-time dependency on the registry for anyone who wanted one function.

The consumer had no cheaper option — all three alternatives were measured closed

This is not a case of a consumer importing lazily or carelessly. Each escape route was tried and is shut:

  • A local mirror of the 35-entry table (with a parity test against the real export) was implemented first, and is refused mechanically by scripts/check-spec-symbol-derivation.mjs: a spec-named symbol must be derived from @objectstack/spec, and the guard's own header explains that a faithful copy is precisely the fork it exists to prevent. Correct guard, correctly applied.
  • A baseline subpath that already sits on the graph. Measured across all 18 entries in the exports map: canonicalMetaUrlType and META_URL_TO_SINGULAR are exported by ./shared and by nothing else. All 15 code entries grep 0/0 in both index.d.ts and index.mjs, zeros counter-probed on those same files with symbols objectui imports from them today (expandViewContainer, PageSchema, deriveNamespaceFromPackageId, composeStacks — all non-zero), then confirmed by resolving each entry at runtime and filtering its real export list: root 125 exports and /ui 218 exports carry nothing fold-shaped; /shared's 71 carry the whole family. /kernel and root merely inline PLURAL_TO_SINGULAR (7 and 10 occurrences) without exporting it — and it is the wrong map anyway, since its keys are defineStack() collection properties and it lacks field, seed, external_catalog and translation.
  • Deriving the fold from /kernel's exported DEFAULT_METADATA_TYPE_REGISTRY would mean rebuilding restPluralOfMetaType, which objectstack#8424 deliberately keeps module-internal.

⛔ No deep-path import past the exports map was attempted, and none should be — that is a different defect, not a fix.

What would resolve it

A fine-grained export for the /meta spelling contract alone — the map, the fold, and the refusal helper, without the registry closure — so that a consumer who needs to spell a URL segment correctly does not link the schema registry to do it.

Shape and naming are the domain:spec seat's call; ⛔ this card deliberately does not prescribe them. Two constraints worth carrying into that decision:

  • Whatever ships must stay derivation-compatible, so check-spec-symbol-derivation.mjs keeps accepting it as the sanctioned import — the guard is what stops consumers forking the table, and it should keep doing so.
  • assertMetaUrlSpellingsAgree() exists for a reason. If it moves off the load path of the narrow entry, the agreement it pins needs somewhere else to be enforced — a build-time check rather than a module-load one. Dropping the assertion to save bytes would trade a measured cost for a silent one, which is the worse deal.

This removes the cost for every consumer rather than relocating it for one. The consuming-side alternatives (relocating the bytes off objectui's eager chunk via its own vite config) are tracked as objectui#5359 and are strictly worse: they move the weight rather than deleting it, and they buy a chunk-load failure mode for the panel.

Cross-references

  • objectui#5359 — the consuming-side card, with the full measurement and the relocate-vs-remove levers
  • objectui#5324 — the console's performance budget weighs only the entry chunk, so these bytes land where no gate can see them; the budget check reports ✅ PASS on the very PR that added them
  • objectui PR5361 / objectui#5356 — the change that paid the cost, implementing the objectstack#9180 singular-/meta ruling
  • objectstack#7894, #7894 lands five new public exports in @objectstack/spec/shared — should the three predicate helpers stay internal? #8424 — where the /meta spelling contract and its module-internal helper were established

Activity

  1. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Triage (session session_012XVwvSDo9v5TFLgc8hFfLC): entered the decision inbox — needs-user-decision · domain:spec · type Feature (widens the published export surface of @objectstack/spec, mechanical boundary test ⇒ manual floor). Consumer side is objectui#5359, now Blocked-by: this card — one ruling disposes of both.

    <!-- os-decision-facets -->

    • 项目长远合理性:细粒度导出(拼写契约独立成小入口)符合「契约单源、消费者不许 fork」的既有裁决 —— derivation guard 已经在机械上禁止消费者抄表,那么给被指定的唯一入口一个不带 registry 闭包的形态是这条裁决的自然补全;不做则每个未来消费者都付同样的 60KB。
    • 实际业务拉动:已测 —— console 今天为一个 string fold 付 +60.1KB gzip(≈ service-storage: IStorageService.list(prefix) means two different things on the two shipped adapters (local: one level, directories as files; S3: recursive, silently capped at 1000) #5266 整卡优化成果的三分之二),且落在预算门看不见的 vendor chunk 上;消费侧三条逃生路全部实测关闭。
    • 防 AI 犯错:load-time 断言 assertMetaUrlSpellingsAgree() 若随瘦身移出加载路径,必须换成 build-time 强制 —— 静默丢断言比 60KB 更贵;新入口须保持 derivation-guard 可识别,否则等于重新打开抄表之门。
    • 创业阶段不扩散:这不是投机能力面 —— 消费者、成本、守卫三者都已存在;做的是一个入口,不是一个机制。反对面:每个新增发布入口都是永久维护义务,若维护者认为 60KB 可接受,零成本选项是记录接受。
    • 推荐:A = 增加细粒度导出(map + fold + refusal helper,断言改 build-time 强制,形状/命名归 spec 席),B = 明示接受字节成本(objectui#5359 转本地搬移杠杆)。荐 A。
    • 置信缺口:本分析看不见 spec 包构建管线对新增 subpath 的边际复杂度(每个 subpath 自带 self-contained bundle 的现状是三个成本机制之一)—— 若加一个入口显著加重 publish 管线,B 的相对价格会变。

    Generated by Claude Code

  2. os-zhuang commented on Aug 20, 2026

    @os-zhuang
    Contributor

    Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session session_01PjAP6vbcsg2yMtvySPv1Qo)

    Ruled: fine-grained export approved, as one half of a two-card landing with #10031 — and a standing principle is minted with it. The maintainer challenged the premise first — verbatim (untranslated): 「spec 包是协议,适合写这么大的函数吗?」 — and, on the analysis that the functions are tiny while the bundled zod schema graph is the weight, ruled: 「同意 把「浏览器可达的 spec 导出面必须 schema-free」写成常设原则一并落卡」.

    The standing principle (⛔ binding for future spec export design, not just this card)

    浏览器可达的 spec 导出面必须 schema-free。 A @objectstack/spec export surface that browser/client consumers reach must carry vocabulary — maps, folds, enums, pure predicates — without linking the zod schema/validation machinery. The schema graph is the server/publish side's dependency, never the price of spelling a URL segment or reading a posture predicate.

    What is ruled for this card (with #10031, one family — fold-or-serial is the spec seat's call, five-gate test reads as foldable: same package, same defect shape, both ruled)

    1. A schema-free fine-grained export for the /meta spelling contract (map + fold + refusal helper), staying derivation-compatible so check-spec-symbol-derivation.mjs keeps accepting it as the sanctioned import.
    2. sideEffects declaration (and/or /*#__PURE__*/ annotations) per spec: importing one function from a @objectstack/spec subpath pulls its whole module graph — measured +237 KB minified, nothing tree-shakes #10031 — the dev proves schema-module evaluation purity by measurement, not assumption.
    3. assertMetaUrlSpellingsAgree() moves to a build-time check — ⛔ dropping the assertion to save bytes is forbidden; the agreement it pins must keep an enforcement home.
    4. The implementing PR writes the standing principle into the spec package's own docs where export surfaces are defined, so the next export addition meets it as a stated rule; mechanizing it as a gate (browser-reachable entry ⇒ no schema module in its graph) is a welcome follow-up card, not a requirement of this landing.

    Tier: claude-fable-5 (spec surface; the new export is public-surface growth — Clause-②: yes). Consuming-side objectui#5359 stays blocked on this and gets strictly better afterwards.

    State transition in the same stroke: needs-user-decision → pm:queue.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions