Repository navigation
Description
Measured while implementing objectstack-ai/objectui#5287 (console top bar); filed unassigned, out of scope for that PR.
What was measured
The console needed exactly one three-value predicate — postureEnforcesWall from @objectstack/spec/security, a pure function over a string union. Bundling it costs a quarter of a megabyte, and it costs that even when the consumer's graph already holds the entire spec root entry.
Measured on @objectstack/spec@17.0.0 as resolved in objectui, with that repo's own esbuild (--bundle --format=esm --minify --tree-shaking=true), an entry importing nothing else:
| entry | minified |
|---|---|
import * as spec from '@objectstack/spec' |
1271.8 KB |
the same plus import { postureEnforcesWall } from '@objectstack/spec/security' |
1509.1 KB (+237.3 KB) |
@objectstack/spec/ui alone |
630.1 KB |
/ui plus /security |
867.4 KB (+237.3 KB) |
The delta is identical in both directions, so the subpath entries do not share their bundled schema modules with each other or with the root entry — each import materializes its own copy of what it reaches.
Why nothing shakes it out
@objectstack/spec's package.json declares no sideEffects field. Both esbuild and rollup then have to assume every module in the package may have side effects on evaluation, and the schema entries do evaluate at module scope. So the retained bytes are not "what you imported" but "every module the entry file reaches", regardless of which export the consumer named.
Why this matters beyond one call site
Downstream consumers pay it on page load. objectui already runs a dedicated build-time guard for exactly this class — apps/console/vite.config.ts's assertLazyLinterStaysLazy, added over 89 KiB gzipped of @objectstack/lint leaking into the eager closure (objectui#5266). A protocol package whose predicates cost 237 KB each pushes consumers toward re-deriving protocol rules locally instead of importing them, which is precisely the drift ADR-0105 put the tenancy posture into the protocol to prevent. objectui#5287 took that trade deliberately and pinned the local copy with a parity test, but the trade should not have to exist.
Directions worth triaging (not a decided fix)
- Declare
sideEffects: false(or a narrow allowlist) if the schema modules are in fact evaluation-pure — the standard fix, and the one that needs a maintainer to confirm the claim rather than an agent to assume it. - Annotate top-level schema construction as
/*#__PURE__*/so bundlers may drop unreached schemas. - Split the pure predicate/vocabulary helpers (
postureEnforcesWall,postureStampsOrganization,postureUsesUnionScope,normalizeTenancyPosture,TENANCY_POSTURES) into a schema-free subpath consumers can import for a few hundred bytes.
Reproduction is two esbuild invocations against an installed @objectstack/spec; no objectui checkout is required.
Activity
os-zhuang commentedon Aug 19, 2026
Triage (triage seat, session
session_014qTKTqjme5Fp5BH9iRmy6t): routed to the decision inbox —needs-user-decision+domain:spec, type Task, assigned to the inbox queue per the #9976 protocol.Rationale: lands in the
packages/specbuild/publish pipeline (tsup config +package.jsonsideEffects/exports). Not a duplicate of #8133 — that card is cross-entry IDENTITY and duplication; this one is intra-entry tree-shaking — but both are the same publish-pipeline design decision, and #8133's hold condition ("a consumer is measured breaking on … bundle size") is at least grazed by this measurement. Recommend deciding the two together; whichever seat implements must treat them as one worksite (serial, one dispatch entry).<!-- os-decision-facets -->
sideEffects: falseon modules that DO register at module scope would make bundlers silently drop live code — the purity claim must be measured, not assumed.Options:
sideEffects: false(or a narrow allowlist) +/*#__PURE__*/on top-level schema construction. No public-face change.@objectstack/specships 18 self-contained entry bundles with no shared chunks, so a consumer mixing entry points gets differentConnectorSchema/defineConnectorobjects (measured) #8133's chunk-splitting question into the same PR (one publish-pipeline rework, restores cross-entry identity too).Recommendation: A, escalating to B only if the purity measurement already forces touching the tsup config. Confidence gap: this analysis has not verified whether any spec module has real module-scope side effects (registry writes, global mutation) — that measurement decides whether A is safe at all, and it should be the implementing dev's first step with a hard stop if impure modules surface.
Generated by Claude Code
os-zhuang commentedon Aug 19, 2026
Self-correction (same triage session): assignee cleared — the #9976 assign-to-inbox routing was retired by skill PR #9994 (merged 13:00Z today; maintainer ruling: decision cards are not pushed, the inbox is digested periodically in discussion). The
needs-user-decisionlabel and the facet block above are the operative record; nothing else changes.Generated by Claude Code
os-zhuang commentedon Aug 20, 2026
Maintainer ruling recorded (2026-08-20, live decision-inbox session with the triage seat, session
session_01PjAP6vbcsg2yMtvySPv1Qo)Ruled: approved as one family with #10096 — full ruling text, the minted standing principle (「浏览器可达的 spec 导出面必须 schema-free」, maintainer verbatim 「同意」), and the four implementation clauses are recorded on #10096 to keep one authoritative copy. This card's slice:
sideEffectsdeclaration (purity proven by measurement, the maintainer-confirmation the body asked for is hereby given at the policy level — the per-module purity claim remains the dev's to prove),/*#__PURE__*/where warranted, and the schema-free predicate/vocabulary subpath direction forpostureEnforcesWalland siblings.State transition in the same stroke:
needs-user-decision→pm:queue. Fold-or-serial with #10096 is thedomain:specseat's call at dispatch (five-gate test reads foldable).Generated by Claude Code
feat(spec): schema-free /meta-spelling entry + sideEffects: false wit…