fix: update mcp and nodes for SiteNode.children string[] schema change

Fixes breakage introduced by #320 which changed `SiteNode.children` from
embedded `BuildingNode | ItemNode` objects to flat `string[]` IDs.

- `packages/mcp/src/lib/rehydrate-site-children.ts`: replace now-obsolete
  re-embedding logic with a no-op passthrough (call-site compatible)
- `packages/mcp/src/tools/variants/generate-variants.ts`: drop the inline
  copy of the same function and its call
- `packages/nodes/src/site/renderer.tsx`: cast `childId as AnyNodeId` since
  `SiteNode.children` is now `string[]`, not `AnyNodeId[]`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaafar El Harouchi
2026-05-21 13:17:15 +01:00
co-authored by Claude Sonnet 4.6
parent 6ea622af56
commit 01639e42e4
3 changed files with 9 additions and 76 deletions
@@ -1,40 +1,12 @@
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph' 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, * Previously re-embedded `SiteNode.children` from flat IDs back to full node
* but core's `SiteNode` schema expects an array of embedded `BuildingNode` / * objects to match the old schema. Since `SiteNode.children` is now
* `ItemNode` objects (see `packages/mcp/CROSS_CUTTING.md` §2). To keep the * `string[]` (upstream change), `cloneSceneGraph` / `forkSceneGraph` already
* cloned graph validating against `AnyNode`, re-embed the site children from * produce the correct form — this function is a no-op kept for call-site
* the flat dict. * compatibility.
*
* Pure: returns a new graph without mutating the input.
*/ */
export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph { export function rehydrateSiteChildren(graph: SceneGraph): SceneGraph {
const out: SceneGraph = { return graph
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<string, unknown>)) {
rehydrated.push(child as AnyNode)
}
}
out.nodes[id as keyof typeof out.nodes] = {
...(node as AnyNode),
children: rehydrated,
} as AnyNode
}
return out
} }
@@ -1,6 +1,6 @@
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { forkSceneGraph, type SceneGraph } from '@pascal-app/core/clone-scene-graph' 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 { z } from 'zod'
import type { SceneOperations } from '../../operations' import type { SceneOperations } from '../../operations'
import { ErrorCode, throwMcpError } from '../errors' 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<string, unknown>)) {
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 * Count how many nodes in a graph fail `AnyNode` validation. Used to keep the
* tool from returning silently corrupt variants. * tool from returning silently corrupt variants.
@@ -140,8 +103,6 @@ export function registerGenerateVariants(server: McpServer, bridge: SceneOperati
for (const kind of mutations) { for (const kind of mutations) {
forked = applyMutation(forked, rng, kind) forked = applyMutation(forked, rng, kind)
} }
// Re-embed site children so variants match the SiteNode schema.
forked = rehydrateSiteChildren(forked)
const invalidCount = countInvalidNodes(forked) const invalidCount = countInvalidNodes(forked)
if (invalidCount > 0) { if (invalidCount > 0) {
+2 -2
View File
@@ -1,6 +1,6 @@
'use client' '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 { NodeRenderer, unionPolygons, useNodeEvents, useViewer } from '@pascal-app/viewer'
import { useMemo, useRef } from 'react' import { useMemo, useRef } from 'react'
import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three' import { BufferGeometry, Float32BufferAttribute, type Group, Path, Shape } from 'three'
@@ -117,7 +117,7 @@ export const SiteRenderer = ({ node }: { node: SiteNode }) => {
<group ref={ref} {...handlers}> <group ref={ref} {...handlers}>
{/* Render children (buildings and items) */} {/* Render children (buildings and items) */}
{node.children.map((childId) => ( {node.children.map((childId) => (
<NodeRenderer key={childId} nodeId={childId} /> <NodeRenderer key={childId} nodeId={childId as AnyNodeId} />
))} ))}
{/* Ground fill: site polygon with slab holes, occludes below-grade geometry */} {/* Ground fill: site polygon with slab holes, occludes below-grade geometry */}