@pascal-app/mcp 1.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (196) hide show
  1. package/README.md +47 -26
  2. package/dist/bin/pascal-mcp.js +8 -2
  3. package/dist/bridge/scene-bridge.d.ts +41 -6
  4. package/dist/bridge/scene-bridge.d.ts.map +1 -1
  5. package/dist/bridge/scene-bridge.js +230 -45
  6. package/dist/index.d.ts +3 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/operations/scene-operations.d.ts +23 -3
  10. package/dist/operations/scene-operations.d.ts.map +1 -1
  11. package/dist/operations/scene-operations.js +31 -1
  12. package/dist/prompts/from-brief.js +1 -1
  13. package/dist/prompts/iterate-on-feedback.js +1 -1
  14. package/dist/prompts/scene-guidance.js +3 -3
  15. package/dist/resources/agent-guide.d.ts.map +1 -1
  16. package/dist/resources/agent-guide.js +22 -1
  17. package/dist/resources/catalog-items.d.ts +4 -5
  18. package/dist/resources/catalog-items.d.ts.map +1 -1
  19. package/dist/resources/catalog-items.js +7 -9
  20. package/dist/resources/design-schema.d.ts +9 -0
  21. package/dist/resources/design-schema.d.ts.map +1 -0
  22. package/dist/resources/design-schema.js +21 -0
  23. package/dist/resources/index.d.ts +3 -1
  24. package/dist/resources/index.d.ts.map +1 -1
  25. package/dist/resources/index.js +5 -2
  26. package/dist/server.d.ts +19 -0
  27. package/dist/server.d.ts.map +1 -1
  28. package/dist/server.js +14 -6
  29. package/dist/storage/disk-artifact-store.d.ts +9 -0
  30. package/dist/storage/disk-artifact-store.d.ts.map +1 -0
  31. package/dist/storage/disk-artifact-store.js +46 -0
  32. package/dist/storage/index.d.ts +1 -0
  33. package/dist/storage/index.d.ts.map +1 -1
  34. package/dist/storage/index.js +1 -0
  35. package/dist/storage/sqlite-scene-store.d.ts +2 -2
  36. package/dist/storage/sqlite-scene-store.d.ts.map +1 -1
  37. package/dist/storage/sqlite-scene-store.js +4 -3
  38. package/dist/storage/types.d.ts +32 -3
  39. package/dist/storage/types.d.ts.map +1 -1
  40. package/dist/storage/types.js +12 -0
  41. package/dist/tools/add-column.d.ts +5 -0
  42. package/dist/tools/add-column.d.ts.map +1 -0
  43. package/dist/tools/add-column.js +58 -0
  44. package/dist/tools/add-object.d.ts +60 -0
  45. package/dist/tools/add-object.d.ts.map +1 -0
  46. package/dist/tools/add-object.js +156 -0
  47. package/dist/tools/apply-patch.d.ts +3 -0
  48. package/dist/tools/apply-patch.d.ts.map +1 -1
  49. package/dist/tools/apply-patch.js +28 -2
  50. package/dist/tools/asset-catalog.d.ts +9 -5
  51. package/dist/tools/asset-catalog.d.ts.map +1 -1
  52. package/dist/tools/asset-catalog.js +10 -12
  53. package/dist/tools/check-collisions.d.ts +1 -1
  54. package/dist/tools/check-collisions.d.ts.map +1 -1
  55. package/dist/tools/check-collisions.js +1 -2
  56. package/dist/tools/clear-scene.d.ts +9 -0
  57. package/dist/tools/clear-scene.d.ts.map +1 -0
  58. package/dist/tools/clear-scene.js +59 -0
  59. package/dist/tools/collections.d.ts +4 -0
  60. package/dist/tools/collections.d.ts.map +1 -0
  61. package/dist/tools/collections.js +52 -0
  62. package/dist/tools/construction-tools.d.ts +8 -41
  63. package/dist/tools/construction-tools.d.ts.map +1 -1
  64. package/dist/tools/construction-tools.js +120 -230
  65. package/dist/tools/create-unit.d.ts +2 -0
  66. package/dist/tools/create-unit.d.ts.map +1 -1
  67. package/dist/tools/describe-node.d.ts.map +1 -1
  68. package/dist/tools/describe-node.js +8 -6
  69. package/dist/tools/errors.d.ts +11 -0
  70. package/dist/tools/errors.d.ts.map +1 -1
  71. package/dist/tools/errors.js +10 -0
  72. package/dist/tools/find-nodes.d.ts +3 -19
  73. package/dist/tools/find-nodes.d.ts.map +1 -1
  74. package/dist/tools/find-nodes.js +150 -53
  75. package/dist/tools/geometry-script-worker.d.ts +2 -0
  76. package/dist/tools/geometry-script-worker.d.ts.map +1 -0
  77. package/dist/tools/geometry-script-worker.js +21 -0
  78. package/dist/tools/get-scene.d.ts +1 -0
  79. package/dist/tools/get-scene.d.ts.map +1 -1
  80. package/dist/tools/get-scene.js +3 -1
  81. package/dist/tools/honest-patch-guard.d.ts +2 -0
  82. package/dist/tools/honest-patch-guard.d.ts.map +1 -0
  83. package/dist/tools/honest-patch-guard.js +32 -0
  84. package/dist/tools/hosted-services.d.ts +4 -0
  85. package/dist/tools/hosted-services.d.ts.map +1 -0
  86. package/dist/tools/hosted-services.js +30 -0
  87. package/dist/tools/index.d.ts +10 -1
  88. package/dist/tools/index.d.ts.map +1 -1
  89. package/dist/tools/index.js +17 -26
  90. package/dist/tools/live-sync.d.ts +24 -5
  91. package/dist/tools/live-sync.d.ts.map +1 -1
  92. package/dist/tools/live-sync.js +47 -26
  93. package/dist/tools/local-geometry-scripts.d.ts +8 -0
  94. package/dist/tools/local-geometry-scripts.d.ts.map +1 -0
  95. package/dist/tools/local-geometry-scripts.js +67 -0
  96. package/dist/tools/measure.d.ts +6 -0
  97. package/dist/tools/measure.d.ts.map +1 -1
  98. package/dist/tools/measure.js +33 -65
  99. package/dist/tools/measurement.d.ts +1 -36
  100. package/dist/tools/measurement.d.ts.map +1 -1
  101. package/dist/tools/measurement.js +2 -94
  102. package/dist/tools/node-world-point.d.ts +37 -0
  103. package/dist/tools/node-world-point.d.ts.map +1 -0
  104. package/dist/tools/node-world-point.js +457 -0
  105. package/dist/tools/normalize-schema-dialect.d.ts.map +1 -1
  106. package/dist/tools/normalize-schema-dialect.js +20 -0
  107. package/dist/tools/patch-guards.d.ts +58 -0
  108. package/dist/tools/patch-guards.d.ts.map +1 -0
  109. package/dist/tools/patch-guards.js +253 -0
  110. package/dist/tools/photo-to-scene/photo-to-scene.d.ts.map +1 -1
  111. package/dist/tools/photo-to-scene/photo-to-scene.js +3 -2
  112. package/dist/tools/place-design.d.ts +34 -0
  113. package/dist/tools/place-design.d.ts.map +1 -0
  114. package/dist/tools/place-design.js +91 -0
  115. package/dist/tools/redo.d.ts +2 -0
  116. package/dist/tools/redo.d.ts.map +1 -1
  117. package/dist/tools/redo.js +2 -2
  118. package/dist/tools/room-tools.d.ts +50 -113
  119. package/dist/tools/room-tools.d.ts.map +1 -1
  120. package/dist/tools/room-tools.js +129 -565
  121. package/dist/tools/scene-lifecycle/create-project.d.ts.map +1 -1
  122. package/dist/tools/scene-lifecycle/create-project.js +26 -1
  123. package/dist/tools/scene-lifecycle/delete-scene.d.ts +2 -0
  124. package/dist/tools/scene-lifecycle/delete-scene.d.ts.map +1 -1
  125. package/dist/tools/scene-lifecycle/delete-scene.js +4 -3
  126. package/dist/tools/scene-lifecycle/save-scene.d.ts +1 -0
  127. package/dist/tools/scene-lifecycle/save-scene.d.ts.map +1 -1
  128. package/dist/tools/scene-lifecycle/save-scene.js +46 -6
  129. package/dist/tools/scene-lifecycle/test-utils.d.ts +2 -2
  130. package/dist/tools/scene-lifecycle/test-utils.d.ts.map +1 -1
  131. package/dist/tools/scene-lifecycle/test-utils.js +2 -1
  132. package/dist/tools/set-unit-members.d.ts +2 -0
  133. package/dist/tools/set-unit-members.d.ts.map +1 -1
  134. package/dist/tools/set-zone.d.ts +2 -0
  135. package/dist/tools/set-zone.d.ts.map +1 -1
  136. package/dist/tools/set-zone.js +2 -2
  137. package/dist/tools/shared-tools.d.ts +30 -0
  138. package/dist/tools/shared-tools.d.ts.map +1 -0
  139. package/dist/tools/shared-tools.js +361 -0
  140. package/dist/tools/structure-tools.d.ts +57 -0
  141. package/dist/tools/structure-tools.d.ts.map +1 -0
  142. package/dist/tools/structure-tools.js +57 -0
  143. package/dist/tools/undo.d.ts +2 -0
  144. package/dist/tools/undo.d.ts.map +1 -1
  145. package/dist/tools/undo.js +2 -2
  146. package/dist/tools/validate-design.d.ts +136 -0
  147. package/dist/tools/validate-design.d.ts.map +1 -0
  148. package/dist/tools/validate-design.js +28 -0
  149. package/dist/tools/validate-scene.d.ts +5 -0
  150. package/dist/tools/validate-scene.d.ts.map +1 -1
  151. package/dist/tools/validate-scene.js +8 -6
  152. package/dist/tools/variants/mutations.js +1 -1
  153. package/dist/tools/view-scene.d.ts +38 -0
  154. package/dist/tools/view-scene.d.ts.map +1 -0
  155. package/dist/tools/view-scene.js +63 -0
  156. package/dist/version.d.ts +5 -0
  157. package/dist/version.d.ts.map +1 -1
  158. package/dist/version.js +17 -3
  159. package/examples/embed-in-agent.ts +1 -1
  160. package/examples/generate-apartment.md +9 -11
  161. package/examples/photo-to-scene.md +1 -1
  162. package/examples/renovate-from-photos.md +3 -4
  163. package/package.json +12 -6
  164. package/dist/tools/create-level.d.ts +0 -21
  165. package/dist/tools/create-level.d.ts.map +0 -1
  166. package/dist/tools/create-level.js +0 -63
  167. package/dist/tools/create-wall.d.ts +0 -22
  168. package/dist/tools/create-wall.d.ts.map +0 -1
  169. package/dist/tools/create-wall.js +0 -57
  170. package/dist/tools/cut-opening.d.ts +0 -25
  171. package/dist/tools/cut-opening.d.ts.map +0 -1
  172. package/dist/tools/cut-opening.js +0 -61
  173. package/dist/tools/delete-node.d.ts +0 -19
  174. package/dist/tools/delete-node.d.ts.map +0 -1
  175. package/dist/tools/delete-node.js +0 -40
  176. package/dist/tools/door-clearance.d.ts +0 -135
  177. package/dist/tools/door-clearance.d.ts.map +0 -1
  178. package/dist/tools/door-clearance.js +0 -334
  179. package/dist/tools/duplicate-level.d.ts +0 -19
  180. package/dist/tools/duplicate-level.d.ts.map +0 -1
  181. package/dist/tools/duplicate-level.js +0 -60
  182. package/dist/tools/geometry.d.ts +0 -24
  183. package/dist/tools/geometry.d.ts.map +0 -1
  184. package/dist/tools/geometry.js +0 -94
  185. package/dist/tools/get-node.d.ts +0 -11
  186. package/dist/tools/get-node.d.ts.map +0 -1
  187. package/dist/tools/get-node.js +0 -29
  188. package/dist/tools/layout-clearance.d.ts +0 -105
  189. package/dist/tools/layout-clearance.d.ts.map +0 -1
  190. package/dist/tools/layout-clearance.js +0 -190
  191. package/dist/tools/place-item.d.ts +0 -22
  192. package/dist/tools/place-item.d.ts.map +0 -1
  193. package/dist/tools/place-item.js +0 -89
  194. package/dist/tools/scene-query.d.ts +0 -62
  195. package/dist/tools/scene-query.d.ts.map +0 -1
  196. package/dist/tools/scene-query.js +0 -670
@@ -0,0 +1,457 @@
1
+ import { computeSegmentTransforms, getFenceCenterlineFrameAt, getLevelElevations, getWallCurveFrameAt, measurementCentroid, nodeRegistry, } from '@pascal-app/core';
2
+ import { composeFrames, frame, IDENTITY_FRAME, nodeLevelFrame, transformPoint, } from '@pascal-app/core/procedural-items';
3
+ import { AnyNode, getRoofSegmentSurfaceY, nodeKindOf, } from '@pascal-app/core/schema';
4
+ function isVec3(value) {
5
+ return (Array.isArray(value) &&
6
+ value.length >= 3 &&
7
+ value.slice(0, 3).every((v) => typeof v === 'number' && Number.isFinite(v)));
8
+ }
9
+ function boundsCentre(points) {
10
+ if (points.length === 0)
11
+ return null;
12
+ const min = [Infinity, Infinity, Infinity];
13
+ const max = [-Infinity, -Infinity, -Infinity];
14
+ for (const p of points) {
15
+ for (let i = 0; i < 3; i++) {
16
+ min[i] = Math.min(min[i], p[i]);
17
+ max[i] = Math.max(max[i], p[i]);
18
+ }
19
+ }
20
+ return [(min[0] + max[0]) / 2, (min[1] + max[1]) / 2, (min[2] + max[2]) / 2];
21
+ }
22
+ /** Area centroid of a plan polygon at height `y`; the vertex average when degenerate. */
23
+ function polygonCentre(polygon, y) {
24
+ if (!Array.isArray(polygon) || polygon.length === 0)
25
+ return null;
26
+ const points = polygon.map(([x, z]) => [x, y, z]);
27
+ const centroid = measurementCentroid(points);
28
+ if (centroid)
29
+ return centroid;
30
+ let cx = 0;
31
+ let cz = 0;
32
+ for (const [x, , z] of points) {
33
+ cx += x;
34
+ cz += z;
35
+ }
36
+ return [cx / points.length, y, cz / points.length];
37
+ }
38
+ function geometryContext(node, nodes) {
39
+ const lookup = (id) => nodes[id];
40
+ const childIds = (n) => 'children' in n && Array.isArray(n.children) ? n.children : [];
41
+ const parent = node.parentId ? (lookup(node.parentId) ?? null) : null;
42
+ return {
43
+ resolve: ((id) => lookup(id)),
44
+ parent,
45
+ children: childIds(node)
46
+ .map(lookup)
47
+ .filter((child) => child !== undefined),
48
+ siblings: parent
49
+ ? childIds(parent)
50
+ .map(lookup)
51
+ .filter((s) => s !== undefined && s.type === node.type)
52
+ : [],
53
+ };
54
+ }
55
+ /** The point at parameter `t` (default 0.5) along a feature, by arc length. */
56
+ function featurePoint(feature, reference) {
57
+ const t = typeof reference.parameters?.t === 'number' ? reference.parameters.t : 0.5;
58
+ const geometry = feature.geometry;
59
+ if (geometry.kind === 'point')
60
+ return geometry.point;
61
+ const points = geometry.kind === 'segment' ? [geometry.start, geometry.end] : geometry.points;
62
+ const closed = geometry.kind === 'polygon' || (geometry.kind === 'path' && geometry.closed === true);
63
+ if (points.length === 0)
64
+ return null;
65
+ const count = closed ? points.length : points.length - 1;
66
+ const lengths = Array.from({ length: Math.max(0, count) }, (_, i) => {
67
+ const a = points[i];
68
+ const b = points[(i + 1) % points.length];
69
+ return Math.hypot(b[0] - a[0], b[1] - a[1], b[2] - a[2]);
70
+ });
71
+ const total = lengths.reduce((sum, length) => sum + length, 0);
72
+ if (total <= 1e-9)
73
+ return points[0];
74
+ let remaining = Math.max(0, Math.min(1, t)) * total;
75
+ for (let i = 0; i < count; i++) {
76
+ const length = lengths[i];
77
+ if (remaining <= length || i === count - 1) {
78
+ const a = points[i];
79
+ const b = points[(i + 1) % points.length];
80
+ const u = length <= 1e-9 ? 0 : remaining / length;
81
+ return [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u, a[2] + (b[2] - a[2]) * u];
82
+ }
83
+ remaining -= length;
84
+ }
85
+ return points[points.length - 1];
86
+ }
87
+ /**
88
+ * A measurement or dimension anchor as the viewer resolves it: a tuple is a
89
+ * free point; a feature anchor follows its referenced node through that kind's
90
+ * registered measurement contribution, and falls back to its stored point when
91
+ * the node or the contribution is missing (`fallback: true`).
92
+ */
93
+ function resolveAnchor(anchor, nodes) {
94
+ if (isVec3(anchor))
95
+ return { point: anchor, fallback: false };
96
+ const feature = anchor;
97
+ if (!(feature?.reference && isVec3(feature.fallback)))
98
+ return null;
99
+ const reference = feature.reference;
100
+ const referenced = nodes[reference.nodeId];
101
+ const contribution = referenced ? nodeRegistry.get(referenced.type)?.measurement : undefined;
102
+ if (referenced && contribution) {
103
+ const context = geometryContext(referenced, nodes);
104
+ const resolved = contribution.resolve?.(referenced, context, reference) ??
105
+ contribution.features(referenced, context).find((c) => c.id === reference.featureId);
106
+ const point = resolved ? featurePoint(resolved, reference) : null;
107
+ if (point)
108
+ return { point, fallback: false };
109
+ }
110
+ return { point: feature.fallback, fallback: true };
111
+ }
112
+ /** The anchors a measurement or construction dimension draws from. */
113
+ function anchorSet(node) {
114
+ if (node.type === 'construction-dimension')
115
+ return { anchors: node.anchors, shape: 'points' };
116
+ if (node.type !== 'measurement')
117
+ return null;
118
+ const m = node.measurement;
119
+ if (m.kind === 'distance' || m.kind === 'angle')
120
+ return { anchors: m.points ?? [], shape: 'points' };
121
+ if (m.kind === 'volume' && isVec3(m.extrusion)) {
122
+ return { anchors: m.base ?? [], shape: 'volume', extrusion: m.extrusion };
123
+ }
124
+ return { anchors: m.base ?? [], shape: 'area' };
125
+ }
126
+ /** Centre of a measurement or dimension: its points' centre, a base's area centroid, a prism's centre. */
127
+ function anchorCentre(set, nodes) {
128
+ const points = set.anchors
129
+ .map((anchor) => resolveAnchor(anchor, nodes)?.point)
130
+ .filter((p) => p !== undefined);
131
+ if (set.shape === 'points')
132
+ return boundsCentre(points);
133
+ const base = measurementCentroid(points) ?? boundsCentre(points);
134
+ if (!base || set.shape === 'area' || !set.extrusion)
135
+ return base;
136
+ return [
137
+ base[0] + set.extrusion[0] / 2,
138
+ base[1] + set.extrusion[1] / 2,
139
+ base[2] + set.extrusion[2] / 2,
140
+ ];
141
+ }
142
+ /**
143
+ * Centre of the geometry a node stores in its own frame, before its transform:
144
+ * mesh vertices, a segment, a path, a polygon or measurement anchors.
145
+ */
146
+ function ownGeometryCentre(node, nodes) {
147
+ const n = node;
148
+ if (node.type === 'block') {
149
+ return boundsCentre(node.topology.vertices.map((v) => v.position));
150
+ }
151
+ if (node.type === 'imported-mesh') {
152
+ const points = [];
153
+ for (const primitive of node.primitives) {
154
+ for (let i = 0; i + 2 < primitive.positions.length; i += 3) {
155
+ points.push([
156
+ primitive.positions[i],
157
+ primitive.positions[i + 1],
158
+ primitive.positions[i + 2],
159
+ ]);
160
+ }
161
+ }
162
+ return boundsCentre(points);
163
+ }
164
+ if (node.type === 'slab')
165
+ return polygonCentre(node.polygon, node.elevation ?? 0);
166
+ if (node.type === 'site')
167
+ return polygonCentre(node.polygon?.points, 0);
168
+ if (Array.isArray(n.polygon))
169
+ return polygonCentre(n.polygon, 0);
170
+ if (Array.isArray(n.start) && Array.isArray(n.end)) {
171
+ const [x1, z1] = n.start;
172
+ const [x2, z2] = n.end;
173
+ return [(x1 + x2) / 2, 0, (z1 + z2) / 2];
174
+ }
175
+ if (Array.isArray(n.path))
176
+ return boundsCentre(n.path.filter(isVec3));
177
+ const anchors = anchorSet(node);
178
+ return anchors ? anchorCentre(anchors, nodes) : null;
179
+ }
180
+ /**
181
+ * Stair segments render at the attachment chain, not at their stored
182
+ * `position`, so resolve them from the stair frame like the stair system does.
183
+ */
184
+ function stairSegmentFrame(node, nodes) {
185
+ const stair = node.parentId ? nodes[node.parentId] : undefined;
186
+ if (stair?.type !== 'stair')
187
+ return null;
188
+ const segments = (stair.children ?? [])
189
+ .map((id) => nodes[id])
190
+ .filter((n) => n?.type === 'stair-segment');
191
+ const index = segments.findIndex((segment) => segment.id === node.id);
192
+ const transform = computeSegmentTransforms(segments)[index];
193
+ if (!transform)
194
+ return null;
195
+ return composeFrames(nodeLevelFrame(stair.id, nodes), frame(transform.position, [0, transform.rotation, 0]));
196
+ }
197
+ /**
198
+ * Built-in kinds the viewer lifts onto the slab under them through the
199
+ * `floorPlaced` capability of their registered definition, and whose footprint
200
+ * core cannot derive without it (core derives item, shelf and procedural-item
201
+ * footprints from the node itself). Headless MCP does not load
202
+ * `@pascal-app/nodes`, so their lift is unresolved and flagged, not guessed. A
203
+ * test keeps this list in step with the definitions.
204
+ */
205
+ export const HEADLESS_UNRESOLVED_FLOOR_LIFT_KINDS = [
206
+ 'block',
207
+ 'cabinet',
208
+ 'cabinet-module',
209
+ 'column',
210
+ 'duct-terminal',
211
+ 'hvac-equipment',
212
+ 'spawn',
213
+ 'stair',
214
+ ];
215
+ const UNRESOLVED_FLOOR_LIFT = new Set(HEADLESS_UNRESOLVED_FLOOR_LIFT_KINDS);
216
+ const CORE_KINDS = new Set(AnyNode.options.map(nodeKindOf));
217
+ const FLOOR_LIFT_UNRESOLVED = 'floor-lift-unresolved-headless: the viewer lifts this onto the slab or ground under it through a node definition this runtime has not registered; the point is on the level plane';
218
+ /**
219
+ * The node on a level that carries `node`'s floor lift (itself or the host it
220
+ * sits on), when that lift is resolved by a definition this runtime lacks: a
221
+ * built-in floor-placed kind or a plugin kind, unregistered.
222
+ */
223
+ function unresolvedFloorLiftHost(node, nodes) {
224
+ const seen = new Set();
225
+ let current = node;
226
+ while (current && !seen.has(current.id)) {
227
+ seen.add(current.id);
228
+ const parent = current.parentId ? nodes[current.parentId] : undefined;
229
+ if (parent?.type === 'level') {
230
+ if (nodeRegistry.get(current.type))
231
+ return undefined;
232
+ const unresolved = UNRESOLVED_FLOOR_LIFT.has(current.type) ||
233
+ (!CORE_KINDS.has(current.type) && 'position' in current);
234
+ return unresolved ? current : undefined;
235
+ }
236
+ current = parent;
237
+ }
238
+ return undefined;
239
+ }
240
+ /**
241
+ * Why a node's point is not where its renderer draws it: kinds whose renderer
242
+ * derives the pose from data this resolver does not model, and floor lifts
243
+ * owned by definitions not registered here. The point is then the stored
244
+ * placement in its host's frame.
245
+ */
246
+ function approximationReason(node, nodes) {
247
+ if (unresolvedFloorLiftHost(node, nodes))
248
+ return FLOOR_LIFT_UNRESOLVED;
249
+ const anchors = anchorSet(node);
250
+ if (anchors?.anchors.some((anchor) => resolveAnchor(anchor, nodes)?.fallback)) {
251
+ return 'anchor-fallback: an anchor follows a referenced node whose measurement features are not registered here (or the node is gone); its stored fallback point is used';
252
+ }
253
+ const parent = node.parentId ? nodes[node.parentId] : undefined;
254
+ switch (node.type) {
255
+ case 'downspout':
256
+ return 'rendered at its gutter outlet below the eave; the point is its gutter or host placement';
257
+ case 'gutter':
258
+ return 'rendered at the roof segment eave height, not its stored height';
259
+ case 'ridge-vent':
260
+ return 'rendered along its roof segment ridge; the point is its stored placement';
261
+ case 'fence':
262
+ return 'floor-lift-unresolved-headless: the fence renderer lifts it onto its host slab or the ground; the point is on the level plane';
263
+ case 'solar-panel':
264
+ case 'skylight':
265
+ return parent?.type === 'roof-segment'
266
+ ? 'roof-surface: the viewer seats it on the finished outer roof surface (deck and shingles); the point is on the structural roof surface'
267
+ : undefined;
268
+ case 'door':
269
+ case 'window':
270
+ if (node.roofSegmentId || (node.type === 'window' && node.dormerId)) {
271
+ return 'hosted on a roof-segment or dormer wall face; the point ignores the face frame';
272
+ }
273
+ return undefined;
274
+ case 'lean-to-extension':
275
+ return parent?.type === 'wall'
276
+ ? 'placed along its host wall centreline; the point uses a straight wall frame'
277
+ : undefined;
278
+ case 'item':
279
+ return node.asset.attachTo === 'wall-side'
280
+ ? 'rendered on the wall face (± half the wall thickness); the point is on the wall centreline'
281
+ : undefined;
282
+ default:
283
+ return undefined;
284
+ }
285
+ }
286
+ /**
287
+ * The node's reference point in its level's frame (or in world space when it
288
+ * has no level ancestor), or null for a container with no geometry of its own.
289
+ */
290
+ function levelLocalPoint(node, nodes) {
291
+ if (node.type === 'wall') {
292
+ const { point } = getWallCurveFrameAt(node, 0.5);
293
+ return [point.x, nodeLevelFrame(node.id, nodes).position[1], point.y];
294
+ }
295
+ if (node.type === 'fence') {
296
+ // Arc or spline midpoint on the level plane; its lift is flagged.
297
+ const { point } = getFenceCenterlineFrameAt(node, 0.5);
298
+ return [point.x, 0, point.y];
299
+ }
300
+ if (node.type === 'stair-segment') {
301
+ const segmentFrame = stairSegmentFrame(node, nodes);
302
+ if (segmentFrame)
303
+ return segmentFrame.position;
304
+ }
305
+ const parent = node.parentId ? nodes[node.parentId] : undefined;
306
+ if (node.type === 'downspout' && node.gutterId && nodes[node.gutterId]?.type === 'gutter') {
307
+ return levelLocalPoint(nodes[node.gutterId], nodes);
308
+ }
309
+ if (node.type === 'chimney' && parent?.type === 'roof-segment') {
310
+ // The renderer ignores the stored Y and stands the chimney on the segment base.
311
+ const [x, , z] = node.position;
312
+ return transformPoint(nodeLevelFrame(parent.id, nodes), [x, 0, z]);
313
+ }
314
+ if ((node.type === 'solar-panel' || node.type === 'skylight') &&
315
+ parent?.type === 'roof-segment') {
316
+ // Both renderers ignore the stored Y and sit on the roof surface; the
317
+ // finished surface (deck and shingles) is the viewer's, so this is flagged.
318
+ const [x, , z] = node.position;
319
+ return transformPoint(nodeLevelFrame(parent.id, nodes), [
320
+ x,
321
+ getRoofSegmentSurfaceY(parent, x, z),
322
+ z,
323
+ ]);
324
+ }
325
+ const own = nodeLevelFrame(node.id, nodes);
326
+ const geometry = ownGeometryCentre(node, nodes);
327
+ if (geometry)
328
+ return transformPoint(own, geometry);
329
+ if (isVec3(node.position))
330
+ return own.position;
331
+ return null;
332
+ }
333
+ function nearestLevelId(node, nodes) {
334
+ const seen = new Set();
335
+ let current = node;
336
+ while (current && !seen.has(current.id)) {
337
+ if (current.type === 'level')
338
+ return current.id;
339
+ seen.add(current.id);
340
+ current = current.parentId ? nodes[current.parentId] : undefined;
341
+ }
342
+ return null;
343
+ }
344
+ function hostOf(id, nodes) {
345
+ const node = nodes[id];
346
+ if (node?.parentId)
347
+ return nodes[node.parentId];
348
+ // The default scene links site → building → level through `children` only.
349
+ return Object.values(nodes).find((candidate) => 'children' in candidate &&
350
+ Array.isArray(candidate.children) &&
351
+ candidate.children.some((child) => child === id || child?.id === id));
352
+ }
353
+ /** A level's frame in world space: its building's transform, then its stacked base Y. */
354
+ function levelWorldFrame(levelId, nodes) {
355
+ const baseY = getLevelElevations(nodes).get(levelId)?.baseY ?? 0;
356
+ const host = hostOf(levelId, nodes);
357
+ const hostFrame = host ? nodeLevelFrame(host.id, nodes) : IDENTITY_FRAME;
358
+ return composeFrames(hostFrame, frame([0, baseY, 0]));
359
+ }
360
+ function childIdsOf(id, nodes) {
361
+ const node = nodes[id];
362
+ const ids = new Set();
363
+ if (node && 'children' in node && Array.isArray(node.children)) {
364
+ for (const child of node.children) {
365
+ const childId = typeof child === 'string' ? child : child?.id;
366
+ if (typeof childId === 'string')
367
+ ids.add(childId);
368
+ }
369
+ }
370
+ if (node?.type === 'unit')
371
+ for (const member of node.members)
372
+ ids.add(member);
373
+ for (const candidate of Object.values(nodes)) {
374
+ if (candidate.parentId === id)
375
+ ids.add(candidate.id);
376
+ }
377
+ return [...ids].filter((childId) => nodes[childId]);
378
+ }
379
+ export function resolveNodeWorldPoint(id, nodes) {
380
+ const linked = withParentIds(nodes);
381
+ const point = worldPoint(id, linked, 0);
382
+ if (!point)
383
+ return null;
384
+ const approximate = approximationReason(linked[id], linked);
385
+ return approximate ? { point, approximate } : { point };
386
+ }
387
+ /**
388
+ * `nodes` with every missing `parentId` filled from the parent's `children`
389
+ * array. The frame walk follows `parentId`, but scenes may link a subtree only
390
+ * through `children` (the default site → building → level assembly does), as
391
+ * SceneBridge's ancestry allows. Returns `nodes` itself when nothing is missing.
392
+ */
393
+ function withParentIds(nodes) {
394
+ const parentOf = new Map();
395
+ for (const node of Object.values(nodes)) {
396
+ if (!('children' in node) || !Array.isArray(node.children))
397
+ continue;
398
+ for (const child of node.children) {
399
+ const childId = typeof child === 'string' ? child : child?.id;
400
+ if (typeof childId === 'string' && !parentOf.has(childId))
401
+ parentOf.set(childId, node.id);
402
+ }
403
+ }
404
+ let linked = null;
405
+ for (const node of Object.values(nodes)) {
406
+ const parentId = parentOf.get(node.id);
407
+ if (node.parentId || !parentId)
408
+ continue;
409
+ linked ??= { ...nodes };
410
+ linked[node.id] = { ...node, parentId };
411
+ }
412
+ return linked ?? nodes;
413
+ }
414
+ function worldPoint(id, nodes, depth) {
415
+ const node = nodes[id];
416
+ if (!node || depth > 32)
417
+ return null;
418
+ if (node.type === 'level') {
419
+ return transformPoint(levelWorldFrame(node.id, nodes), levelPlanCentre(node.id, nodes));
420
+ }
421
+ const levelId = nearestLevelId(node, nodes);
422
+ const toWorld = (point) => levelId ? transformPoint(levelWorldFrame(levelId, nodes), point) : point;
423
+ const local = levelLocalPoint(node, nodes);
424
+ if (local)
425
+ return toWorld(local);
426
+ const points = childIdsOf(id, nodes)
427
+ .map((childId) => worldPoint(childId, nodes, depth + 1))
428
+ .filter((p) => p !== null);
429
+ if (points.length > 0)
430
+ return boundsCentre(points);
431
+ return toWorld(nodeLevelFrame(node.id, nodes).position);
432
+ }
433
+ /**
434
+ * A level's plan centre on its base plane: the centre of its direct children's
435
+ * level-local points. Read from their stored geometry and own transform only,
436
+ * because resolving every wall's slab support makes a large level take seconds.
437
+ */
438
+ function levelPlanCentre(levelId, nodes) {
439
+ const points = [];
440
+ for (const childId of childIdsOf(levelId, nodes)) {
441
+ const child = nodes[childId];
442
+ const transform = child;
443
+ const position = isVec3(transform.position) ? transform.position : [0, 0, 0];
444
+ const rotation = typeof transform.rotation === 'number'
445
+ ? [0, transform.rotation, 0]
446
+ : isVec3(transform.rotation)
447
+ ? transform.rotation
448
+ : [0, 0, 0];
449
+ const geometry = ownGeometryCentre(child, nodes);
450
+ if (geometry)
451
+ points.push(transformPoint(frame(position, rotation), geometry));
452
+ else if (isVec3(transform.position))
453
+ points.push(position);
454
+ }
455
+ const centre = boundsCentre(points);
456
+ return centre ? [centre[0], 0, centre[2]] : [0, 0, 0];
457
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"normalize-schema-dialect.d.ts","sourceRoot":"","sources":["../../src/tools/normalize-schema-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AAuBxE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAWlE"}
1
+ {"version":3,"file":"normalize-schema-dialect.d.ts","sourceRoot":"","sources":["../../src/tools/normalize-schema-dialect.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAA;AA0CxE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAWlE"}
@@ -1,5 +1,23 @@
1
1
  import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
2
2
  const DIALECT_2020_12 = 'https://json-schema.org/draft/2020-12/schema';
3
+ /**
4
+ * A tuple of one item schema, `items: [A, A]` (draft-07), as 2020-12 writes an array of that length:
5
+ * `items: A` with minItems and maxItems. Clients reading 2020-12 refuse the list form, and Claude
6
+ * Code left out 9 tools over it (2026-10-03). The tools still validate with their own tuples.
7
+ */
8
+ function untuple(record, key) {
9
+ const list = record[key];
10
+ if (!(Array.isArray(list) && list.length > 0))
11
+ return;
12
+ const first = JSON.stringify(list[0]);
13
+ if (!list.every((entry) => JSON.stringify(entry) === first))
14
+ return;
15
+ delete record.prefixItems;
16
+ delete record.additionalItems;
17
+ record.items = list[0];
18
+ record.minItems ??= list.length;
19
+ record.maxItems ??= list.length;
20
+ }
3
21
  function retargetDialect(value) {
4
22
  if (Array.isArray(value)) {
5
23
  for (const item of value)
@@ -10,6 +28,8 @@ function retargetDialect(value) {
10
28
  const record = value;
11
29
  if (typeof record.$schema === 'string')
12
30
  record.$schema = DIALECT_2020_12;
31
+ untuple(record, 'items');
32
+ untuple(record, 'prefixItems');
13
33
  for (const key of Object.keys(record))
14
34
  retargetDialect(record[key]);
15
35
  }
@@ -0,0 +1,58 @@
1
+ import { type NodeDeletionPlan, type NodeDeletionScene } from '@pascal-app/core';
2
+ import { AnyNode, type AnyNodeId } from '@pascal-app/core/schema';
3
+ import type { Patch } from '../bridge/scene-bridge.js';
4
+ export type PatchRefusalCode = 'node_exists' | 'identity_change' | 'immutable_field' | 'invalid_parent' | 'invalid_update' | 'regenerated_default' | 'scripted_field' | 'unknown_field' | 'unknown_material' | 'field_required' | 'shadowed_field';
5
+ /**
6
+ * A check apply_patch runs over the whole patch before applying it, registered by the module that
7
+ * owns what it protects, so a feature brings its own check (the honest-update check is the
8
+ * first). It may rewrite an update's data (the cleaned data is what is written) and
9
+ * refuses by throwing: a PatchRefusedError comes back as `{ code, patchIndex, id, message }`, an
10
+ * McpError as it is. Lower `order` runs first.
11
+ */
12
+ export type PatchGuard = {
13
+ name: string;
14
+ order: number;
15
+ run(patches: Patch[], nodes: Readonly<Record<string, AnyNode>>): void;
16
+ };
17
+ /** Runs a guard on every apply_patch from now on; registering a guard again replaces it. */
18
+ export declare function registerPatchGuard(guard: PatchGuard): void;
19
+ export declare function runPatchGuards(patches: Patch[], nodes: Readonly<Record<string, AnyNode>>): void;
20
+ /**
21
+ * A patch op refused because it would break node identity, the hierarchy or
22
+ * the node's schema. apply_patch returns it as a tool error whose text is
23
+ * JSON `{ code, patchIndex, id, message }`.
24
+ */
25
+ export declare class PatchRefusedError extends Error {
26
+ readonly code: PatchRefusalCode;
27
+ readonly patchIndex: number;
28
+ readonly nodeId: string;
29
+ constructor(code: PatchRefusalCode, patchIndex: number, nodeId: string, detail: string);
30
+ }
31
+ /**
32
+ * Dry-runs a patch against the scene as each op leaves it and refuses:
33
+ * - a create whose id is already present (`node_exists`): a silent replace
34
+ * orphans the old node's children;
35
+ * - an update that changes `id`, `type`, `object` or `children`
36
+ * (`immutable_field`); restating the current value passes;
37
+ * - an update whose merged node has schema issues the node did not have
38
+ * before (`invalid_update`); kinds without a schema in this runtime pass;
39
+ * - an update writing a material preset the catalog does not know
40
+ * (`invalid_update`; a create is rejected like other invalid creates): it
41
+ * would render as the default finish;
42
+ * - an update of what a node's script owns, its `source` or the size it
43
+ * built (`scripted_field`): only a rebuild keeps them and the geometry in step;
44
+ * - any op on a node an earlier delete in the patch removed, or on a default
45
+ * gutter or downspout that delete regenerates, and any update of the roof
46
+ * segment holding them (`regenerated_default`).
47
+ *
48
+ * Deletes run through the bridge's own deletion preview (`planDeletion`; the
49
+ * local bridge uses core's store planner), one call per run of consecutive
50
+ * deletes as the bridge batches them, so cascades and wall merges are seen as
51
+ * that bridge applies them. A bridge without a preview gets a stricter rule:
52
+ * a create may not reuse any id present at the start of the patch.
53
+ * An update of a roof segment unsettles the default gutters its refresh may
54
+ * regenerate, as a delete does.
55
+ * Lives in the tool layer so every bridge behind `apply_patch` inherits it.
56
+ */
57
+ export declare function assertPatchKeepsIdentity(patches: readonly Patch[], nodes: Readonly<Record<string, AnyNode>>, rootNodeIds: readonly AnyNodeId[], planDeletion?: (scene: NodeDeletionScene, ids: AnyNodeId[]) => NodeDeletionPlan): void;
58
+ //# sourceMappingURL=patch-guards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"patch-guards.d.ts","sourceRoot":"","sources":["../../src/tools/patch-guards.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EAKvB,MAAM,kBAAkB,CAAA;AAKzB,OAAO,EAAE,OAAO,EAAE,KAAK,SAAS,EAAyB,MAAM,yBAAyB,CAAA;AACxF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wBAAwB,CAAA;AAEnD,MAAM,MAAM,gBAAgB,GACxB,aAAa,GACb,iBAAiB,GACjB,iBAAiB,GACjB,gBAAgB,GAChB,gBAAgB,GAChB,qBAAqB,GACrB,gBAAgB,GAEhB,eAAe,GACf,kBAAkB,GAClB,gBAAgB,GAChB,gBAAgB,CAAA;AAEpB;;;;;;GAMG;AACH,MAAM,MAAM,UAAU,GAAG;IACvB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,CAAA;IACb,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,IAAI,CAAA;CACtE,CAAA;AAID,4FAA4F;AAC5F,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,UAAU,QAKnD;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,KAAK,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAExF;AAED;;;;GAIG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAExC,QAAQ,CAAC,IAAI,EAAE,gBAAgB;IAC/B,QAAQ,CAAC,UAAU,EAAE,MAAM;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM;gBAFd,IAAI,EAAE,gBAAgB,EACtB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACvB,MAAM,EAAE,MAAM;CAKjB;AAkDD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,SAAS,KAAK,EAAE,EACzB,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACxC,WAAW,EAAE,SAAS,SAAS,EAAE,EACjC,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,EAAE,GAAG,EAAE,SAAS,EAAE,KAAK,gBAAgB,GAC9E,IAAI,CA+KN"}