fix(extension-api): unify NodeEntityId/WidgetEntityId brand and tighten World stub

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:<graphUuid>:<localId>').

Also:
- World.getComponent / setComponent / removeComponent / entitiesWith are
  now properly generic over <TData, TEntity>, 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<string>() (method-generic does
  not exist) → widget.getValue() as string.

Result: 'pnpm typecheck' is now clean on ext-api/i-foundation.
This commit is contained in:
Christian Byrne
2026-05-10 20:38:44 -07:00
parent 192c102c7a
commit e010c47110
6 changed files with 57 additions and 16 deletions

View File

@@ -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:<graphUuid>:<localId>`).
*
* @stability stable
*/
export type NodeEntityId = number & { readonly __brand: 'NodeEntityId' }
import type { NodeEntityId } from '@/world/entityIds'
export type { NodeEntityId }
// ─── Geometry ────────────────────────────────────────────────────────────────

View File

@@ -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 ────────────────────────────────────────────────────────────

View File

@@ -16,7 +16,7 @@ defineNodeExtension({
if (widget.getOption('dynamicPrompts')) {
widget.on('beforeSerialize', (e) => {
if (e.context === 'prompt') {
const value = widget.getValue<string>()
const value = widget.getValue() as string
e.setSerializedValue(
typeof value === 'string' ? processDynamicPrompt(value) : value
)

View File

@@ -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<T = unknown>(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) => {

View File

@@ -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<object, unknown>('WidgetComponentContainer')
export const WidgetComponentDisplay = defineComponentKey<object, unknown>('WidgetComponentDisplay')
export const WidgetComponentSchema = defineComponentKey<object, unknown>('WidgetComponentSchema')
export const WidgetComponentSerialize = defineComponentKey<object, unknown>('WidgetComponentSerialize')
export const WidgetComponentValue = defineComponentKey<object, unknown>('WidgetComponentValue')
interface WidgetContainerData {
widgetIds: WidgetEntityId[]
}
interface WidgetDisplayData {
label?: string
hidden?: boolean
disabled?: boolean
}
interface WidgetSchemaData {
type?: string
options?: Record<string, unknown>
}
interface WidgetSerializeData {
serialize?: boolean
}
interface WidgetValueData {
value?: unknown
}
export const WidgetComponentContainer = defineComponentKey<WidgetContainerData, NodeEntityId>('WidgetComponentContainer')
export const WidgetComponentDisplay = defineComponentKey<WidgetDisplayData, WidgetEntityId>('WidgetComponentDisplay')
export const WidgetComponentSchema = defineComponentKey<WidgetSchemaData, WidgetEntityId>('WidgetComponentSchema')
export const WidgetComponentSerialize = defineComponentKey<WidgetSerializeData, WidgetEntityId>('WidgetComponentSerialize')
export const WidgetComponentValue = defineComponentKey<WidgetValueData, WidgetEntityId>('WidgetComponentValue')

View File

@@ -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<TData, TEntity extends EntityId>(
entityId: TEntity,
key: ComponentKey<TData, TEntity>
): TData | undefined
setComponent<TData, TEntity extends EntityId>(
entityId: TEntity,
key: ComponentKey<TData, TEntity>,
data: TData
): void
removeComponent<TData, TEntity extends EntityId>(
entityId: TEntity,
key: ComponentKey<TData, TEntity>
): void
entitiesWith<TData, TEntity extends EntityId>(
key: ComponentKey<TData, TEntity>
): TEntity[]
}
export function getWorld(): World {