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:
consta=awaitimport('./packages/spec/dist/index.mjs');constb=awaitimport('./packages/spec/dist/integration/index.mjs');a.defineConnector===b.defineConnector// falsea.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:
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
exports map, each a self-contained bundle. ConnectorSchema is defined in 5 of them (index, integration, kernel, system, cloud).- Largest bundles:
system/index.mjs 1.9M, index.mjs 1.6M, kernel/index.mjs 1.4M, cloud/index.mjs 1.3M, api/index.mjs 1.2M. packages/spec/dist totals 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
Map or WeakMap keyed by a Zod schema, a registry that de-dupes by object reference, an instanceof-style check — gets two entries where the author expected one. This fails quietly; there is no error, just a lookup miss. zod itself is external, so the usual worst case (two Zod runtimes disagreeing about what a ZodType is) 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
exports map 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:
Generated by Claude Code
Filing unassigned; found while implementing #7991 (PR #8129, the
packages/qa/downstream-contractsource alias). Deliberately not fixed there — that card's ruling is that a problem thedistboundary was hiding is a finding in its own right, and this one is about howpackages/specis built and published, which is a different blast radius from a QA harness alias.The fact, measured against the published
distdirectlyNot through vitest, not through any alias — a bare
import()of the built artifacts:Both specifiers are public:
@objectstack/specand@objectstack/spec/integrationare entry points in the package's ownexportsmap, 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.mjsopens with exactly one import, and it is external: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:exportsmap, each a self-contained bundle.ConnectorSchemais defined in 5 of them (index,integration,kernel,system,cloud).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:
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.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
distthat 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
defineConnectoris one object across the root and theintegrationnamespace. 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)
exportsmap and with CJS consumers.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
specis 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.dist/*.d.ts.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