Files
editor/packages/mcp/test-reports/t3-scenario/apartment.json
T
Adrian PerezandClaude Opus 4.7 e8d0b13ff5 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>
2026-04-18 19:29:28 +02:00

489 lines
12 KiB
JSON

{
"nodes": {
"site_xs1r72ib2ymzpjus": {
"object": "node",
"id": "site_xs1r72ib2ymzpjus",
"type": "site",
"parentId": null,
"visible": true,
"metadata": {},
"polygon": {
"type": "polygon",
"points": [[-15, -15], [15, -15], [15, 15], [-15, 15]]
},
"children": [
{
"object": "node",
"id": "building_bfqg91ai9ijps9ej",
"type": "building",
"parentId": null,
"visible": true,
"metadata": {},
"children": ["level_wyuoxj87czq3v0re"],
"position": [0, 0, 0],
"rotation": [0, 0, 0]
}
]
},
"building_bfqg91ai9ijps9ej": {
"object": "node",
"id": "building_bfqg91ai9ijps9ej",
"type": "building",
"parentId": null,
"visible": true,
"metadata": {},
"children": ["level_wyuoxj87czq3v0re"],
"position": [0, 0, 0],
"rotation": [0, 0, 0]
},
"level_wyuoxj87czq3v0re": {
"object": "node",
"id": "level_wyuoxj87czq3v0re",
"type": "level",
"parentId": null,
"visible": true,
"metadata": {},
"children": [
"wall_y87bsrljd2245n51",
"wall_sja6jpda73tlhxwb",
"wall_aegff27krjwgmkmi",
"wall_dcymglyle9ff09ti",
"wall_rl83cc5tnbf4b34j",
"wall_qv53jm9kvl7k6slf",
"wall_8y52fwzco2ep7fb1",
"wall_hrfixeusz7zb7x63",
"wall_1ullk9bm6dw15i9t",
"wall_c4fjjswnk0mprctm",
"wall_p359pffjf3qs59cy",
"zone_3fyksm10tb0dhn1e",
"zone_u95l1bt35jci3gvu",
"zone_r9ma8tvsqt9w1zey",
"zone_l189q61kf9ra2m8t"
],
"level": 0
},
"wall_y87bsrljd2245n51": {
"object": "node",
"id": "wall_y87bsrljd2245n51",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": ["window_47x40mtv2l4ca9p4", "window_n09awmg5m3ct4fvn"],
"thickness": 0.2,
"height": 2.7,
"start": [0, 0],
"end": [10, 0],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_sja6jpda73tlhxwb": {
"object": "node",
"id": "wall_sja6jpda73tlhxwb",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [10, 0],
"end": [10, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_aegff27krjwgmkmi": {
"object": "node",
"id": "wall_aegff27krjwgmkmi",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": ["window_xlta3f3f0cnmbti3"],
"thickness": 0.2,
"height": 2.7,
"start": [10, 8],
"end": [0, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_dcymglyle9ff09ti": {
"object": "node",
"id": "wall_dcymglyle9ff09ti",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [0, 8],
"end": [0, 0],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_rl83cc5tnbf4b34j": {
"object": "node",
"id": "wall_rl83cc5tnbf4b34j",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [0, 5],
"end": [3, 5],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_qv53jm9kvl7k6slf": {
"object": "node",
"id": "wall_qv53jm9kvl7k6slf",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": ["door_cjzja4lt8owg88wg"],
"thickness": 0.2,
"height": 2.7,
"start": [3, 5],
"end": [3, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_8y52fwzco2ep7fb1": {
"object": "node",
"id": "wall_8y52fwzco2ep7fb1",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [7, 5],
"end": [10, 5],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_hrfixeusz7zb7x63": {
"object": "node",
"id": "wall_hrfixeusz7zb7x63",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": ["door_bs7bf0azevq9vd76"],
"thickness": 0.2,
"height": 2.7,
"start": [7, 5],
"end": [7, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_1ullk9bm6dw15i9t": {
"object": "node",
"id": "wall_1ullk9bm6dw15i9t",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": ["door_o8etwqsemfgj5mkj"],
"thickness": 0.2,
"height": 2.7,
"start": [4, 6],
"end": [6, 6],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_c4fjjswnk0mprctm": {
"object": "node",
"id": "wall_c4fjjswnk0mprctm",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [4, 6],
"end": [4, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"wall_p359pffjf3qs59cy": {
"object": "node",
"id": "wall_p359pffjf3qs59cy",
"type": "wall",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"children": [],
"thickness": 0.2,
"height": 2.7,
"start": [6, 6],
"end": [6, 8],
"frontSide": "unknown",
"backSide": "unknown"
},
"zone_3fyksm10tb0dhn1e": {
"object": "node",
"id": "zone_3fyksm10tb0dhn1e",
"type": "zone",
"name": "bedroom-1",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [[0, 5], [3, 5], [3, 8], [0, 8]],
"color": "#3b82f6"
},
"zone_u95l1bt35jci3gvu": {
"object": "node",
"id": "zone_u95l1bt35jci3gvu",
"type": "zone",
"name": "bedroom-2",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [[7, 5], [10, 5], [10, 8], [7, 8]],
"color": "#3b82f6"
},
"zone_r9ma8tvsqt9w1zey": {
"object": "node",
"id": "zone_r9ma8tvsqt9w1zey",
"type": "zone",
"name": "bathroom",
"parentId": "level_wyuoxj87czq3v0re",
"visible": true,
"metadata": {},
"polygon": [[4, 6], [6, 6], [6, 8], [4, 8]],
"color": "#3b82f6"
},
"zone_l189q61kf9ra2m8t": {
"object": "node",
"id": "zone_l189q61kf9ra2m8t",
"type": "zone",
"name": "living-kitchen",
"parentId": "level_wyuoxj87czq3v0re",
"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]
],
"color": "#3b82f6"
},
"door_cjzja4lt8owg88wg": {
"object": "node",
"id": "door_cjzja4lt8owg88wg",
"type": "door",
"parentId": "wall_qv53jm9kvl7k6slf",
"visible": true,
"metadata": {},
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_qv53jm9kvl7k6slf",
"width": 0.9,
"height": 2.1,
"frameThickness": 0.05,
"frameDepth": 0.07,
"threshold": true,
"thresholdHeight": 0.02,
"hingesSide": "left",
"swingDirection": "inward",
"segments": [
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
},
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
}
],
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
},
"door_bs7bf0azevq9vd76": {
"object": "node",
"id": "door_bs7bf0azevq9vd76",
"type": "door",
"parentId": "wall_hrfixeusz7zb7x63",
"visible": true,
"metadata": {},
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_hrfixeusz7zb7x63",
"width": 0.9,
"height": 2.1,
"frameThickness": 0.05,
"frameDepth": 0.07,
"threshold": true,
"thresholdHeight": 0.02,
"hingesSide": "left",
"swingDirection": "inward",
"segments": [
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
},
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
}
],
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
},
"door_o8etwqsemfgj5mkj": {
"object": "node",
"id": "door_o8etwqsemfgj5mkj",
"type": "door",
"parentId": "wall_1ullk9bm6dw15i9t",
"visible": true,
"metadata": {},
"position": [0.5, 1.05, 0],
"rotation": [0, 0, 0],
"wallId": "wall_1ullk9bm6dw15i9t",
"width": 0.9,
"height": 2.1,
"frameThickness": 0.05,
"frameDepth": 0.07,
"threshold": true,
"thresholdHeight": 0.02,
"hingesSide": "left",
"swingDirection": "inward",
"segments": [
{
"type": "panel",
"heightRatio": 0.4,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
},
{
"type": "panel",
"heightRatio": 0.6,
"columnRatios": [1],
"dividerThickness": 0.03,
"panelDepth": 0.01,
"panelInset": 0.04
}
],
"handle": true,
"handleHeight": 1.05,
"handleSide": "right",
"contentPadding": [0.04, 0.04],
"doorCloser": false,
"panicBar": false,
"panicBarHeight": 1
},
"window_47x40mtv2l4ca9p4": {
"object": "node",
"id": "window_47x40mtv2l4ca9p4",
"type": "window",
"parentId": "wall_y87bsrljd2245n51",
"visible": true,
"metadata": {},
"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],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
"sillDepth": 0.08,
"sillThickness": 0.03
},
"window_n09awmg5m3ct4fvn": {
"object": "node",
"id": "window_n09awmg5m3ct4fvn",
"type": "window",
"parentId": "wall_y87bsrljd2245n51",
"visible": true,
"metadata": {},
"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],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
"sillDepth": 0.08,
"sillThickness": 0.03
},
"window_xlta3f3f0cnmbti3": {
"object": "node",
"id": "window_xlta3f3f0cnmbti3",
"type": "window",
"parentId": "wall_aegff27krjwgmkmi",
"visible": true,
"metadata": {},
"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],
"columnDividerThickness": 0.03,
"rowDividerThickness": 0.03,
"sill": true,
"sillDepth": 0.08,
"sillThickness": 0.03
}
},
"rootNodeIds": ["site_xs1r72ib2ymzpjus"],
"collections": {}
}