feat!: collapse the public surface onto Entity - #24
Merged
Conversation
Records the decision to export a single name, the namespace-merge mechanism, and the emit constraint a spike turned up: a namespace member that shares a name with the type it aliases is emitted by tsdown's dts bundler as a circular self-alias. That compiles, so it fails silently -- in the spike it voided the construction seal, detected only because the consumer fixture's `@ts-expect-error` went unused. Not yet implemented; sequenced behind #23, which edits the same docs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#23 merged as 8c2485b and this branch is rebased onto it, so the docs pass starts from the corrected text rather than racing it. Records the one thing that merge added to this change's scope: the shared imports preamble it introduced appears in eight files, each of which collapses to `import { Entity }`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`computed`, `InvalidEntity` and every public type now hang off the builder.
A bare `computed` is too generic to take from a consumer's import scope -- it
collides outright with Vue, MobX, Angular signals and Solid -- and this repo
had already made that call once, for `union`, and written down the reasoning.
Applying the same test consistently leaves one name to write against.
`EntityUnion` becomes `Entity.Union`; the stutter only existed to disambiguate
a top-level name.
Two things the design did not survive contact with.
`index.ts` still exports `BaseInstance`, `ConstructionKey` and `Sealed` at the
top level. Hiding them behind the namespace builds and typechecks, then fails
the consumer pass with TS4020: a downstream library compiling with
`declaration: true` emits the underlying type name, not the namespace path
aliasing it, so unexported they are private names. That is the same regression
`types.ts` already documents for `ConstructionKey`.
`Entity.InvalidEntity` is a namespace re-export rather than a type alias.
Declaring it as a type stops expando inference supplying the value, and the
runtime assignment fails with TS2339; `export { InvalidEntity }` carries both
meanings.
The `*Src` aliases in entity.ts are load bearing and commented as such. A
namespace member sharing a name with the type it aliases is emitted by tsdown's
dts bundler as a circular self-alias, which compiles and then silently
degenerates the type -- measured to void the construction seal, detected only
because a consumer `@ts-expect-error` went unused.
BREAKING CHANGE: `computed`, `InvalidEntity`, `Input`, `Output`, `CreateInput`,
`Patch`, `ComputedField` and `EntityUnion` are no longer top-level exports. Use
`Entity.computed`, `Entity.InvalidEntity`, `Entity.Input`, `Entity.Output`,
`Entity.CreateInput`, `Entity.Patch`, `Entity.ComputedField` and `Entity.Union`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Collapses @btravstack/entity’s public API surface onto the Entity builder (values and types), to avoid generic-name collisions in consumer import scopes and to apply the repo’s “one concept, one name” rule consistently.
Changes:
- Move public helper values/types behind
Entityvia expando properties +export declare namespace Entity(e.g.,Entity.computed,Entity.InvalidEntity,Entity.Output,Entity.Union). - Update all internal tests, consumer fixture, and docs to use the new spellings.
- Preserve
BaseInstance,ConstructionKey, andSealedas top-level type exports (while also exposing them underEntity.*) for downstreamdeclaration: truecompatibility.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| README.md | Update example import and computed usage to Entity.computed. |
| packages/entity/src/union.ts | Export UnionMember type so Entity.Union can express constraints. |
| packages/entity/src/nesting.spec.ts | Update test imports and computed call site to Entity.computed. |
| packages/entity/src/index.ts | Export only Entity as the main entry point; keep top-level type escape hatches. |
| packages/entity/src/extend.spec.ts | Update test imports and computed call site to Entity.computed. |
| packages/entity/src/entity.ts | Attach computed/union/InvalidEntity onto Entity; introduce merged Entity namespace for public types. |
| packages/entity/src/entity.test-d.ts | Update type-level assertions to Entity.* helper types and Entity.computed. |
| packages/entity/src/contract.spec.ts | Update computed usage to Entity.computed. |
| packages/entity/src/computed.spec.ts | Update computed usage to Entity.computed. |
| packages/entity/README.md | Update package README examples to Entity.computed and single import. |
| packages/entity/consumer/index.ts | Update consumer fixture to validate all Entity.* members against built d.ts output. |
| docs/superpowers/specs/2026-08-07-entity-namespace-design.md | Add design/spec doc capturing the mechanism and constraints. |
| docs/reference.md | Update reference docs to Entity.computed, Entity.InvalidEntity, and Entity.* helper types. |
| docs/how-to/test-domain-logic.md | Update shared imports preamble to import { Entity }. |
| docs/how-to/persist-and-rehydrate.md | Update helper type example to Entity.Output. |
| docs/how-to/model-an-aggregate.md | Update imports and computed example to Entity.computed. |
| docs/how-to/http-contract.md | Update shared imports preamble to import { Entity }. |
| docs/explanation.md | Update shared imports preamble to import { Entity }. |
| CLAUDE.md | Document the new “write against Entity” rule and the *Src alias constraint. |
| .changeset/entity-namespace.md | Add changeset documenting the breaking change and new spellings. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Everything you write against now hangs off the builder. A bare
computedis too generic to take from a consumer's import scope — it collides outright with Vue, MobX, Angular signals and Solid — and this repo had already made that call once, forunion, and written the reasoning intoentity.ts:The convention was never "standalone by default". It is a name-pollution test, and applying it consistently leaves one name.
computedEntity.computedInvalidEntityEntity.InvalidEntityInputOutputCreateInputPatchEntity.Input…ComputedFieldEntity.ComputedFieldEntityUnionEntity.Union— the stutter only existed to disambiguate a top-level nameNo deprecated top-level aliases are kept, per the repo's "resist convenience aliases" rule. Breaking, at
0.1.0, so a minor bump.Mechanism
Entitystays afunctiondeclaration merged with a type-onlyexport declare namespace Entity. Values attach as expando properties, asEntity.unionalready did. This is forced rather than chosen:export const Entity = Object.assign(fn, {…})cannot merge with a namespace.The
*Srcaliases are load bearingWriting the obvious thing —
— emits
type ConstructionKey = ConstructionKeyintodist/index.d.mts. tsdown's dts bundler collapses the import to a bare name that resolves to the member itself. It compiles. The type just degenerates, and in the spike that silently voided the construction seal; the only signal was the consumer fixture's@ts-expect-erroron a forged key going unused. Each source type is therefore imported under a distinct*Srcalias, with the measurement recorded as a comment.Two things the design didn't survive
Both caught by the gate rather than by review, and both written into the spec's Implementation notes.
index.tsexports four names, not one.BaseInstance,ConstructionKeyandSealedmust stay top-level. Moving them behindEntitybuilt and typechecked fine, then failed the consumer pass:A downstream library compiling with
declaration: trueemits the underlying type name, not the namespace path aliasing it, so unexported they are private names — the same regressiontypes.tsalready documents forConstructionKey. They are exported both ways now. The rule is "one name you write against", not "one export".Entity.InvalidEntityis a re-export, not a type alias. Declaring it as atypemember stops expando inference supplying the value, so the runtime assignment fails withTS2339.export { InvalidEntity }inside the namespace carries both meanings.computedandunionare unaffected — no type member shares their name.Verification
Full gate green:
format --check,lint,typecheck(all three passes),test(120 passed),knip,build.Three checks specific to this change:
TS2578: Unused '@ts-expect-error'.consumer/index.tsnow names every namespace member, because a degenerated member is only visible as a directive going unused.d.mtsscanned programmatically for self-referential aliases — none. Top-level exports are exactly{ type BaseInstance, type ConstructionKey, Entity, type Sealed }.d.mtsvia a scratch module in the consumer project — the helper types,Entity.computed,Entity.InvalidEntityas both value and type,P.tag("InvalidEntity"), and the repository signature frompersist-and-rehydrate.md. Not committed.Also
CLAUDE.md: records the new export rule and the*Srcconstraint; corrects the stale claim thatshape()is "the only sanctioned way to build a domain object" — it is internal and not exported at all.docs/superpowers/specs/2026-08-07-entity-namespace-design.mdcarries the design and the two corrections.🤖 Generated with Claude Code