diff --git a/packages/mcp/src/lib/rehydrate-site-children.ts b/packages/mcp/src/lib/rehydrate-site-children.ts index fb0435a5e..46cd38169 100644 --- a/packages/mcp/src/lib/rehydrate-site-children.ts +++ b/packages/mcp/src/lib/rehydrate-site-children.ts @@ -1,40 +1,12 @@ import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' -import type { AnyNode } from '@pascal-app/core/schema' /** - * `cloneSceneGraph` normalises `SiteNode.children` to an array of node IDs, - * but core's `SiteNode` schema expects an array of embedded `BuildingNode` / - * `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the - * cloned graph validating against `AnyNode`, re-embed the site children from - * the flat dict. - * - * Pure: returns a new graph without mutating the input. + * Previously re-embedded `SiteNode.children` from flat IDs back to full node + * objects to match the old schema. Since `SiteNode.children` is now + * `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already + * produce the correct form — this function is a no-op kept for call-site + * compatibility. */ export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph { - const out: SceneGraph = { - nodes: { ...graph.nodes }, - rootNodeIds: [...graph.rootNodeIds], - ...(graph.collections ? { collections: graph.collections } : {}), - } - for (const [id, node] of Object.entries(out.nodes)) { - if (node.type !== 'site') continue - const childrenField = (node as { children?: unknown[] }).children - if (!Array.isArray(childrenField)) continue - const rehydrated: AnyNode[] = [] - for (const child of childrenField) { - if (typeof child === 'string') { - const target = out.nodes[child as keyof typeof out.nodes] - if (target && (target.type === 'building' || target.type === 'item')) { - rehydrated.push(target) - } - } else if (child && typeof child === 'object' && 'id' in (child as Record)) { - rehydrated.push(child as AnyNode) - } - } - out.nodes[id as keyof typeof out.nodes] = { - ...(node as AnyNode), - children: rehydrated, - } as AnyNode - } - return out + return graph } diff --git a/packages/mcp/src/tools/variants/generate-variants.ts b/packages/mcp/src/tools/variants/generate-variants.ts index a96e9cf92..4c04d115c 100644 --- a/packages/mcp/src/tools/variants/generate-variants.ts +++ b/packages/mcp/src/tools/variants/generate-variants.ts @@ -1,6 +1,6 @@ import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import { forkSceneGraph, type SceneGraph } from '@pascal-app/core/clone-scene-graph' -import { type AnyNode, AnyNode as AnyNodeSchema } from '@pascal-app/core/schema' +import { AnyNode as AnyNodeSchema } from '@pascal-app/core/schema' import { z } from 'zod' import type { SceneOperations } from '../../operations' import { ErrorCode, throwMcpError } from '../errors' @@ -43,43 +43,6 @@ export const generateVariantsOutput = { ), } -/** - * `forkSceneGraph` normalises `SiteNode.children` to string IDs, but the - * `SiteNode` schema declares that field as an array of full `BuildingNode` / - * `ItemNode` objects (see CROSS_CUTTING §2). To keep variants validating - * against `AnyNode`, re-embed the site children from the flat dict. - * - * Pure: returns a new graph without mutating the input. - */ -function rehydrateSiteChildren(graph: SceneGraph): SceneGraph { - const out: SceneGraph = { - nodes: { ...graph.nodes }, - rootNodeIds: [...graph.rootNodeIds], - ...(graph.collections ? { collections: graph.collections } : {}), - } - for (const [id, node] of Object.entries(out.nodes)) { - if (node.type !== 'site') continue - const childrenField = (node as { children?: unknown[] }).children - if (!Array.isArray(childrenField)) continue - const rehydrated: AnyNode[] = [] - for (const child of childrenField) { - if (typeof child === 'string') { - const target = out.nodes[child as keyof typeof out.nodes] - if (target && (target.type === 'building' || target.type === 'item')) { - rehydrated.push(target) - } - } else if (child && typeof child === 'object' && 'id' in (child as Record)) { - rehydrated.push(child as AnyNode) - } - } - out.nodes[id as keyof typeof out.nodes] = { - ...(node as AnyNode), - children: rehydrated, - } as AnyNode - } - return out -} - /** * Count how many nodes in a graph fail `AnyNode` validation. Used to keep the * tool from returning silently corrupt variants. @@ -140,8 +103,6 @@ export function registerGenerateVariants(server: McpServer, bridge: SceneOperati for (const kind of mutations) { forked = applyMutation(forked, rng, kind) } - // Re-embed site children so variants match the SiteNode schema. - forked = rehydrateSiteChildren(forked) const invalidCount = countInvalidNodes(forked) if (invalidCount > 0) { diff --git a/packages/nodes/src/site/renderer.tsx b/packages/nodes/src/site/renderer.tsx index 89ff65cd1..ecec72bc1 100644 --- a/packages/nodes/src/site/renderer.tsx +++ b/packages/nodes/src/site/renderer.tsx @@ -1,6 +1,6 @@ 'use client' -import { type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core' +import { type AnyNodeId, type SiteNode, type SlabNode, useRegistry, useScene } from '@pascal-app/core' import { NodeRenderer, unionPolygons, useNodeEvents, useViewer } from '@pascal-app/viewer' import { useMemo, useRef } from 'react' import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three' @@ -117,7 +117,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => { {/* Render children (buildings and items) */} {node.children.map((childId) => ( - + ))} {/* Ground fill: site polygon with slab holes, occludes below-grade geometry */}