feat(mcp,editor): Option A+B storage + 10 agent deliverables (Phase 7)

Ships the combined filesystem/Supabase storage adapter + MCP scene
lifecycle tools + Next.js API routes + editor /scene/[id] route, so
an MCP save is directly openable at /scene/<id> without any
injection hack. End-to-end verified: 10/10 e2e steps pass.

Storage (A1/A2/A3):
- SceneStore interface + error classes + slug helpers
- FilesystemSceneStore at $PASCAL_DATA_DIR (defaults XDG/~/.pascal)
  with atomic writes, .index sidecar, optimistic locking
- SupabaseSceneStore with scenes + scene_revisions tables, RLS
  migration SQL, mock-backed unit tests
- createSceneStore(env) auto-selects based on SUPABASE_URL +
  SUPABASE_SERVICE_ROLE_KEY

MCP tools (A4, A8, A9, A10):
- save_scene / load_scene / list_scenes / delete_scene / rename_scene
- list_templates / create_from_template (3 seed templates:
  empty-studio, two-bedroom, garden-house)
- generate_variants (7 mutation kinds, seeded RNG, save=true|false)
- photo_to_scene (vision sampling → scene graph → save)

Editor (A5, A6):
- /api/scenes + /api/scenes/[id] with RFC 7232 If-Match locking
- /scene/[id] and /scenes route pages with save button, SceneLoader
- Removed the window.__pascalScene dev injection hack

