From e010c47110e97bbda36fb3341536c991213631e2 Mon Sep 17 00:00:00 2001 From: Christian Byrne Date: Sun, 10 May 2026 20:38:44 -0700 Subject: [PATCH] fix(extension-api): unify NodeEntityId/WidgetEntityId brand and tighten World stub MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves ~40 pre-existing typecheck failures on the foundation branch caused by divergent entity-ID brand definitions: - src/world/entityIds.ts brands as string + brand - src/extension-api/{node,widget}.ts brands as number + brand Both are now unified by re-exporting the world-layer brand from the public API surface (string is canonical because Phase A entity IDs are formatted like 'node::'). Also: - World.getComponent / setComponent / removeComponent / entitiesWith are now properly generic over , so call sites get typed data back instead of unknown. - WidgetComponentSchema/Display/Value/Serialize/Container in src/world/widgets/widgetComponents.ts get real data shapes (type/options/label/hidden/disabled/value/serialize/widgetIds) instead of opaque 'object'. - getMode() casts the int return to NodeMode union. - Hook result is typed as unknown so the runtime defensive Promise + setupReturn checks don't trip TS2358 / TS1345. - dynamicPrompts.v2.ts: widget.getValue() (method-generic does not exist) → widget.getValue() as string. Result: 'pnpm typecheck' is now clean on ext-api/i-foundation. --- src/extension-api/node.ts | 7 ++++-- src/extension-api/widget.ts | 6 +++-- src/extensions/core/dynamicPrompts.v2.ts | 2 +- src/services/extension-api-service.ts | 5 ++-- src/world/widgets/widgetComponents.ts | 30 ++++++++++++++++++++---- src/world/worldInstance.ts | 23 ++++++++++++++---- 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/extension-api/node.ts b/src/extension-api/node.ts index eda59f3f0e..05e3b69d5e 100644 --- a/src/extension-api/node.ts +++ b/src/extension-api/node.ts @@ -23,11 +23,14 @@ import type { WidgetHandle, WidgetOptions } from './widget' /** * Branded entity ID for nodes. Prevents mixing node IDs with widget IDs - * at compile time. The underlying value is always `number`. + * at compile time. Re-exported from the world layer so the entire codebase + * shares a single brand. The underlying value is `string` in Phase A + * (e.g. `node::`). * * @stability stable */ -export type NodeEntityId = number & { readonly __brand: 'NodeEntityId' } +import type { NodeEntityId } from '@/world/entityIds' +export type { NodeEntityId } // ─── Geometry ──────────────────────────────────────────────────────────────── diff --git a/src/extension-api/widget.ts b/src/extension-api/widget.ts index a9780a0ce2..b0ebe69161 100644 --- a/src/extension-api/widget.ts +++ b/src/extension-api/widget.ts @@ -21,11 +21,13 @@ import type { AsyncHandler, Handler, Unsubscribe } from './events' /** * Branded entity ID for widgets. Prevents mixing widget IDs with node IDs - * at compile time. The underlying value is always `number`. + * at compile time. Re-exported from the world layer so the entire codebase + * shares a single brand. The underlying value is `string` in Phase A. * * @stability stable */ -export type WidgetEntityId = number & { readonly __brand: 'WidgetEntityId' } +import type { WidgetEntityId } from '@/world/entityIds' +export type { WidgetEntityId } // ─── Widget value ──────────────────────────────────────────────────────────── diff --git a/src/extensions/core/dynamicPrompts.v2.ts b/src/extensions/core/dynamicPrompts.v2.ts index d7dacc2ead..a028bb6f94 100644 --- a/src/extensions/core/dynamicPrompts.v2.ts +++ b/src/extensions/core/dynamicPrompts.v2.ts @@ -16,7 +16,7 @@ defineNodeExtension({ if (widget.getOption('dynamicPrompts')) { widget.on('beforeSerialize', (e) => { if (e.context === 'prompt') { - const value = widget.getValue() + const value = widget.getValue() as string e.setSerializedValue( typeof value === 'string' ? processDynamicPrompt(value) : value ) diff --git a/src/services/extension-api-service.ts b/src/services/extension-api-service.ts index 81e2f34d44..8a6fe2b4d3 100644 --- a/src/services/extension-api-service.ts +++ b/src/services/extension-api-service.ts @@ -42,6 +42,7 @@ import { defineComponentKey } from '@/world/componentKey' import type { NodeHandle, + NodeMode, SlotInfo, SlotEntityId as PublicSlotEntityId, Point, @@ -287,7 +288,7 @@ function createNodeHandle(nodeId: NodeEntityId): NodeHandle { return world.getComponent(nodeId, NodeVisualKey)?.title ?? '' }, getMode() { - return world.getComponent(nodeId, ExecutionKey)?.mode ?? 0 + return (world.getComponent(nodeId, ExecutionKey)?.mode ?? 0) as NodeMode }, getProperty(key: string): T | undefined { @@ -552,7 +553,7 @@ export function mountExtensionsForNode(nodeEntityId: NodeEntityId): void { _currentScope = record pauseTracking() try { - const result = hook(createNodeHandle(nodeEntityId)) + const result: unknown = hook(createNodeHandle(nodeEntityId)) if (result instanceof Promise) { // Async setup is not supported (D10c) — catch to prevent unhandled rejection result.catch((err) => { diff --git a/src/world/widgets/widgetComponents.ts b/src/world/widgets/widgetComponents.ts index 2f270b2e76..f10dedd296 100644 --- a/src/world/widgets/widgetComponents.ts +++ b/src/world/widgets/widgetComponents.ts @@ -2,9 +2,29 @@ // Tests mock this module via vi.mock('@/world/widgets/widgetComponents'). import { defineComponentKey } from '../componentKey' +import type { NodeEntityId, WidgetEntityId } from '../entityIds' -export const WidgetComponentContainer = defineComponentKey('WidgetComponentContainer') -export const WidgetComponentDisplay = defineComponentKey('WidgetComponentDisplay') -export const WidgetComponentSchema = defineComponentKey('WidgetComponentSchema') -export const WidgetComponentSerialize = defineComponentKey('WidgetComponentSerialize') -export const WidgetComponentValue = defineComponentKey('WidgetComponentValue') +interface WidgetContainerData { + widgetIds: WidgetEntityId[] +} +interface WidgetDisplayData { + label?: string + hidden?: boolean + disabled?: boolean +} +interface WidgetSchemaData { + type?: string + options?: Record +} +interface WidgetSerializeData { + serialize?: boolean +} +interface WidgetValueData { + value?: unknown +} + +export const WidgetComponentContainer = defineComponentKey('WidgetComponentContainer') +export const WidgetComponentDisplay = defineComponentKey('WidgetComponentDisplay') +export const WidgetComponentSchema = defineComponentKey('WidgetComponentSchema') +export const WidgetComponentSerialize = defineComponentKey('WidgetComponentSerialize') +export const WidgetComponentValue = defineComponentKey('WidgetComponentValue') diff --git a/src/world/worldInstance.ts b/src/world/worldInstance.ts index 17b0d2f028..34f3a0c3bb 100644 --- a/src/world/worldInstance.ts +++ b/src/world/worldInstance.ts @@ -1,11 +1,26 @@ // Phase A stub — replaced by real ECS world when PR #11939 lands. // Tests mock this module via vi.mock('@/world/worldInstance'). +import type { ComponentKey } from './componentKey' +import type { EntityId } from './entityIds' + export interface World { - getComponent(entityId: unknown, key: unknown): unknown - setComponent(entityId: unknown, key: unknown, data: unknown): void - removeComponent(entityId: unknown, key: unknown): void - entitiesWith(key: unknown): unknown[] + getComponent( + entityId: TEntity, + key: ComponentKey + ): TData | undefined + setComponent( + entityId: TEntity, + key: ComponentKey, + data: TData + ): void + removeComponent( + entityId: TEntity, + key: ComponentKey + ): void + entitiesWith( + key: ComponentKey + ): TEntity[] } export function getWorld(): World {