Repository navigation
@objectstack/spec ships 18 self-contained entry bundles with no shared chunks, so a consumer mixing entry points gets different ConnectorSchema / defineConnector objects (measured) #8133
Description
Restart-when: any PR touches the packages/spec build/publish pipeline (tsup config, or the exports map in packages/spec/package.json), or a consumer is measured breaking on cross-entry identity or bundle size
Filing unassigned; found while implementing #7991 (PR #8129, the packages/qa/downstream-contract source alias). Deliberately not fixed there — that card's ruling is that a problem the dist boundary was hiding is a finding in its own right, and this one is about how packages/spec is built and published, which is a different blast radius from a QA harness alias.
The fact, measured against the published dist directly
Not through vitest, not through any alias — a bare import() of the built artifacts:
const a = await import('./packages/spec/dist/index.mjs');
const b = await import('./packages/spec/dist/integration/index.mjs');
a.defineConnector === b.defineConnector // false
a.ConnectorSchema === b.ConnectorSchema // falseBoth specifiers are public: @objectstack/spec and @objectstack/spec/integration are entry points in the package's own exports map, and both export these symbols. A third party that imports one from the root and one from the namespace holds two different objects.
Why: the subpath bundles share nothing
Each entry bundle inlines its whole dependency graph. dist/integration/index.mjs opens with exactly one import, and it is external:
import { z } from 'zod';There is no shared internal chunk to import — so ConnectorSchema's definition is emitted independently into every entry bundle that reaches it. Measured on a fresh build:
- 18 entry points in the
exportsmap, each a self-contained bundle. ConnectorSchemais defined in 5 of them (index,integration,kernel,system,cloud).- Largest bundles:
system/index.mjs1.9M,index.mjs1.6M,kernel/index.mjs1.4M,cloud/index.mjs1.3M,api/index.mjs1.2M. packages/spec/disttotals 101M.
Impact — stated honestly, because I did not demonstrate a live break
What is measured is the duplication and the identity split. What I did not do is find a consumer that breaks on it today, so this should be graded rather than assumed:
- Identity comparison across entry points is silently false. Anything keyed on schema identity — a
MaporWeakMapkeyed by a Zod schema, a registry that de-dupes by object reference, aninstanceof-style check — gets two entries where the author expected one. This fails quietly; there is no error, just a lookup miss. zoditself is external, so the usual worst case (two Zod runtimes disagreeing about what aZodTypeis) does not apply here. That meaningfully lowers the severity and is why I am not filing this as a P0.- Payload duplication is real regardless of identity: a consumer importing three namespaces pulls three overlapping copies.
The reason it is worth a card even undemonstrated: this is the shape of bug that is invisible until it isn't, and #7991 is a fresh example of the same family — a property of dist that no test could see, being consumed as if it were a property of the source.
Note on how this surfaced
PR #8129 adds a pin case asserting defineConnector is one object across the root and the integration namespace. Aliased to source it passes (one tree, one copy); unaliased it fails. That pin's purpose there is to guard the alias and to stand watch for a dual instance, and it does both — but the underlying duplication it revealed belongs to the build, not to that harness, which is why it is filed here instead.
Fix directions (not chosen here)
- A — enable code splitting for the multi-entry build so shared modules land in a common chunk that every entry imports. Restores identity and collapses the duplication; needs a check that the emitted chunk layout is compatible with the
exportsmap and with CJS consumers. - B — accept the duplication and remove the ambiguity by making namespace subpaths re-export from the root bundle rather than rebuild it.
- C — decide the identity guarantee is not offered and say so in the package docs, so nobody writes identity-keyed code against it.
A is the contract-first shape: the symbols are documented as the same exports, so they should be the same objects. Which of these is right is a maintainer call about how spec is published, which is why this is filed rather than patched.
Prior art (searched before filing)
Searched open issues for dual-instance / subpath-exports / duplicate-chunk / tsup-code-splitting phrasings against this repo; no open issue covers this. Related but distinct:
packages/qa/downstream-contract— the repo's backward-compatibility gate for@objectstack/specreports 14/14 green against a spec change that breaks its own fixture, because it readsspec/dist#7991 / PR test(qa): make the spec backward-compatibility gate read spec source, not spec dist #8129 — the card this was found under. Samedist-versus-source family, different surface: that one is which tree a test resolves, this one is what the published artifact contains.- downstream-contract's OTHER half — its typecheck — still resolves @objectstack/spec types through dist, so a narrowed export type reads green (measured) #8021 (open) — the same package's typecheck half still resolving spec types through
dist/*.d.ts. - A package with unit tests and no
vitest.config.tslets a stale@objectstack/coredist decide its verdicts — #7668 fixed one, nothing stops the next #7849 / PR ci(test-alias): gate that a unit test judges source, not a sibling's dist #7966,attach-requires-parent-editc3: #4757 multi-delete pin can't execute against the prebuilt@objectstack/core(stale-dist / CI) #7668 / PR test(service-storage): resolve@objectstack/corefrom source so a stale dist can't decide a pin (#7668) #7778 — the test-source-alias gate and its constraint. Both readvitest.config.*; neither looks at bundle layout.
Generated by Claude Code
Generated by Claude Code
Activity
hotlong commentedon Aug 12, 2026
Triage: routed
domain:spec-tooling(build/publish machinery ofpackages/spec— the machine face, not the semantic face), held asfinding, type Task.Grading rationale: measured and real (identity split + 101M dist + 5 independent copies of
ConnectorSchema), but no live break demonstrated — the card itself says so, andzodbeing external removes the worst dual-runtime case. Under the startup-scope lens this does not earn a dispatch slot today; under the AI-error-resistance lens the identity trap is real but currently guarded by the #8129 pin standing watch. Choosing between code-splitting the published artifact (A), root re-export (B), or documenting no-identity-guarantee (C) is a publish-shape decision with CJS-compat blast radius — if this promotes, it promotes toneeds-user-decision, not straight to queue.Re-grade triggers, either of which reopens grading: (1) any consumer measured breaking on cross-entry identity or bundle size in practice; (2) the next time spec's build/publish pipeline is touched for another reason — fold the decision in then, when the surface is already hot.
Generated by Claude Code
hotlong commentedon Aug 12, 2026
The same duplication has a type-axis cost, measured under #8021 (PR #8176) — recording it here rather than filing a twin, since it is this issue's root cause seen from the other side.
#8021 put a
pathsblock inpackages/qa/downstream-contract/tsconfig.jsonso the backward-compatibility gate'stypecheckreadspackages/spec/srcinstead ofpackages/spec/dist/*.d.ts. The obvious way to pin the bare-entry rule looked like the type-level twin of this issue's runtime assertion (expect(fromPackageRoot).toBe(fromNamespace)): anEqualidentity check betweentypeof defineConnectorreached through@objectstack/specand through@objectstack/spec/integration.It had to be rejected on measurement. In the mixed state that check exists to detect — root resolving to
dist, the namespace resolving tosrc, i.e. exactly the two-copies condition this issue describes — tsc had not finished comparing the two zod-derived types after nine minutes and was killed. The same configuration with that one assertion removed completes in seconds, so the cost is the cross-tree structural comparison itself, not the mixed program.So the runtime symptom of the 18-self-contained-entry-bundles layout is "two different function objects"; the type-level symptom is a structural comparison that does not terminate in useful time. Practical consequence for anyone else pinning this invariant: the identity assertion is available on the runtime axis and not available on the type axis. PR #8176 pins the bare-entry rule by simulating tsc's
pathsresolution instead, the waycheck:test-source-aliaspins the Vite side.Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 13, 2026
State-machine migration, maintainer-authorized (2026-08-13, live triage session: 「我授权你先迁标签」; semantics on #8449): this card is a graded hold — verdict and re-grade triggers are in the grading comments above — so
finding→pm:on-hold. Verdict, triggers, domain unchanged.Generated by Claude Code
os-zhuang commentedon Aug 19, 2026
Evidence update, not a release (triage seat, session
session_014qTKTqjme5Fp5BH9iRmy6t): #10031 measured a consumer-side bundle-size cost of this package's publish shape (+237.3 KB per subpath import, no tree-shaking, nosideEffectsfield) — adjacent to this card's Restart-when second arm but a deliberate consumer trade (objectui#5287 pinned a local copy), not a break, so the hold stands. #10031 is now in the decision inbox with a recommendation to decide both publish-pipeline questions together; if that ruling lands, it supersedes this hold — the implementing seat should treat both cards as one worksite.Generated by Claude Code
objectstack-fleet commentedon Sep 23, 2026
关
not_planned—— 维护者逐张复核 on-hold 卡时同意关闭;体积那一半已解决,身份那一半没有实际受害者分诊席(
session_01Tw7jnJinGHvoGSi8aFkhPJ),2026-09-23T08:17Z。维护者 2026-09-23 在分诊会话里逐张复核pm:on-hold卡,对第三组的回复原文:「第三组 同意」。 本卡在那一组里的建议是关闭。呈给维护者的事实(本地
main上核对过)@objectstack/specsubpath pulls its whole module graph — measured +237 KB minified, nothing tree-shakes #10031 处理:packages/spec/package.json现在带"sideEffects": false,packages/spec/tsup.config.ts里的pureSchemaConstruction插件给延迟构造的 schema 标了 pure,使用方的打包器可以裁掉没用到的部分。spec: importing one function from a@objectstack/specsubpath pulls its whole module graph — measured +237 KB minified, nothing tree-shakes #10031 已关闭。splitting: false没变,18 个入口依然各自打包,从@objectstack/spec和从@objectstack/spec/integration拿到的ConnectorSchema仍是两个不同的对象。但只有按对象身份做判断的代码才会受影响(zod是外部依赖,不会出现两套运行时),挂起 6 周,没有任何使用方出问题。@objectstack/specsubpath pulls its whole module graph — measured +237 KB minified, nothing tree-shakes #10031、@objectstack/spec/sharedcosts a consumer 60.1 KB gzipped to import one string fold — the /meta spelling contract has no fine-grained export #10096、[seam→spec]@objectstack/spec@17.1.0statically importspg-connection-stringfrom six of its sixteen entry points, with nobrowserexport condition — objectui's site build is red onmain#11072),每次动手的人都选择不带上本卡 ——@objectstack/spec/sharedcosts a consumer 60.1 KB gzipped to import one string fold — the /meta spelling contract has no fine-grained export #10096 甚至直接在tsup.config.ts第 91 行写了「@objectstack/specships 18 self-contained entry bundles with no shared chunks, so a consumer mixing entry points gets differentConnectorSchema/defineConnectorobjects (measured) #8133 stays on hold」。重开条件
有使用方实测到因为跨入口身份不一致而出错(例如以 schema 对象作键的
Map/WeakMap查不到),或者因为包体积出了真实问题。届时按正文 A/B/C 三个方向选一个。关闭理由:
not_planned,同时摘掉pm:on-hold。Generated by Claude Code
fix(gate): correct why check-published-files lets packages/spec ship …