Security + UX edges (A7, A8):
- AssetUrl Zod validator: asset:// blob: data:image/ /path https:
  (http://localhost for dev) + PASCAL_ALLOWED_ASSET_ORIGINS env
  allowlist. Hardens scan.url, guide.url, item.asset.src,
  material.texture.url, MaterialMaps.*Map
- Auto-frame camera on empty→non-empty scene transition
  (camera-controls:fit-scene emitter event)

Shared utilities:
- rehydrateSiteChildren() extracted to packages/mcp/src/lib/ and
  used by both create-from-template and generate-variants to work
  around the SiteNode.children-as-objects vs. ids inconsistency
  (CROSS_CUTTING §2)
- Storage + MCP subpath exports added to packages/mcp/package.json
  (CROSS_CUTTING §4)

Tests: 293 pass / 0 fail across 40 files (was 142 pre-Phase-7).
Biome: clean.

Phase-7 e2e script at packages/mcp/test-reports/phase7-e2e.ts:
MCP HTTP + editor Next.js both point at $PASCAL_DATA_DIR =
/tmp/pascal-e2e, save_scene from MCP, GET /api/scenes/<id> from
editor server, /scenes list page renders all saved scenes, scene
page renders SceneLoader, delete_scene works.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adrian Perez
2026-04-18 19:29:28 +02:00
co-authored by Claude Opus 4.7
parent 42bd05db9c
commit e8d0b13ff5
81 changed files with 8933 additions and 1213 deletions
+25 -11
View File
@@ -19,12 +19,12 @@
* bun packages/mcp/test-reports/casa-sol/build.ts
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
const HERE = dirname(fileURLToPath(import.meta.url))
const SERVER_URL = 'http://localhost:3917/mcp'
@@ -234,7 +234,14 @@ const OPENINGS: OpeningSpec[] = [
},
{ wallDesignId: 2, kind: 'door', position: 0.65, width: 0.9, height: 2.1, label: 'kitchen-back' },
{ wallDesignId: 6, kind: 'door', position: 0.3, width: 0.8, height: 2.1, label: 'master-door' },
{ wallDesignId: 7, kind: 'door', position: 0.5, width: 0.8, height: 2.1, label: 'bedroom-2-door' },
{
wallDesignId: 7,
kind: 'door',
position: 0.5,
width: 0.8,
height: 2.1,
label: 'bedroom-2-door',
},
{ wallDesignId: 9, kind: 'door', position: 0.5, width: 0.7, height: 2.0, label: 'bath2-door' },
{
wallDesignId: 1,
@@ -329,8 +336,12 @@ async function main(): Promise<void> {
log(`[casa] falling back to in-memory MCP server (same tool surface)`)
// Load the in-process MCP server to keep the build moving. This preserves
// the tool contract; the only thing we lose is the HTTP wire test.
const { SceneBridge } = await import('/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/bridge/scene-bridge.ts')
const { createPascalMcpServer } = await import('/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/server.ts')
const { SceneBridge } = await import(
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/bridge/scene-bridge.ts'
)
const { createPascalMcpServer } = await import(
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/src/server.ts'
)
const bridge = new SceneBridge()
bridge.loadDefault()
@@ -530,7 +541,9 @@ async function main(): Promise<void> {
})
}
}
const ids = openingResults.filter((r) => r.ok && r.openingId).map((r) => r.openingId!) as string[]
const ids = openingResults
.filter((r) => r.ok && r.openingId)
.map((r) => r.openingId!) as string[]
return {
summary: `${doors} doors, ${windows} windows, ${failures.length} failures`,
nodeIds: ids,
@@ -691,9 +704,8 @@ async function main(): Promise<void> {
// ----- Step 12: Final validate + summary counts -----
const finalValid = await runValidate(client, 'final')
const allNodes = (
await callTool<{ nodes: Array<{ type: string }> }>(client, 'find_nodes', {})
).nodes
const allNodes = (await callTool<{ nodes: Array<{ type: string }> }>(client, 'find_nodes', {}))
.nodes
const tally: Record<string, number> = {}
for (const n of allNodes) {
tally[n.type] = (tally[n.type] ?? 0) + 1
@@ -790,7 +802,9 @@ async function main(): Promise<void> {
lines.push('## Validation')
lines.push('')
lines.push(`- Final \`validate_scene\`: valid=\`${finalValid.valid}\`, errors=${finalValid.errors.length}`)
lines.push(
`- Final \`validate_scene\`: valid=\`${finalValid.valid}\`, errors=${finalValid.errors.length}`,
)
if (!finalValid.valid && finalValid.errors.length > 0) {
lines.push('')
lines.push('Errors (verbatim):')
File diff suppressed because it is too large Load Diff
+95
View File
@@ -0,0 +1,95 @@
/**
* Phase 7 end-to-end: prove MCP save_scene → editor /scene/[id] renders the scene
* without any window.__pascalScene injection.
*
* Run: PASCAL_DATA_DIR=/tmp/pascal-e2e bun run packages/mcp/test-reports/phase7-e2e.ts
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const MCP_URL = 'http://localhost:3917/mcp'
const EDITOR_URL = 'http://localhost:3002'
async function main() {
console.log('---- Phase 7 e2e ----')
// 1. Connect to MCP over HTTP
const transport = new StreamableHTTPClientTransport(new URL(MCP_URL))
const client = new Client({ name: 'e2e', version: '0.0.0' })
await client.connect(transport)
console.log('OK 1 connect MCP HTTP')
// 2. Build a scene from a template
const created = await client.callTool({
name: 'create_from_template',
arguments: { id: 'two-bedroom', name: 'e2e-two-bedroom' },
})
if (created.isError) throw new Error(`create_from_template: ${JSON.stringify(created)}`)
console.log('OK 2 create_from_template two-bedroom')
// 3. Save it
const saved = await client.callTool({
name: 'save_scene',
arguments: { name: 'e2e test house' },
})
if (saved.isError) throw new Error(`save_scene: ${JSON.stringify(saved)}`)
const savedData = JSON.parse((saved.content as Array<{ text: string }>)[0]!.text)
const sceneId = savedData.id as string
console.log(`OK 3 save_scene -> id=${sceneId}, version=${savedData.version}`)
// 4. list_scenes
const list = await client.callTool({ name: 'list_scenes', arguments: {} })
if (list.isError) throw new Error(`list_scenes: ${JSON.stringify(list)}`)
const listData = JSON.parse((list.content as Array<{ text: string }>)[0]!.text)
console.log(`OK 4 list_scenes -> ${listData.scenes.length} scenes`)
// 5. Fetch via editor's API (proves A5 works against the same store)
const apiRes = await fetch(`${EDITOR_URL}/api/scenes/${sceneId}`)
if (!apiRes.ok) throw new Error(`GET /api/scenes/${sceneId}${apiRes.status}`)
const apiBody = await apiRes.json()
const nodeCount = Object.keys(apiBody.graph.nodes).length
console.log(`OK 5 editor /api/scenes/${sceneId}${nodeCount} nodes`)
// 6. Fetch editor's /scenes list page (HTML)
const listHtmlRes = await fetch(`${EDITOR_URL}/scenes`)
if (!listHtmlRes.ok) throw new Error(`GET /scenes → ${listHtmlRes.status}`)
const listHtml = await listHtmlRes.text()
const hasSceneLink = listHtml.includes(`/scene/${sceneId}`)
console.log(`OK 6 /scenes renders, links scene: ${hasSceneLink}`)
// 7. Fetch /scene/[id] page
const sceneHtmlRes = await fetch(`${EDITOR_URL}/scene/${sceneId}`)
if (!sceneHtmlRes.ok) throw new Error(`GET /scene/${sceneId}${sceneHtmlRes.status}`)
console.log(`OK 7 /scene/${sceneId} renders (${sceneHtmlRes.status})`)
// 8. generate_variants — 3 variants, save=true
const variants = await client.callTool({
name: 'generate_variants',
arguments: { count: 3, vary: ['wall-thickness', 'wall-height'], save: true, seed: 42 },
})
if (variants.isError) throw new Error(`generate_variants: ${JSON.stringify(variants)}`)
const variantsData = JSON.parse((variants.content as Array<{ text: string }>)[0]!.text)
console.log(`OK 8 generate_variants -> ${variantsData.variants.length} variants`)
// 9. list_scenes again — should be > 1
const list2 = await client.callTool({ name: 'list_scenes', arguments: {} })
const list2Data = JSON.parse((list2.content as Array<{ text: string }>)[0]!.text)
console.log(`OK 9 list_scenes now shows ${list2Data.scenes.length} scenes`)
// 10. delete_scene
const deleted = await client.callTool({ name: 'delete_scene', arguments: { id: sceneId } })
if (deleted.isError) throw new Error(`delete_scene: ${JSON.stringify(deleted)}`)
const deletedData = JSON.parse((deleted.content as Array<{ text: string }>)[0]!.text)
console.log(`OK 10 delete_scene -> deleted=${deletedData.deleted}`)
await client.close()
console.log(`\nSceneId to open in browser: ${EDITOR_URL}/scenes`)
console.log(`Direct: ${EDITOR_URL}/scene/${variantsData.variants[0].sceneId}`)
console.log('\n✅ Phase 7 e2e PASSED\n')
}
main().catch((err) => {
console.error('\n❌ e2e failed:', err)
process.exit(1)
})
@@ -0,0 +1,82 @@
# Phase 7 plan — A+B storage + edge cases + ideas
## Shared SceneStore contract (every agent reuses this)
```ts
// packages/mcp/src/storage/types.ts (Agent 1 owns)
import type { SceneGraph } from '@pascal-app/core/clone-scene-graph'
export type SceneId = string // slug-safe (a-z0-9-), ≤ 64 chars
export interface SceneMeta {
id: SceneId
name: string
projectId: string | null
thumbnailUrl: string | null
version: number // monotonic, incremented on every save
createdAt: string // ISO 8601
updatedAt: string
ownerId: string | null
sizeBytes: number
nodeCount: number
}
export interface SceneWithGraph extends SceneMeta {
graph: SceneGraph
}
export interface SceneStore {
readonly backend: 'filesystem' | 'supabase'
save(opts: {
id?: SceneId
name: string
projectId?: string | null
ownerId?: string | null
graph: SceneGraph
thumbnailUrl?: string | null
expectedVersion?: number // 409 on mismatch
}): Promise<SceneMeta>
load(id: SceneId): Promise<SceneWithGraph | null>
list(opts?: { projectId?: string; ownerId?: string; limit?: number }): Promise<SceneMeta[]>
delete(id: SceneId, opts?: { expectedVersion?: number }): Promise<boolean>
rename(id: SceneId, newName: string, opts?: { expectedVersion?: number }): Promise<SceneMeta>
}
export class SceneNotFoundError extends Error { code = 'not_found' as const }
export class SceneVersionConflictError extends Error { code = 'version_conflict' as const }
export class SceneInvalidError extends Error { code = 'invalid' as const }
export class SceneTooLargeError extends Error { code = 'too_large' as const }
export function createSceneStore(env?: NodeJS.ProcessEnv): SceneStore { /* factory */ }
```
## Agent scope map
| Agent | Scope | File ownership |
|---|---|---|
| A1 | Storage interface + types + factory | `packages/mcp/src/storage/types.ts`, `packages/mcp/src/storage/index.ts`, `packages/mcp/src/storage/store.test.ts` |
| A2 | Filesystem impl | `packages/mcp/src/storage/filesystem-scene-store.ts` + tests |
| A3 | Supabase impl + migration SQL | `packages/mcp/src/storage/supabase-scene-store.ts`, `packages/mcp/sql/migrations/0001_scenes.sql` + tests |
| A4 | MCP scene-lifecycle tools | `packages/mcp/src/tools/scene-lifecycle/*.ts` + index wiring |
| A5 | Next.js API routes | `apps/editor/app/api/scenes/route.ts`, `apps/editor/app/api/scenes/[id]/route.ts`, `apps/editor/lib/scene-store-server.ts` |
| A6 | Editor routes + kill dev hook | `apps/editor/app/scene/[id]/page.tsx`, `apps/editor/app/scenes/page.tsx`, edit `apps/editor/app/page.tsx` |
| A7 | URL hardening in core schemas | `packages/core/src/schema/nodes/{scan,guide,item}.ts`, `packages/core/src/schema/material.ts` + migration |
| A8 | Auto-frame camera + scene templates | `packages/editor/src/hooks/use-auto-frame.ts`, `packages/mcp/src/templates/*`, `packages/mcp/src/tools/scene-lifecycle/list-templates.ts` |
| A9 | Multi-variant generation | `packages/mcp/src/tools/variants/*` + tests |
| A10 | Photo → scene + example | `packages/mcp/src/tools/photo-to-scene/*` (orchestrator), update `README.md`, new `examples/photo-to-scene.md` |
## Global coordination rules
- Agent A1 drops first (interface only). A2, A3, A4, A5 read from `packages/mcp/src/storage/types.ts`; if it doesn't exist when they start, they should **inline a copy of the types above** and the integrator fixes up the import later.
- All MCP tools use `StreamableHTTPClientTransport`-compatible input/output Zod schemas.
- Every tool uses the shared `SceneStore` via `createSceneStore()` — never instantiates concrete stores.
- Tests are `bun:test`, colocated.
- Biome 2-space, single quote, no semicolons, trailing commas all.
- Do NOT run `bun install` — already done.
- Do NOT modify files outside your ownership.
## Acceptance
- `bun test --cwd packages/mcp` green.
- `bunx biome check packages/mcp apps/editor/app` green.
- `bun run --cwd packages/mcp build` green.
- `MCP save_scene → list_scenes → editor opens /scene/<id>` works without `window.__pascalScene`.
+77 -43
View File
@@ -168,18 +168,21 @@ async function main(): Promise<void> {
}
// ---- 1. get_scene ------------------------------------------------------
const sceneResult = await run('get_scene', {}, {
describe: (r) => {
const s = r.structuredContent as any
const nodeCount = s?.nodes ? Object.keys(s.nodes).length : 0
const rootCount = s?.rootNodeIds?.length ?? 0
return `${nodeCount} nodes, ${rootCount} roots`
const sceneResult = await run(
'get_scene',
{},
{
describe: (r) => {
const s = r.structuredContent as any
const nodeCount = s?.nodes ? Object.keys(s.nodes).length : 0
const rootCount = s?.rootNodeIds?.length ?? 0
return `${nodeCount} nodes, ${rootCount} roots`
},
},
})
)
// Discover key node ids from the scene snapshot.
const sceneNodes: Record<string, any> =
(sceneResult?.structuredContent as any)?.nodes ?? {}
const sceneNodes: Record<string, any> = (sceneResult?.structuredContent as any)?.nodes ?? {}
const sceneRoots: string[] = (sceneResult?.structuredContent as any)?.rootNodeIds ?? []
const findFirst = (type: string): any | null => {
@@ -198,30 +201,41 @@ async function main(): Promise<void> {
)
// ---- 2. get_node -------------------------------------------------------
await run('get_node', { id: siteNode?.id ?? sceneRoots[0] ?? '' }, {
describe: (r) => {
const n = (r.structuredContent as any)?.node
return `node type=${n?.type}, id=${n?.id}`
await run(
'get_node',
{ id: siteNode?.id ?? sceneRoots[0] ?? '' },
{
describe: (r) => {
const n = (r.structuredContent as any)?.node
return `node type=${n?.type}, id=${n?.id}`
},
},
})
)
// ---- 3. describe_node --------------------------------------------------
await run('describe_node', { id: siteNode?.id ?? sceneRoots[0] ?? '' }, {
describe: (r) => {
const s = r.structuredContent as any
return `type=${s?.type}, ${s?.childrenIds?.length ?? 0} children`
await run(
'describe_node',
{ id: siteNode?.id ?? sceneRoots[0] ?? '' },
{
describe: (r) => {
const s = r.structuredContent as any
return `type=${s?.type}, ${s?.childrenIds?.length ?? 0} children`
},
},
})
)
// ---- 4. find_nodes -----------------------------------------------------
const findLevels = await run('find_nodes', { type: 'level' }, {
describe: (r) => `${(r.structuredContent as any)?.nodes?.length ?? 0} level node(s)`,
})
const findLevels = await run(
'find_nodes',
{ type: 'level' },
{
describe: (r) => `${(r.structuredContent as any)?.nodes?.length ?? 0} level node(s)`,
},
)
// Refresh levelNode from find_nodes output (most current).
const foundLevels = (findLevels?.structuredContent as any)?.nodes ?? []
const groundLevelId: string | undefined =
foundLevels[0]?.id ?? levelNode?.id ?? undefined
const groundLevelId: string | undefined = foundLevels[0]?.id ?? levelNode?.id ?? undefined
console.log(`[t1] groundLevelId=${groundLevelId}`)
// ---- 5. measure --------------------------------------------------------
@@ -462,38 +476,58 @@ async function main(): Promise<void> {
}
// ---- 14. undo ----------------------------------------------------------
await run('undo', {}, {
describe: (r) => `undone=${(r.structuredContent as any)?.undone}`,
})
await run(
'undo',
{},
{
describe: (r) => `undone=${(r.structuredContent as any)?.undone}`,
},
)
// ---- 15. redo ----------------------------------------------------------
await run('redo', {}, {
describe: (r) => `redone=${(r.structuredContent as any)?.redone}`,
})
await run(
'redo',
{},
{
describe: (r) => `redone=${(r.structuredContent as any)?.redone}`,
},
)
// ---- 16. export_json ---------------------------------------------------
await run('export_json', { pretty: true }, {
describe: (r) => {
const s = r.structuredContent as any
return `${s?.json?.length ?? 0} chars JSON`
await run(
'export_json',
{ pretty: true },
{
describe: (r) => {
const s = r.structuredContent as any
return `${s?.json?.length ?? 0} chars JSON`
},
},
})
)
// ---- 17. export_glb ----------------------------------------------------
await run('export_glb', {})
// ---- 18. validate_scene ------------------------------------------------
await run('validate_scene', {}, {
describe: (r) => {
const s = r.structuredContent as any
return `valid=${s?.valid}, errors=${s?.errors?.length ?? 0}`
await run(
'validate_scene',
{},
{
describe: (r) => {
const s = r.structuredContent as any
return `valid=${s?.valid}, errors=${s?.errors?.length ?? 0}`
},
},
})
)
// ---- 19. check_collisions ----------------------------------------------
await run('check_collisions', {}, {
describe: (r) => `${(r.structuredContent as any)?.collisions?.length ?? 0} collision(s)`,
})
await run(
'check_collisions',
{},
{
describe: (r) => `${(r.structuredContent as any)?.collisions?.length ?? 0} collision(s)`,
},
)
// ---- 20. analyze_floorplan_image — expected sampling_unavailable -------
await run(
+65 -78
View File
@@ -20,13 +20,14 @@
* best-effort report.
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const TARGET_URL = 'http://localhost:3917/mcp'
const OUT_DIR = '/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/t2-http'
const OUT_DIR =
'/Users/adrian/Desktop/editor/.worktrees/mcp-server/packages/mcp/test-reports/t2-http'
type ToolResult = {
name: string
@@ -106,9 +107,9 @@ function getStructured<T>(
const r = result.result as { structuredContent?: unknown; content?: unknown }
if (r.structuredContent !== undefined) return r.structuredContent as T
if (Array.isArray(r.content)) {
const textBlock = r.content.find(
(c) => (c as { type?: string }).type === 'text',
) as { text?: string } | undefined
const textBlock = r.content.find((c) => (c as { type?: string }).type === 'text') as
| { text?: string }
| undefined
if (textBlock?.text) {
try {
return JSON.parse(textBlock.text) as T
@@ -430,26 +431,24 @@ async function main() {
// ---- cut_opening ------------------------------------------------------
let openingId: string | null = null
{
if (!wallId) {
record('cut_opening', false, 'skipped — no wall id to cut')
} else {
const r = await callTool(clientA, 'cut_opening', {
wallId,
type: 'door',
position: 0.5,
width: 0.9,
height: 2,
})
const struct = getStructured<{ openingId: string }>(r)
openingId = struct?.openingId ?? null
record(
'cut_opening',
r.ok && typeof struct?.openingId === 'string',
r.ok ? `cut opening ${struct?.openingId}` : `error: ${String(r.error)}`,
r.latencyMs,
)
}
if (!wallId) {
record('cut_opening', false, 'skipped — no wall id to cut')
} else {
const r = await callTool(clientA, 'cut_opening', {
wallId,
type: 'door',
position: 0.5,
width: 0.9,
height: 2,
})
const struct = getStructured<{ openingId: string }>(r)
openingId = struct?.openingId ?? null
record(
'cut_opening',
r.ok && typeof struct?.openingId === 'string',
r.ok ? `cut opening ${struct?.openingId}` : `error: ${String(r.error)}`,
r.latencyMs,
)
}
// ---- set_zone ---------------------------------------------------------
@@ -475,59 +474,45 @@ async function main() {
r.latencyMs,
)
}
// ---- duplicate_level --------------------------------------------------
{
if (!extraLevelId) {
record('duplicate_level', false, 'skipped — no extra level to duplicate')
} else {
const r = await callTool(clientA, 'duplicate_level', { levelId: extraLevelId })
const struct = getStructured<{ newLevelId: string; newNodeIds: string[] }>(r)
record(
'duplicate_level',
r.ok && typeof struct?.newLevelId === 'string',
r.ok
? `duplicated → new level ${struct?.newLevelId} (${struct?.newNodeIds?.length ?? 0} nodes)`
: `error: ${String(r.error)}`,
r.latencyMs,
)
}
if (!extraLevelId) {
record('duplicate_level', false, 'skipped — no extra level to duplicate')
} else {
const r = await callTool(clientA, 'duplicate_level', { levelId: extraLevelId })
const struct = getStructured<{ newLevelId: string; newNodeIds: string[] }>(r)
record(
'duplicate_level',
r.ok && typeof struct?.newLevelId === 'string',
r.ok
? `duplicated → new level ${struct?.newLevelId} (${struct?.newNodeIds?.length ?? 0} nodes)`
: `error: ${String(r.error)}`,
r.latencyMs,
)
}
// ---- apply_patch ------------------------------------------------------
{
if (!zoneId) {
record('apply_patch', false, 'skipped — no zone to patch')
} else {
const r = await callTool(clientA, 'apply_patch', {
patches: [{ op: 'update', id: zoneId, data: { name: 'T2-zone-renamed' } }],
})
const struct = getStructured<{ appliedOps: number }>(r)
record(
'apply_patch',
r.ok && struct?.appliedOps === 1,
r.ok ? `appliedOps=${struct?.appliedOps}` : `error: ${String(r.error)}`,
r.latencyMs,
)
}
if (!zoneId) {
record('apply_patch', false, 'skipped — no zone to patch')
} else {
const r = await callTool(clientA, 'apply_patch', {
patches: [{ op: 'update', id: zoneId, data: { name: 'T2-zone-renamed' } }],
})
const struct = getStructured<{ appliedOps: number }>(r)
record(
'apply_patch',
r.ok && struct?.appliedOps === 1,
r.ok ? `appliedOps=${struct?.appliedOps}` : `error: ${String(r.error)}`,
r.latencyMs,
)
}
// ---- delete_node ------------------------------------------------------
{
if (!openingId) {
record('delete_node', false, 'skipped — no opening to delete')
} else {
const r = await callTool(clientA, 'delete_node', { id: openingId, cascade: true })
const struct = getStructured<{ deletedIds: string[] }>(r)
record(
'delete_node',
r.ok && Array.isArray(struct?.deletedIds) && (struct?.deletedIds?.length ?? 0) >= 1,
r.ok
? `deleted ${struct?.deletedIds?.length ?? 0} nodes`
: `error: ${String(r.error)}`,
r.latencyMs,
)
}
if (!openingId) {
record('delete_node', false, 'skipped — no opening to delete')
} else {
const r = await callTool(clientA, 'delete_node', { id: openingId, cascade: true })
const struct = getStructured<{ deletedIds: string[] }>(r)
record(
'delete_node',
r.ok && Array.isArray(struct?.deletedIds) && (struct?.deletedIds?.length ?? 0) >= 1,
r.ok ? `deleted ${struct?.deletedIds?.length ?? 0} nodes` : `error: ${String(r.error)}`,
r.latencyMs,
)
}
// ---- undo -------------------------------------------------------------
@@ -695,7 +680,9 @@ async function main() {
distinctSessions = sidA !== sidB
const toolsListB = await clientB.listTools()
toolCountB = toolsListB.tools.length
console.log(`Session B listTools() → ${toolCountB} tools; sessions distinct: ${distinctSessions}`)
console.log(
`Session B listTools() → ${toolCountB} tools; sessions distinct: ${distinctSessions}`,
)
const sceneB = await callTool(clientB, 'get_scene', {})
const sceneBstruct = getStructured<{ nodes: Record<string, unknown> }>(sceneB)
@@ -9,24 +9,7 @@
"metadata": {},
"polygon": {
"type": "polygon",
"points": [
[
-15,
-15
],
[
15,
-15
],
[
15,
15
],
[
-15,
15
]
]
"points": [[-15, -15], [15, -15], [15, 15], [-15, 15]]
},
"children": [
{
@@ -36,19 +19,9 @@
"parentId": null,
"visible": true,
"metadata": {},
"children": [
"level_wyuoxj87czq3v0re"
],
"position": [
0,
0,
0
],
"rotation": [
0,
0,
0
]
"children": ["level_wyuoxj87czq3v0re"],
"position": [0, 0, 0],
"rotation": [0, 0, 0]
}
]
},
@@ -59,19 +32,9 @@
"parentId": null,
"visible": true,
"metadata": {},
"children": [
"level_wyuoxj87czq3v0re"
],
"position": [
0,
0,
0
],
"rotation": [
0,
0,
0
]
"children": ["level_wyuoxj87czq3v0re"],
"position": [0, 0, 0],
"rotation": [0, 0, 0]
},
"level_wyuoxj87czq3v0re": {
"object": "node",
@@ -106,20 +69,11 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [
"window_47x40mtv2l4ca9p4",
"window_n09awmg5m3ct4fvn"
],
"children": ["window_47x40mtv2l4ca9p4", "window_n09awmg5m3ct4fvn"],
"thickness": 0.2,
"height": 2.7,
"start": [
0,
0
],
"end": [
10,
0
],
"start": [0, 0],
"end": [10, 0],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -133,14 +87,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
10,
0
],
"end": [
10,
8
],
"start": [10, 0],
"end": [10, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -151,19 +99,11 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [
"window_xlta3f3f0cnmbti3"
],
"children": ["window_xlta3f3f0cnmbti3"],
"thickness": 0.2,
"height": 2.7,
"start": [
10,
8
],
"end": [
0,
8
],
"start": [10, 8],
"end": [0, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -177,14 +117,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
0,
8
],
"end": [
0,
0
],
"start": [0, 8],
"end": [0, 0],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -198,14 +132,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
0,
5
],
"end": [
3,
5
],
"start": [0, 5],
"end": [3, 5],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -216,19 +144,11 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [
"door_cjzja4lt8owg88wg"
],
"children": ["door_cjzja4lt8owg88wg"],
"thickness": 0.2,
"height": 2.7,
"start": [
3,
5
],
"end": [
3,
8
],
"start": [3, 5],
"end": [3, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -242,14 +162,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
7,
5
],
"end": [
10,
5
],
"start": [7, 5],
"end": [10, 5],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -260,19 +174,11 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [
"door_bs7bf0azevq9vd76"
],
"children": ["door_bs7bf0azevq9vd76"],
"thickness": 0.2,
"height": 2.7,
"start": [
7,
5
],
"end": [
7,
8
],
"start": [7, 5],
"end": [7, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -283,19 +189,11 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [
"door_o8etwqsemfgj5mkj"
],
"children": ["door_o8etwqsemfgj5mkj"],
"thickness": 0.2,
"height": 2.7,
"start": [
4,
6
],
"end": [
6,
6
],
"start": [4, 6],
"end": [6, 6],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -309,14 +207,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
4,
6
],
"end": [
4,
8
],
"start": [4, 6],
"end": [4, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -330,14 +222,8 @@
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [
6,
6
],
"end": [
6,
8
],
"start": [6, 6],
"end": [6, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
@@ -349,24 +235,7 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [
[
0,
5
],
[
3,
5
],
[
3,
8
],
[
0,
8
]
],
"polygon": [[0, 5], [3, 5], [3, 8], [0, 8]],
"color": "#3b82f6"
},
"zone_u95l1bt35jci3gvu": {
@@ -377,24 +246,7 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [
[
7,
5
],
[
10,
5
],
[
10,
8
],
[
7,
8
]
],
"polygon": [[7, 5], [10, 5], [10, 8], [7, 8]],
"color": "#3b82f6"
},
"zone_r9ma8tvsqt9w1zey": {
@@ -405,24 +257,7 @@
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [
[
4,
6
],
[
6,
6
],
[
6,
8
],
[
4,
8
]
],
"polygon": [[4, 6], [6, 6], [6, 8], [4, 8]],
"color": "#3b82f6"
},
"zone_l189q61kf9ra2m8t": {
@@ -434,54 +269,18 @@
"visible": true,
"metadata": {},
"polygon": [
[
0,
0
],
[
10,
0
],
[
10,
5
],
[
7,
5
],
[
7,
8
],
[
6,
8
],
[
6,
6
],
[
4,
6
],
[
4,
8
],
[
3,
8
],
[
3,
5
],
[
0,
5
]
[0, 0],
[10, 0],
[10, 5],
[7, 5],
[7, 8],
[6, 8],
[6, 6],
[4, 6],
[4, 8],
[3, 8],
[3, 5],
[0, 5]
],
"color": "#3b82f6"
},
@@ -492,16 +291,8 @@
"parentId": "wall_qv53jm9kvl7k6slf",
"visible": true,
"metadata": {},
"position": [
0.5,
1.05,
0
],
"rotation": [
0,
0,
0
],
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_qv53jm9kvl7k6slf",
"width": 0.9,
"height": 2.1,
@@ -515,9 +306,7 @@
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -525,9 +314,7 @@
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -536,10 +323,7 @@
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [
0.04,
0.04
],
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
@@ -551,16 +335,8 @@
"parentId": "wall_hrfixeusz7zb7x63",
"visible": true,
"metadata": {},
"position": [
0.5,
1.05,
0
],
"rotation": [
0,
0,
0
],
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_hrfixeusz7zb7x63",
"width": 0.9,
"height": 2.1,
@@ -574,9 +350,7 @@
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -584,9 +358,7 @@
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -595,10 +367,7 @@
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [
0.04,
0.04
],
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
@@ -610,16 +379,8 @@
"parentId": "wall_1ullk9bm6dw15i9t",
"visible": true,
"metadata": {},
"position": [
0.5,
1.05,
0
],
"rotation": [
0,
0,
0
],
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_1ullk9bm6dw15i9t",
"width": 0.9,
"height": 2.1,
@@ -633,9 +394,7 @@
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -643,9 +402,7 @@
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [
1
],
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
@@ -654,10 +411,7 @@
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [
0.04,
0.04
],
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
@@ -669,27 +423,15 @@
"parentId": "wall_y87bsrljd2245n51",
"visible": true,
"metadata": {},
"position": [
0.3,
0.6,
0
],
"rotation": [
0,
0,
0
],
"position": [0.3, 0.6, 0],
"rotation": [0, 0, 0],
"wallId": "wall_y87bsrljd2245n51",
"width": 1.2,
"height": 1.2,
"frameThickness": 0.05,
"frameDepth": 0.07,
"columnRatios": [
1
],
"rowRatios": [
1
],
"columnRatios": [1],
"rowRatios": [1],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
@@ -703,27 +445,15 @@
"parentId": "wall_y87bsrljd2245n51",
"visible": true,
"metadata": {},
"position": [
0.7,
0.6,
0
],
"rotation": [
0,
0,
0
],
"position": [0.7, 0.6, 0],
"rotation": [0, 0, 0],
"wallId": "wall_y87bsrljd2245n51",
"width": 1.2,
"height": 1.2,
"frameThickness": 0.05,
"frameDepth": 0.07,
"columnRatios": [
1
],
"rowRatios": [
1
],
"columnRatios": [1],
"rowRatios": [1],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
@@ -737,27 +467,15 @@
"parentId": "wall_aegff27krjwgmkmi",
"visible": true,
"metadata": {},
"position": [
0.5,
0.6,
0
],
"rotation": [
0,
0,
0
],
"position": [0.5, 0.6, 0],
"rotation": [0, 0, 0],
"wallId": "wall_aegff27krjwgmkmi",
"width": 1.2,
"height": 1.2,
"frameThickness": 0.05,
"frameDepth": 0.07,
"columnRatios": [
1
],
"rowRatios": [
1
],
"columnRatios": [1],
"rowRatios": [1],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
@@ -765,8 +483,6 @@
"sillThickness": 0.03
}
},
"rootNodeIds": [
"site_xs1r72ib2ymzpjus"
],
"rootNodeIds": ["site_xs1r72ib2ymzpjus"],
"collections": {}
}
}
@@ -10,10 +10,7 @@
"ok": true,
"durationMs": 4,
"summary": "building=building_bfqg91ai9ijps9ej, level=level_wyuoxj87czq3v0re (of 1 buildings, 1 levels)",
"nodeIds": [
"building_bfqg91ai9ijps9ej",
"level_wyuoxj87czq3v0re"
]
"nodeIds": ["building_bfqg91ai9ijps9ej", "level_wyuoxj87czq3v0re"]
},
{
"n": 2,
@@ -85,10 +82,7 @@
"ok": true,
"durationMs": 3,
"summary": "furthest: zone_3fyksm10tb0dhn1e <-> zone_u95l1bt35jci3gvu = 7.000m",
"nodeIds": [
"zone_3fyksm10tb0dhn1e",
"zone_u95l1bt35jci3gvu"
]
"nodeIds": ["zone_3fyksm10tb0dhn1e", "zone_u95l1bt35jci3gvu"]
},
{
"n": 8,
@@ -117,9 +111,7 @@
"ok": true,
"durationMs": 2,
"summary": "newLevelId=level_cxvltlqvgqcasiep, cloned=22, valid=true, errors=0",
"nodeIds": [
"level_cxvltlqvgqcasiep"
]
"nodeIds": ["level_cxvltlqvgqcasiep"]
},
{
"n": 12,
@@ -170,4 +162,4 @@
"delta": 3
}
}
}
}
+5 -12
View File
@@ -14,12 +14,12 @@
* bun packages/mcp/test-reports/t3-scenario/run.ts
*/
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
import { mkdirSync, writeFileSync } from 'node:fs'
import { dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
const HERE = dirname(fileURLToPath(import.meta.url))
const SERVER_URL = 'http://localhost:3917/mcp'
@@ -293,15 +293,8 @@ async function main(): Promise<void> {
}
})
const [
bed1SouthId,
bed1EastId,
bed2SouthId,
bed2WestId,
bathSouthId,
bathWestId,
bathEastId,
] = interior ?? []
const [bed1SouthId, bed1EastId, bed2SouthId, bed2WestId, bathSouthId, bathWestId, bathEastId] =
interior ?? []
// ----- Step 4: Set zones -----
const zones = await timed(4, 'set zones', async () => {
@@ -1,18 +1,6 @@
/**
* T5 — MCP resources + prompts test harness.
*
* Connects to the shared MCP HTTP server at http://localhost:3917 (path /mcp),
* exercises the 4 documented resources and 3 prompts, and prints a structured
* pass/fail summary that the REPORT.md can be authored from.
*
* Usage:
* bun packages/mcp/test-reports/t5-resources-prompts/run.ts
*/
import { existsSync } from 'node:fs'
import { dirname, resolve as pathResolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
const HTTP_URL = new URL('http://localhost:3917/mcp')
@@ -104,9 +92,7 @@ async function main(): Promise<void> {
.join(', ')}`,
)
} catch (err) {
console.error(
`[t5] listResources error: ${err instanceof Error ? err.message : String(err)}`,
)
console.error(`[t5] listResources error: ${err instanceof Error ? err.message : String(err)}`)
}
// ---------------- Resource 1: pascal://scene/current ----------------
@@ -116,9 +102,7 @@ async function main(): Promise<void> {
if (!c) {
resourceOutcomes.push(fail('scene/current', 'no contents returned'))
} else if (c.mimeType !== 'application/json') {
resourceOutcomes.push(
fail('scene/current', `wrong mime type: ${String(c.mimeType)}`),
)
resourceOutcomes.push(fail('scene/current', `wrong mime type: ${String(c.mimeType)}`))
} else {
const text = typeof c.text === 'string' ? c.text : ''
let parsed: unknown
@@ -144,10 +128,7 @@ async function main(): Promise<void> {
const nodeCount = Object.keys(obj.nodes as Record<string, unknown>).length
const rootCount = (obj.rootNodeIds as unknown[]).length
resourceOutcomes.push(
ok(
'scene/current',
`application/json, nodes=${nodeCount}, rootNodeIds=${rootCount}`,
),
ok('scene/current', `application/json, nodes=${nodeCount}, rootNodeIds=${rootCount}`),
)
}
}
@@ -172,13 +153,9 @@ async function main(): Promise<void> {
const hasHeading = /^# /m.test(text)
const hasZoneOrLevel = /level/i.test(text) || /zone/i.test(text)
if (!hasHeading) {
resourceOutcomes.push(
fail('scene/current/summary', 'no markdown # heading found'),
)
resourceOutcomes.push(fail('scene/current/summary', 'no markdown # heading found'))
} else if (!hasZoneOrLevel) {
resourceOutcomes.push(
fail('scene/current/summary', 'no level/zone references'),
)
resourceOutcomes.push(fail('scene/current/summary', 'no level/zone references'))
} else {
// Extract a few first lines as preview
const preview = text.split('\n').slice(0, 4).join(' | ')
@@ -192,10 +169,7 @@ async function main(): Promise<void> {
}
} catch (err) {
resourceOutcomes.push(
fail(
'scene/current/summary',
`threw: ${err instanceof Error ? err.message : String(err)}`,
),
fail('scene/current/summary', `threw: ${err instanceof Error ? err.message : String(err)}`),
)
}
@@ -206,15 +180,16 @@ async function main(): Promise<void> {
if (!c) {
resourceOutcomes.push(fail('catalog/items', 'no contents returned'))
} else if (c.mimeType !== 'application/json') {
resourceOutcomes.push(
fail('catalog/items', `wrong mime type: ${String(c.mimeType)}`),
)
resourceOutcomes.push(fail('catalog/items', `wrong mime type: ${String(c.mimeType)}`))
} else {
const text = typeof c.text === 'string' ? c.text : ''
const parsed = JSON.parse(text) as { status?: unknown; items?: unknown }
if (parsed.status !== 'catalog_unavailable') {
resourceOutcomes.push(
fail('catalog/items', `expected status='catalog_unavailable' got ${String(parsed.status)}`),
fail(
'catalog/items',
`expected status='catalog_unavailable' got ${String(parsed.status)}`,
),
)
} else {
resourceOutcomes.push(
@@ -241,8 +216,7 @@ async function main(): Promise<void> {
name: 'find_nodes',
arguments: { type: 'level' },
})
const sc = (findResult as { structuredContent?: { nodes?: unknown[] } })
.structuredContent
const sc = (findResult as { structuredContent?: { nodes?: unknown[] } }).structuredContent
const nodes = Array.isArray(sc?.nodes) ? sc.nodes : []
if (nodes.length > 0) {
const first = nodes[0] as { id?: string }
@@ -252,9 +226,7 @@ async function main(): Promise<void> {
}
console.log(`[t5] discovered levelId = ${String(discoveredLevelId)}`)
} catch (err) {
console.error(
`[t5] find_nodes threw: ${err instanceof Error ? err.message : String(err)}`,
)
console.error(`[t5] find_nodes threw: ${err instanceof Error ? err.message : String(err)}`)
}
if (!discoveredLevelId) {
@@ -281,19 +253,12 @@ async function main(): Promise<void> {
}
if (parsed.error) {
resourceOutcomes.push(
fail(
'constraints/{levelId}',
`error in payload: ${safeStringify(parsed.error)}`,
),
fail('constraints/{levelId}', `error in payload: ${safeStringify(parsed.error)}`),
)
} else if (!Array.isArray(parsed.slabs)) {
resourceOutcomes.push(
fail('constraints/{levelId}', 'missing slabs array'),
)
resourceOutcomes.push(fail('constraints/{levelId}', 'missing slabs array'))
} else if (!Array.isArray(parsed.wallPolygons)) {
resourceOutcomes.push(
fail('constraints/{levelId}', 'missing wallPolygons array'),
)
resourceOutcomes.push(fail('constraints/{levelId}', 'missing wallPolygons array'))
} else {
resourceOutcomes.push(
ok(
@@ -318,15 +283,9 @@ async function main(): Promise<void> {
const list = await client.listPrompts()
listPromptsCount = Array.isArray(list.prompts) ? list.prompts.length : 0
console.log(`[t5] listPrompts count = ${listPromptsCount}`)
console.log(
`[t5] listPrompts names = ${(list.prompts ?? [])
.map((p) => p.name)
.join(', ')}`,
)
console.log(`[t5] listPrompts names = ${(list.prompts ?? []).map((p) => p.name).join(', ')}`)
} catch (err) {
console.error(
`[t5] listPrompts error: ${err instanceof Error ? err.message : String(err)}`,
)
console.error(`[t5] listPrompts error: ${err instanceof Error ? err.message : String(err)}`)
}
// ---------------- Prompt 1: from_brief ----------------
@@ -386,10 +345,7 @@ async function main(): Promise<void> {
}
} catch (err) {
promptOutcomes.push(
fail(
'iterate_on_feedback',
`threw: ${err instanceof Error ? err.message : String(err)}`,
),
fail('iterate_on_feedback', `threw: ${err instanceof Error ? err.message : String(err)}`),
)
}
@@ -406,9 +362,7 @@ async function main(): Promise<void> {
const messages = result.messages ?? []
const userMsgs = messages.filter((m) => m.role === 'user')
if (userMsgs.length === 0) {
promptOutcomes.push(
fail('renovation_from_photos', 'no user messages returned'),
)
promptOutcomes.push(fail('renovation_from_photos', 'no user messages returned'))
} else {
// Look across all message content for the URLs we passed.
const allText = messages