Skip to content

ecs: schema-based ArchetypeRowOf/ArchetypeHandleOf for stripInternal services#123

Open
krisnye wants to merge 1 commit into
mainfrom
krisnye/archetype-row-schema
Open

ecs: schema-based ArchetypeRowOf/ArchetypeHandleOf for stripInternal services#123
krisnye wants to merge 1 commit into
mainfrom
krisnye/archetype-row-schema

Conversation

@krisnye

@krisnye krisnye commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Lets a consumer that keeps a hand-written public service interface (not type Service = Database.Plugin.ToDatabase<typeof plugin>) and marks the plugin database type @internal derive its public archetype-handle types so db.archetypes is assignable with no cast — and emit cleanly under stripInternal: true.

Why Database.Archetype.RowOf (#122) doesn't cover this

Database.Archetype.RowOf<S, K> derives from the plugin database type. When that type is @internal (kept small / hides internals), declaration emit breaks two ways:

  1. Referenced publicly → TS7056. The emitter serializes typeof plugin into the consumer's .d.ts.
  2. @internal → dangling/dropped. TypeScript never resolves a row through a stripped symbol — it preserves the reference, leaving a dangling import type { … } → downstream TS2305.

I verified empirically that no extractor form can force inlining through an @internal symbol (named alias, inline anonymous, and deep-Expand all either dangle or get dropped). The only sound path is to reference a public symbol.

What this adds

ArchetypeSchema / ArchetypeRowOf / ArchetypeHandleOf (in src/ecs/store/archetype-row.ts) resolve an archetype's row from a small public schema ({ components, archetypes }) rather than the plugin database type — the same row as db.archetypes[K], but the emitted type references only public symbols.

import type { ArchetypeHandleOf } from "@adobe/data/ecs";

export const trackComponents = { trackKind: { type: "string" }, muted: { type: "boolean" } } as const;
export const trackSchema = { components: trackComponents, archetypes: { Track: ["trackKind", "muted"] } } as const;

// plugin built from trackSchema may stay @internal
export interface TrackService {
  readonly archetypes: { readonly Track: ArchetypeHandleOf<typeof trackSchema, "Track"> };
}

db.archetypes.Track (from the @internal db) assigns to TrackService["archetypes"]["Track"] with no cast; downstream resolves Track to its concrete columns.

Acceptance gate

scripts/emit-stripinternal (pnpm check:emit) emits a stripInternal fixture and type-checks a downstream consumer, asserting: no TS7056; the @internal plugin + db type are stripped; the service .d.ts is self-contained (no plugin/@internal reference, small); and rows resolve to exact concrete columns via a mutual-assignability gate (no dangling import).

The README documents the pattern plus the declaration-emit footguns surfaced while building the gate (reference the handle inline; no fenced ```ts blocks with import/export/interface in JSDoc above an exported type; don't let the schema be a module's lone export).

Verification

  • tsc -b clean; eslint clean; full suite (2531 tests) green.
  • node scripts/emit-stripinternal/check.mjs → PASS (all 9 assertions).

Related PRs

Follows #122 (Database.Archetype.RowOf for derived service types).

…ernal services

Consumers that keep a hand-written public service interface (not
Database.Plugin.ToDatabase<typeof plugin>) and mark the plugin database type
@internal could not name their public archetype rows: referencing the plugin db
type serializes typeof plugin into the emitted .d.ts (TS7056), and deriving from
the @internal type leaves a dangling reference to the stripped symbol (TS2305) —
declaration emit never resolves a row *through* an @internal symbol, it
preserves the reference.

Add ArchetypeSchema / ArchetypeRowOf / ArchetypeHandleOf, which resolve an
archetype's row from a small *public* schema ({ components, archetypes }) rather
than the plugin database type. The emitted type then references only public
symbols: no plugin type, no @internal symbol, self-contained, and downstream
rows resolve to concrete columns.

Add an end-to-end emit acceptance gate (scripts/emit-stripinternal, `pnpm
check:emit`): emits a stripInternal fixture and type-checks a downstream
consumer, asserting no TS7056, the @internal plugin/db type is stripped, the
service .d.ts is self-contained, and rows resolve to exact concrete columns.

The README documents the pattern plus the declaration-emit footguns surfaced
while building the gate (reference the handle inline; no fenced code blocks in
JSDoc above an exported type; don't let the schema be a module's lone export).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant