@snaptrude/plugin-core 0.9.16 → 0.9.18-edgesetbacks-20261021.1

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 (221) hide show
  1. package/AGENTS.md +231 -231
  2. package/CHANGELOG.md +408 -402
  3. package/CLAUDE.md +11 -11
  4. package/api-manifest.json +1510 -701
  5. package/dist/api/analysis/daylight.d.ts +5 -5
  6. package/dist/api/analysis/solar.d.ts +2 -2
  7. package/dist/api/core/camera/index.d.ts +29 -0
  8. package/dist/api/core/camera/index.d.ts.map +1 -1
  9. package/dist/api/core/geom/create/index.d.ts +7 -1
  10. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  11. package/dist/api/core/geom/update/contour.d.ts +8 -2
  12. package/dist/api/core/geom/update/contour.d.ts.map +1 -1
  13. package/dist/api/core/storeys/index.d.ts +5 -2
  14. package/dist/api/core/storeys/index.d.ts.map +1 -1
  15. package/dist/api/core/zoom/index.d.ts +13 -0
  16. package/dist/api/core/zoom/index.d.ts.map +1 -1
  17. package/dist/api/design/bim.d.ts +299 -0
  18. package/dist/api/design/bim.d.ts.map +1 -0
  19. package/dist/api/design/boolean/index.d.ts +2 -0
  20. package/dist/api/design/boolean/index.d.ts.map +1 -1
  21. package/dist/api/design/catalog.d.ts +320 -0
  22. package/dist/api/design/catalog.d.ts.map +1 -0
  23. package/dist/api/design/create/bulk-items.d.ts +65 -25
  24. package/dist/api/design/create/bulk-items.d.ts.map +1 -1
  25. package/dist/api/design/create/index.d.ts +299 -100
  26. package/dist/api/design/create/index.d.ts.map +1 -1
  27. package/dist/api/design/create/opening-fields.d.ts +13 -12
  28. package/dist/api/design/create/opening-fields.d.ts.map +1 -1
  29. package/dist/api/design/delete/index.d.ts +1 -0
  30. package/dist/api/design/delete/index.d.ts.map +1 -1
  31. package/dist/api/design/doors/index.d.ts +18 -24
  32. package/dist/api/design/doors/index.d.ts.map +1 -1
  33. package/dist/api/design/family.d.ts +813 -35
  34. package/dist/api/design/family.d.ts.map +1 -1
  35. package/dist/api/design/index.d.ts +16 -0
  36. package/dist/api/design/index.d.ts.map +1 -1
  37. package/dist/api/design/networks.d.ts +1703 -0
  38. package/dist/api/design/networks.d.ts.map +1 -0
  39. package/dist/api/design/query/grids.d.ts +2 -2
  40. package/dist/api/design/query/index.d.ts +42 -32
  41. package/dist/api/design/query/index.d.ts.map +1 -1
  42. package/dist/api/design/selection/index.d.ts +10 -10
  43. package/dist/api/design/types/index.d.ts +40 -24
  44. package/dist/api/design/types/index.d.ts.map +1 -1
  45. package/dist/api/design/update/index.d.ts +111 -6
  46. package/dist/api/design/update/index.d.ts.map +1 -1
  47. package/dist/api/design/windows/index.d.ts +8 -8
  48. package/dist/api/design/windows/index.d.ts.map +1 -1
  49. package/dist/api/entity/buildableEnvelope.d.ts +84 -2
  50. package/dist/api/entity/buildableEnvelope.d.ts.map +1 -1
  51. package/dist/api/presentation/aiInspiration.d.ts +10 -10
  52. package/dist/api/presentation/annotate.d.ts +38 -22
  53. package/dist/api/presentation/annotate.d.ts.map +1 -1
  54. package/dist/api/presentation/drawingContract.d.ts +32 -0
  55. package/dist/api/presentation/drawingContract.d.ts.map +1 -0
  56. package/dist/api/presentation/drawings.d.ts +3523 -0
  57. package/dist/api/presentation/drawings.d.ts.map +1 -0
  58. package/dist/api/presentation/export.d.ts +23 -5
  59. package/dist/api/presentation/export.d.ts.map +1 -1
  60. package/dist/api/presentation/index.d.ts +41 -12
  61. package/dist/api/presentation/index.d.ts.map +1 -1
  62. package/dist/api/presentation/placedViews.d.ts +106 -6
  63. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  64. package/dist/api/presentation/sections.d.ts +461 -0
  65. package/dist/api/presentation/sections.d.ts.map +1 -0
  66. package/dist/api/presentation/shapes.d.ts +2 -2
  67. package/dist/api/presentation/sheets.d.ts +175 -19
  68. package/dist/api/presentation/sheets.d.ts.map +1 -1
  69. package/dist/api/presentation/viewPresets.d.ts +222 -0
  70. package/dist/api/presentation/viewPresets.d.ts.map +1 -0
  71. package/dist/api/presentation/views.d.ts +141 -8
  72. package/dist/api/presentation/views.d.ts.map +1 -1
  73. package/dist/api/presentation/walkthroughs.d.ts +6 -6
  74. package/dist/errors/codes.d.ts +3 -1
  75. package/dist/errors/codes.d.ts.map +1 -1
  76. package/dist/index.cjs +3883 -2385
  77. package/dist/index.cjs.map +1 -1
  78. package/dist/index.js +3744 -2385
  79. package/dist/index.js.map +1 -1
  80. package/package.json +1 -1
  81. package/scripts/check-dist-fresh.mjs +22 -22
  82. package/scripts/generate-manifest.mjs +243 -238
  83. package/scripts/generate-manifest.test.mjs +303 -303
  84. package/src/api/analysis/daylight.ts +470 -470
  85. package/src/api/analysis/heatmaps.ts +683 -683
  86. package/src/api/analysis/illuminance.ts +155 -155
  87. package/src/api/analysis/index.ts +61 -61
  88. package/src/api/analysis/shadows.ts +183 -183
  89. package/src/api/analysis/solar.ts +237 -237
  90. package/src/api/analysis/sunlightHours.ts +211 -211
  91. package/src/api/analysis/sunpath.ts +83 -83
  92. package/src/api/analysis/weather.ts +179 -179
  93. package/src/api/core/buildings.ts +485 -485
  94. package/src/api/core/camera/index.ts +696 -667
  95. package/src/api/core/comment/index.ts +376 -376
  96. package/src/api/core/geom/create/index.ts +2139 -2133
  97. package/src/api/core/geom/delete/index.ts +84 -84
  98. package/src/api/core/geom/index.ts +35 -35
  99. package/src/api/core/geom/query/arc.ts +169 -169
  100. package/src/api/core/geom/query/brep.ts +621 -621
  101. package/src/api/core/geom/query/circle.ts +404 -404
  102. package/src/api/core/geom/query/contour.ts +540 -540
  103. package/src/api/core/geom/query/curve.ts +1102 -1102
  104. package/src/api/core/geom/query/edge.ts +130 -130
  105. package/src/api/core/geom/query/face.ts +508 -508
  106. package/src/api/core/geom/query/halfedge.ts +210 -210
  107. package/src/api/core/geom/query/index.ts +63 -63
  108. package/src/api/core/geom/query/profile.ts +473 -473
  109. package/src/api/core/geom/query/vertex.ts +205 -205
  110. package/src/api/core/geom/update/contour.ts +323 -317
  111. package/src/api/core/geom/update/curve.ts +209 -209
  112. package/src/api/core/geom/update/index.ts +29 -29
  113. package/src/api/core/geom/update/profile.ts +379 -379
  114. package/src/api/core/groups.ts +521 -521
  115. package/src/api/core/handles/index.ts +234 -234
  116. package/src/api/core/history/index.ts +84 -84
  117. package/src/api/core/index.ts +99 -99
  118. package/src/api/core/io/export/index.ts +126 -126
  119. package/src/api/core/io/import/index.ts +504 -504
  120. package/src/api/core/io/index.ts +42 -42
  121. package/src/api/core/io/job/index.ts +140 -140
  122. package/src/api/core/io/query/index.ts +71 -71
  123. package/src/api/core/io/terrain/index.ts +360 -360
  124. package/src/api/core/io/underlay/index.ts +705 -705
  125. package/src/api/core/layers.ts +540 -540
  126. package/src/api/core/math/index.ts +20 -20
  127. package/src/api/core/math/quat.ts +172 -172
  128. package/src/api/core/math/vec3.ts +179 -179
  129. package/src/api/core/mode/index.ts +108 -108
  130. package/src/api/core/project/index.ts +479 -479
  131. package/src/api/core/proposals/index.ts +570 -570
  132. package/src/api/core/storeys/index.ts +311 -308
  133. package/src/api/core/storeys/layers.ts +339 -339
  134. package/src/api/core/tags.ts +911 -911
  135. package/src/api/core/units/index.ts +164 -164
  136. package/src/api/core/user.ts +46 -46
  137. package/src/api/core/zoom/index.ts +64 -51
  138. package/src/api/design/bim.ts +210 -0
  139. package/src/api/design/boolean/index.ts +181 -179
  140. package/src/api/design/catalog.ts +203 -0
  141. package/src/api/design/create/bulk-items.ts +294 -254
  142. package/src/api/design/create/grids.ts +137 -137
  143. package/src/api/design/create/index.ts +2178 -1980
  144. package/src/api/design/create/opening-fields.ts +40 -37
  145. package/src/api/design/delete/index.ts +75 -74
  146. package/src/api/design/dimensions.ts +453 -453
  147. package/src/api/design/doors/index.ts +516 -500
  148. package/src/api/design/edit/index.ts +54 -54
  149. package/src/api/design/erase/index.ts +151 -151
  150. package/src/api/design/family.ts +1342 -704
  151. package/src/api/design/furniture/index.ts +319 -319
  152. package/src/api/design/grids.ts +237 -237
  153. package/src/api/design/index.ts +304 -274
  154. package/src/api/design/lock.ts +69 -69
  155. package/src/api/design/materials/index.ts +608 -608
  156. package/src/api/design/networks.ts +607 -0
  157. package/src/api/design/query/geometry/index.ts +319 -319
  158. package/src/api/design/query/grids.ts +197 -197
  159. package/src/api/design/query/index.ts +1236 -1226
  160. package/src/api/design/query/referenceLines.ts +52 -52
  161. package/src/api/design/query/spaces.ts +275 -275
  162. package/src/api/design/selection/index.ts +273 -273
  163. package/src/api/design/shared.ts +40 -40
  164. package/src/api/design/transform/index.ts +254 -254
  165. package/src/api/design/types/index.ts +163 -156
  166. package/src/api/design/update/index.ts +1096 -983
  167. package/src/api/design/visibility.ts +143 -143
  168. package/src/api/design/windows/index.ts +351 -333
  169. package/src/api/entity/buildableEnvelope.ts +397 -322
  170. package/src/api/entity/index.ts +30 -30
  171. package/src/api/entity/referenceLine.ts +235 -235
  172. package/src/api/entity/space.ts +612 -612
  173. package/src/api/entity/story.ts +517 -517
  174. package/src/api/index.ts +47 -47
  175. package/src/api/presentation/aiInspiration.ts +1116 -1116
  176. package/src/api/presentation/annotate.ts +417 -385
  177. package/src/api/presentation/camera.ts +289 -289
  178. package/src/api/presentation/diagrams.ts +194 -194
  179. package/src/api/presentation/drawingContract.ts +70 -0
  180. package/src/api/presentation/drawings.ts +1179 -0
  181. package/src/api/presentation/export.ts +134 -108
  182. package/src/api/presentation/import.ts +159 -159
  183. package/src/api/presentation/index.ts +160 -126
  184. package/src/api/presentation/placedViews.ts +1211 -1120
  185. package/src/api/presentation/sections.ts +420 -0
  186. package/src/api/presentation/shapes.ts +274 -274
  187. package/src/api/presentation/sheets.ts +746 -585
  188. package/src/api/presentation/slideshow.ts +134 -134
  189. package/src/api/presentation/tables.ts +84 -84
  190. package/src/api/presentation/viewPresets.ts +241 -0
  191. package/src/api/presentation/views.ts +742 -673
  192. package/src/api/presentation/walkthroughs.ts +1201 -1201
  193. package/src/api/program/adjacency.ts +164 -164
  194. package/src/api/program/areas.ts +408 -408
  195. package/src/api/program/classification.ts +138 -138
  196. package/src/api/program/cores.ts +83 -83
  197. package/src/api/program/departments.ts +517 -517
  198. package/src/api/program/index.ts +73 -73
  199. package/src/api/program/labels.ts +332 -332
  200. package/src/api/program/layout.ts +422 -422
  201. package/src/api/program/metadata.ts +364 -364
  202. package/src/api/program/metrics.ts +120 -120
  203. package/src/api/program/site.ts +748 -748
  204. package/src/api/program/spreadsheet.ts +2698 -2698
  205. package/src/api/workspace/index.ts +612 -612
  206. package/src/errors/codes.ts +143 -136
  207. package/src/errors/envelope.ts +75 -75
  208. package/src/errors/index.ts +21 -21
  209. package/src/errors/plugin-error.ts +134 -134
  210. package/src/handles.ts +297 -297
  211. package/src/host-utils.ts +67 -67
  212. package/src/index.ts +7 -7
  213. package/src/massParameters.ops.ts +246 -246
  214. package/src/massParameters.ts +427 -427
  215. package/src/types.ts +16 -16
  216. package/test/drawingContract.test.mjs +288 -0
  217. package/test/errors.test.mjs +185 -184
  218. package/test/massParameters.test.mjs +952 -952
  219. package/test/presentationViewSettings.test.mjs +85 -0
  220. package/tsconfig.json +22 -22
  221. package/tsup.config.ts +12 -12
@@ -1,952 +1,952 @@
1
- // Node built-in test runner suite for the shared mass-parameter record schemas.
2
- // Run: node --test test/
3
- // Imports the BUILT package (dist) — run `pnpm build` first.
4
- import test from "node:test"
5
- import assert from "node:assert/strict"
6
- import { readFileSync } from "node:fs"
7
- import {
8
- MassParametersRecord,
9
- MassParametersPatch,
10
- OperationRecord,
11
- ContourRecord,
12
- ProfileRecord,
13
- CurveRecord,
14
- EdgeSelector,
15
- FaceSelector,
16
- ParameterPatchValue,
17
- SweepAlongCurveRecordOptions,
18
- MAX_PROFILE_CURVES,
19
- MAX_CONTOUR_HOLES,
20
- MAX_SPLINE_THROUGH_POINTS,
21
- MAX_SWEEP_PATH_POINTS,
22
- MAX_LOFT_SECTIONS,
23
- MAX_OPERATIONS,
24
- MAX_OPERATION_EDGES,
25
- MAX_OPERATION_FACES,
26
- MAX_PATH_CURVES,
27
- MAX_TWIST_LEVELS,
28
- MAX_FILLET_LAW_SAMPLES,
29
- MAX_PATCH_BOUNDARY_CURVES,
30
- MAX_PATCH_INTERIOR_POINTS,
31
- CurveHandle,
32
- SplineHandle,
33
- ArenaKind,
34
- PluginHandlesReleaseAllArgs,
35
- PluginGeomCreateBrepFromSweepArgs,
36
- PluginGeomCreateBrepFromLoftArgs,
37
- PluginGeomCreateBrepFromFilletArgs,
38
- PluginGeomCreateBrepFromChamferArgs,
39
- PluginGeomCreateBrepFromShellArgs,
40
- PluginGeomCreateBrepFromExtrusionArgs,
41
- PluginGeomCreateBrepFromPatchArgs,
42
- PluginGeomCreateBrepFromSweepAlongCurveArgs,
43
- PluginGeomCreateBrepsFromSplitByFaceArgs,
44
- PluginGeomCreateCurvesFromProjectionArgs,
45
- PluginGeomCreateSplineFromControlPointsArgs,
46
- PluginDesignUpdateParametersArgs,
47
- PluginGeomQueryFaceGetAreaArgs,
48
- PluginGeomQueryFaceGetCentroidArgs,
49
- } from "../dist/index.js"
50
-
51
- const p = (x, y, z) => ({ x, y, z })
52
-
53
- /** A square outer loop, no holes — the simplest legal contour record. */
54
- const squareContour = {
55
- outer: {
56
- curves: [
57
- { type: "line", start: p(0, 0, 0), end: p(4, 0, 0) },
58
- { type: "line", start: p(4, 0, 0), end: p(4, 0, 4) },
59
- { type: "line", start: p(4, 0, 4), end: p(0, 0, 4) },
60
- { type: "line", start: p(0, 0, 4), end: p(0, 0, 0) },
61
- ],
62
- },
63
- holes: [],
64
- }
65
-
66
- test("accepts one record of every kind", () => {
67
- const records = [
68
- { kind: "sphere", version: 1, values: { centre: p(0, 5, 0), radius: 5 } },
69
- {
70
- kind: "cylinder",
71
- version: 1,
72
- values: { base: p(0, 0, 0), axis: p(0, 1, 0), radius: 1, height: 4 },
73
- },
74
- {
75
- kind: "cone",
76
- version: 1,
77
- values: { base: p(0, 0, 0), axis: p(0, 1, 0), baseRadius: 2, topRadius: 0, height: 3 },
78
- },
79
- {
80
- kind: "torus",
81
- version: 1,
82
- values: { centre: p(0, 6, 0), axis: p(0, 1, 0), majorRadius: 5, minorRadius: 1 },
83
- },
84
- {
85
- kind: "extrusion",
86
- version: 1,
87
- values: { contour: squareContour, direction: p(0, 1, 0), amount: 3 },
88
- },
89
- {
90
- kind: "revolution",
91
- version: 1,
92
- values: {
93
- contour: squareContour,
94
- axisOrigin: p(0, 0, 0),
95
- axisDirection: p(0, 1, 0),
96
- angleInDegrees: 360,
97
- },
98
- },
99
- {
100
- kind: "loft",
101
- version: 1,
102
- values: { sections: [squareContour, squareContour], options: { compatibility: "auto" } },
103
- },
104
- {
105
- kind: "sweep",
106
- version: 1,
107
- values: {
108
- profile: squareContour,
109
- path: [p(0, 0, 0), p(0, 3, 0)],
110
- options: { transition: "round" },
111
- },
112
- },
113
- ]
114
- for (const record of records) {
115
- const parsed = MassParametersRecord.safeParse(record)
116
- assert.equal(parsed.success, true, `${record.kind}: ${JSON.stringify(parsed.error?.issues)}`)
117
- assert.deepEqual(parsed.data, record)
118
- }
119
- })
120
-
121
- test("both spline curve forms round-trip inside a contour", () => {
122
- const contour = {
123
- outer: {
124
- curves: [
125
- { type: "splineThrough", fitVersion: 1, throughPoints: [p(0, 0, 0), p(1, 0, 2), p(3, 0, 1)] },
126
- {
127
- type: "splinePoles",
128
- degree: 3,
129
- knots: [0, 1],
130
- mults: [4, 4],
131
- poles: [p(3, 0, 1), p(2, 0, 0), p(1, 0, -1), p(0, 0, 0)],
132
- },
133
- ],
134
- },
135
- holes: [],
136
- }
137
- assert.equal(ContourRecord.safeParse(contour).success, true)
138
- })
139
-
140
- test("rejects a record that is not the truth", () => {
141
- // Unknown key — the host names it back to the caller, never silently drops it.
142
- assert.equal(
143
- MassParametersRecord.safeParse({
144
- kind: "torus",
145
- version: 1,
146
- values: { centre: p(0, 0, 0), axis: p(0, 1, 0), majorRadius: 5, minorRadius: 1, radius: 5 },
147
- }).success,
148
- false,
149
- )
150
- // Unknown schema version — restore degrades the mass to non-parametric.
151
- assert.equal(
152
- MassParametersRecord.safeParse({
153
- kind: "sphere",
154
- version: 2,
155
- values: { centre: p(0, 0, 0), radius: 5 },
156
- }).success,
157
- false,
158
- )
159
- // Non-finite / non-positive values.
160
- for (const radius of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
161
- assert.equal(
162
- MassParametersRecord.safeParse({
163
- kind: "sphere",
164
- version: 1,
165
- values: { centre: p(0, 0, 0), radius },
166
- }).success,
167
- false,
168
- `radius ${radius} must be rejected`,
169
- )
170
- }
171
- // A spline record with neither form's fields.
172
- assert.equal(CurveRecord.safeParse({ type: "splineThrough", throughPoints: [] }).success, false)
173
- })
174
-
175
- test("array caps are contract, not truncation", () => {
176
- const points = (n) => Array.from({ length: n }, (_, i) => p(i, 0, 0))
177
- const spline = (n) => ({ type: "splineThrough", fitVersion: 1, throughPoints: points(n) })
178
- assert.equal(CurveRecord.safeParse(spline(MAX_SPLINE_THROUGH_POINTS)).success, true)
179
- assert.equal(CurveRecord.safeParse(spline(MAX_SPLINE_THROUGH_POINTS + 1)).success, false)
180
-
181
- const sweep = (n) => ({
182
- kind: "sweep",
183
- version: 1,
184
- values: { profile: squareContour, path: points(n) },
185
- })
186
- assert.equal(MassParametersRecord.safeParse(sweep(MAX_SWEEP_PATH_POINTS)).success, true)
187
- assert.equal(MassParametersRecord.safeParse(sweep(MAX_SWEEP_PATH_POINTS + 1)).success, false)
188
-
189
- const loft = (n) => ({
190
- kind: "loft",
191
- version: 1,
192
- values: { sections: Array.from({ length: n }, () => squareContour) },
193
- })
194
- assert.equal(MassParametersRecord.safeParse(loft(MAX_LOFT_SECTIONS)).success, true)
195
- assert.equal(MassParametersRecord.safeParse(loft(MAX_LOFT_SECTIONS + 1)).success, false)
196
-
197
- // The caps bind the RECORD only. The creator args (shipped in 0.9.6 without
198
- // any cap) keep accepting an over-cap call — the host builds it and mints no
199
- // record (the recipe-less fallback). See "wire schemas keep 0.9.6 acceptance".
200
- const sweepArgs = (n) => ({ profile: "contour_1", path: points(n) })
201
- assert.equal(
202
- PluginGeomCreateBrepFromSweepArgs.safeParse(sweepArgs(MAX_SWEEP_PATH_POINTS)).success,
203
- true,
204
- )
205
- const loftArgs = (n) => ({
206
- bottomContour: "contour_1",
207
- topContour: "contour_2",
208
- intermediateContours: Array.from({ length: n }, (_, i) => `contour_${i + 3}`),
209
- })
210
- assert.equal(
211
- PluginGeomCreateBrepFromLoftArgs.safeParse(loftArgs(MAX_LOFT_SECTIONS - 2)).success,
212
- true,
213
- )
214
-
215
- // A contour's own arrays are bounded too — a 20 000-curve loop or a
216
- // 5 000-hole contour must not ride the save/collab payload as a record.
217
- const loop = (n) => ({
218
- curves: Array.from({ length: n }, (_, i) => ({
219
- type: "line",
220
- start: p(i, 0, 0),
221
- end: p(i + 1, 0, 0),
222
- })),
223
- })
224
- assert.equal(ProfileRecord.safeParse(loop(MAX_PROFILE_CURVES)).success, true)
225
- assert.equal(ProfileRecord.safeParse(loop(MAX_PROFILE_CURVES + 1)).success, false)
226
-
227
- const holed = (n) => ({
228
- outer: squareContour.outer,
229
- holes: Array.from({ length: n }, () => squareContour.outer),
230
- })
231
- assert.equal(ContourRecord.safeParse(holed(MAX_CONTOUR_HOLES)).success, true)
232
- assert.equal(ContourRecord.safeParse(holed(MAX_CONTOUR_HOLES + 1)).success, false)
233
- })
234
-
235
- /** sphere → shell → fillet: the chained record of binding-design-4b §5.2. */
236
- const chainedRecord = {
237
- kind: "sphere",
238
- version: 1,
239
- values: { centre: p(0, 23.6, 0), radius: 23.622 },
240
- operations: [
241
- {
242
- op: "shell",
243
- openFaces: [{ index: 3, kind: "plane", edgeCount: 2 }],
244
- thickness: 1.181,
245
- input: { faceCount: 4, edgeCount: 6 },
246
- },
247
- {
248
- op: "fillet",
249
- edges: [
250
- { index: 2, faces: [0, 3], curve: "arc" },
251
- { index: 5, faces: [1, 3], curve: "arc" },
252
- ],
253
- radius: 0.787,
254
- input: { faceCount: 8, edgeCount: 14 },
255
- },
256
- ],
257
- }
258
-
259
- test("a chained operations record round-trips", () => {
260
- const parsed = MassParametersRecord.safeParse(chainedRecord)
261
- assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
262
- assert.deepEqual(parsed.data, chainedRecord)
263
-
264
- // `operations` is additive and optional — a 4A-era record still parses.
265
- const { operations, ...base } = chainedRecord
266
- assert.equal(MassParametersRecord.safeParse(base).success, true)
267
-
268
- // Every op kind on its own.
269
- const chamfer = {
270
- op: "chamfer",
271
- edges: [{ index: 0, faces: [0, 1], curve: "line" }],
272
- distance: 0.1,
273
- input: { faceCount: 6, edgeCount: 12 },
274
- }
275
- const offset = { op: "offset", distance: -0.4, input: { faceCount: 4, edgeCount: 6 } }
276
- for (const op of [operations[0], operations[1], chamfer, offset]) {
277
- assert.deepEqual(OperationRecord.parse(op), op)
278
- }
279
- })
280
-
281
- test("operation records reject what cannot be replayed", () => {
282
- const counts = { faceCount: 4, edgeCount: 6 }
283
- // A selector list is never empty — an op that selected nothing is not a record.
284
- assert.equal(OperationRecord.safeParse({ op: "fillet", edges: [], radius: 1, input: counts }).success, false)
285
- // Fractional / negative indices cannot address a face or an edge.
286
- assert.equal(
287
- OperationRecord.safeParse({
288
- op: "fillet",
289
- edges: [{ index: 1.5, faces: [0, 1], curve: "arc" }],
290
- radius: 1,
291
- input: counts,
292
- }).success,
293
- false,
294
- )
295
- assert.equal(
296
- OperationRecord.safeParse({
297
- op: "fillet",
298
- edges: [{ index: 0, faces: [0, -1], curve: "arc" }],
299
- radius: 1,
300
- input: counts,
301
- }).success,
302
- false,
303
- )
304
- // A face pair is exactly two faces.
305
- assert.equal(
306
- OperationRecord.safeParse({
307
- op: "fillet",
308
- edges: [{ index: 0, faces: [0, 1, 2], curve: "arc" }],
309
- radius: 1,
310
- input: counts,
311
- }).success,
312
- false,
313
- )
314
- // Unknown surface / curve kinds, unknown op, and a zero offset.
315
- assert.equal(
316
- OperationRecord.safeParse({
317
- op: "shell",
318
- openFaces: [{ index: 0, kind: "nurbs", edgeCount: 2 }],
319
- thickness: 1,
320
- input: counts,
321
- }).success,
322
- false,
323
- )
324
- assert.equal(OperationRecord.safeParse({ op: "draft", angle: 3, input: counts }).success, false)
325
- assert.equal(OperationRecord.safeParse({ op: "offset", distance: 0, input: counts }).success, false)
326
- // Non-positive scalars.
327
- for (const radius of [0, -1, Number.NaN]) {
328
- assert.equal(
329
- OperationRecord.safeParse({
330
- op: "fillet",
331
- edges: [{ index: 0, faces: [0, 1], curve: "arc" }],
332
- radius,
333
- input: counts,
334
- }).success,
335
- false,
336
- `radius ${radius} must be rejected`,
337
- )
338
- }
339
- })
340
-
341
- test("operation caps are contract, not truncation", () => {
342
- const counts = { faceCount: 4, edgeCount: 6 }
343
- const fillet = (n) => ({
344
- op: "fillet",
345
- edges: Array.from({ length: n }, (_, i) => ({ index: i, faces: [0, 1], curve: "line" })),
346
- radius: 1,
347
- input: counts,
348
- })
349
- assert.equal(OperationRecord.safeParse(fillet(MAX_OPERATION_EDGES)).success, true)
350
- assert.equal(OperationRecord.safeParse(fillet(MAX_OPERATION_EDGES + 1)).success, false)
351
-
352
- const shell = (n) => ({
353
- op: "shell",
354
- openFaces: Array.from({ length: n }, (_, i) => ({ index: i, kind: "plane", edgeCount: 4 })),
355
- thickness: 1,
356
- input: counts,
357
- })
358
- assert.equal(OperationRecord.safeParse(shell(MAX_OPERATION_FACES)).success, true)
359
- assert.equal(OperationRecord.safeParse(shell(MAX_OPERATION_FACES + 1)).success, false)
360
-
361
- const chain = (n) => ({ ...chainedRecord, operations: Array.from({ length: n }, () => fillet(1)) })
362
- assert.equal(MassParametersRecord.safeParse(chain(MAX_OPERATIONS)).success, true)
363
- assert.equal(MassParametersRecord.safeParse(chain(MAX_OPERATIONS + 1)).success, false)
364
-
365
- // The creator args are NOT capped (see "wire schemas keep 0.9.6 acceptance").
366
- const edges = (n) => Array.from({ length: n }, (_, i) => `edge_${i}`)
367
- const faces = (n) => Array.from({ length: n }, (_, i) => `face_${i}`)
368
- const filletArgs = (n) => ({ brep: "brep_1", edges: edges(n), radius: 1 })
369
- assert.equal(PluginGeomCreateBrepFromFilletArgs.safeParse(filletArgs(MAX_OPERATION_EDGES)).success, true)
370
- const shellArgs = (n) => ({ brep: "brep_1", openFaces: faces(n), thickness: 1 })
371
- assert.equal(PluginGeomCreateBrepFromShellArgs.safeParse(shellArgs(MAX_OPERATION_FACES)).success, true)
372
- })
373
-
374
- test("wire schemas keep 0.9.6 acceptance: record caps never reach the call", () => {
375
- // The five inputs the external review (2026-09-06, #5) ran against the
376
- // published 0.9.6 schemas: all accepted there, so all must stay accepted
377
- // (feedback-no-breaking-api-changes — a shipped member never narrows). The
378
- // record caps above still hold; the host answers an over-cap call with a
379
- // solid and no record, never with VALIDATION.
380
- const pts = (n) => Array.from({ length: n }, (_, i) => p(i, 0, 0))
381
- const ids = (prefix, n) => Array.from({ length: n }, (_, i) => `${prefix}_${i}`)
382
- const accepted = [
383
- [
384
- "loft with 31 intermediates",
385
- PluginGeomCreateBrepFromLoftArgs.safeParse({
386
- bottomContour: "contour_1",
387
- topContour: "contour_2",
388
- intermediateContours: ids("contour", MAX_LOFT_SECTIONS - 1),
389
- }),
390
- ],
391
- [
392
- "sweep with 513 path points",
393
- PluginGeomCreateBrepFromSweepArgs.safeParse({
394
- profile: "contour_1",
395
- path: pts(MAX_SWEEP_PATH_POINTS + 1),
396
- }),
397
- ],
398
- [
399
- "fillet with 65 edges",
400
- PluginGeomCreateBrepFromFilletArgs.safeParse({
401
- brep: "brep_1",
402
- edges: ids("edge", MAX_OPERATION_EDGES + 1),
403
- radius: 1,
404
- }),
405
- ],
406
- [
407
- "chamfer with 65 edges",
408
- PluginGeomCreateBrepFromChamferArgs.safeParse({
409
- brep: "brep_1",
410
- edges: ids("edge", MAX_OPERATION_EDGES + 1),
411
- distance: 1,
412
- }),
413
- ],
414
- [
415
- "shell with 33 open faces",
416
- PluginGeomCreateBrepFromShellArgs.safeParse({
417
- brep: "brep_1",
418
- openFaces: ids("face", MAX_OPERATION_FACES + 1),
419
- thickness: 1,
420
- }),
421
- ],
422
- ]
423
- for (const [what, result] of accepted) {
424
- assert.equal(result.success, true, `${what} must stay schema-valid: ${JSON.stringify(result.error?.issues)}`)
425
- }
426
- })
427
-
428
- test("MassParametersPatch takes both a partial and a full record", () => {
429
- // Partial forms: a scalar, a point, a whole array, and the empty op list.
430
- for (const patch of [
431
- { radius: 31.496 },
432
- { centre: p(1, 2, 3) },
433
- { path: [p(0, 0, 0), p(0, 3, 0)] },
434
- { sections: [squareContour, squareContour] },
435
- { contour: squareContour },
436
- { options: { seamAlignment: "authored" } },
437
- { options: { startScale: 1, endScale: 2, transition: { bevel: 0.2 } } },
438
- { operations: chainedRecord.operations },
439
- { operations: [] },
440
- ]) {
441
- const parsed = MassParametersPatch.safeParse(patch)
442
- assert.equal(parsed.success, true, `${JSON.stringify(patch)}: ${JSON.stringify(parsed.error?.issues)}`)
443
- assert.deepEqual(parsed.data, patch)
444
- }
445
-
446
- // Full-record form: read → edit → send the whole thing back.
447
- const echoed = MassParametersPatch.safeParse(chainedRecord)
448
- assert.equal(echoed.success, true)
449
- assert.deepEqual(echoed.data, chainedRecord)
450
-
451
- // …including the record edited to drop every operation. `{ operations: [] }`
452
- // is the documented drop idiom, so the whole-record route must take it too
453
- // (the host normalises the empty list to an absent key before storing).
454
- const dropped = { ...chainedRecord, operations: [] }
455
- const droppedParse = MassParametersPatch.safeParse(dropped)
456
- assert.equal(droppedParse.success, true, JSON.stringify(droppedParse.error?.issues))
457
- assert.deepEqual(droppedParse.data, dropped)
458
- assert.equal(MassParametersRecord.safeParse(dropped).success, true)
459
-
460
- // Values outside the closed union are refused at the wire, not written on.
461
- for (const patch of [
462
- { radius: "8" },
463
- { radius: Number.POSITIVE_INFINITY },
464
- { centre: { x: 0, y: 0 } },
465
- { operations: [{ op: "fillet", edges: [], radius: 1, input: { faceCount: 4, edgeCount: 6 } }] },
466
- { nested: { deeply: { wrong: true } } },
467
- ]) {
468
- assert.equal(MassParametersPatch.safeParse(patch).success, false, JSON.stringify(patch))
469
- }
470
- })
471
-
472
- test("the 4B call args parse", () => {
473
- assert.equal(
474
- PluginDesignUpdateParametersArgs.safeParse({ component: "component_1", patch: { radius: 8 } })
475
- .success,
476
- true,
477
- )
478
- assert.equal(
479
- PluginDesignUpdateParametersArgs.safeParse({ component: "component_1", patch: { radius: null } })
480
- .success,
481
- false,
482
- )
483
-
484
- const poles = (n) => Array.from({ length: n }, (_, i) => `vec3_${i}`)
485
- assert.equal(
486
- PluginGeomCreateSplineFromControlPointsArgs.safeParse({ controlPoints: poles(4) }).success,
487
- true,
488
- )
489
- assert.equal(
490
- PluginGeomCreateSplineFromControlPointsArgs.safeParse({
491
- controlPoints: poles(4),
492
- options: { degree: 2 },
493
- }).success,
494
- true,
495
- )
496
- // degree is 2 or 3 — 1 is a polyline, 4+ is not offered.
497
- for (const degree of [1, 4, 2.5]) {
498
- assert.equal(
499
- PluginGeomCreateSplineFromControlPointsArgs.safeParse({
500
- controlPoints: poles(4),
501
- options: { degree },
502
- }).success,
503
- false,
504
- `degree ${degree} must be rejected`,
505
- )
506
- }
507
- assert.equal(
508
- PluginGeomCreateSplineFromControlPointsArgs.safeParse({ controlPoints: poles(2) }).success,
509
- false,
510
- )
511
- })
512
-
513
- test("CurveHandle accepts a spline handle", () => {
514
- assert.equal(SplineHandle.parse("spline_1").id, "spline_1")
515
- for (const id of ["line_1", "arc_1", "spline_1"]) {
516
- assert.equal(CurveHandle.parse(id).id, id)
517
- }
518
- assert.equal(CurveHandle.safeParse("circle_1").success, false)
519
- })
520
-
521
- test("a spline handle is releasable like every other value handle", () => {
522
- // ArenaKind types release / endScope(retain) and validates releaseAll's kind.
523
- assert.equal(ArenaKind.safeParse("spline").success, true)
524
- assert.equal(PluginHandlesReleaseAllArgs.safeParse({ kind: "spline" }).success, true)
525
- })
526
-
527
- test("CurveHandle keeps the union type that 0.9.13 shipped, so .options still compiles", () => {
528
- const dts = readFileSync(new URL("../dist/handles.d.ts", import.meta.url), "utf8")
529
- assert.ok(/declare const CurveHandle: z\.ZodUnion</.test(dts), "CurveHandle is not declared as a ZodUnion")
530
- assert.equal(CurveHandle.options.length, 3)
531
- })
532
-
533
- // ── Freeform surfaces, sweeps, patches and variable blends ──────────────────
534
-
535
- const line = (a, b) => ({ type: "line", start: a, end: b })
536
- const topology = { faceCount: 6, edgeCount: 12 }
537
- const edgeSel = { index: 3, faces: [0, 1], curve: "spline" }
538
-
539
- test("a sweep-along-a-curve record round-trips, and its path is curves", () => {
540
- const record = {
541
- kind: "sweepAlongCurve",
542
- version: 1,
543
- values: {
544
- profile: squareContour,
545
- path: [
546
- line(p(0, 0, 0), p(0, 10, 0)),
547
- {
548
- type: "arc",
549
- start: p(0, 10, 0),
550
- end: p(5, 15, 0),
551
- centre: p(5, 10, 0),
552
- axis: p(0, 0, 1),
553
- },
554
- ],
555
- options: { orientation: "upright" },
556
- },
557
- }
558
- const parsed = MassParametersRecord.safeParse(record)
559
- assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
560
- assert.deepEqual(parsed.data, record)
561
-
562
- // The options bag is the RECORD's: pure JSON, no rail handle.
563
- assert.equal(SweepAlongCurveRecordOptions.safeParse({ orientation: "fixed" }).success, true)
564
- assert.equal(SweepAlongCurveRecordOptions.safeParse({ orientation: "banking" }).success, false)
565
- assert.equal(SweepAlongCurveRecordOptions.safeParse({ rail: "line_1" }).success, false)
566
-
567
- // Caps and vocabulary are contract: 1..64 curves, no XYZ path.
568
- const withPath = (path) => ({ ...record, values: { ...record.values, path } })
569
- assert.equal(
570
- MassParametersRecord.safeParse(
571
- withPath(Array.from({ length: MAX_PATH_CURVES }, () => line(p(0, 0, 0), p(1, 0, 0)))),
572
- ).success,
573
- true,
574
- )
575
- assert.equal(
576
- MassParametersRecord.safeParse(
577
- withPath(Array.from({ length: MAX_PATH_CURVES + 1 }, () => line(p(0, 0, 0), p(1, 0, 0)))),
578
- ).success,
579
- false,
580
- )
581
- assert.equal(MassParametersRecord.safeParse(withPath([])).success, false)
582
- assert.equal(MassParametersRecord.safeParse(withPath([p(0, 0, 0), p(1, 0, 0)])).success, false)
583
- // The shipped polyline `sweep` kind is untouched — its path is still points.
584
- assert.equal(
585
- MassParametersRecord.safeParse({
586
- kind: "sweep",
587
- version: 1,
588
- values: { profile: squareContour, path: [p(0, 0, 0), p(0, 3, 0)] },
589
- }).success,
590
- true,
591
- )
592
- })
593
-
594
- test("an extrusion record carries twist, taper and levels", () => {
595
- const straight = {
596
- kind: "extrusion",
597
- version: 1,
598
- values: { contour: squareContour, direction: p(0, 1, 0), amount: 40 },
599
- }
600
- const twisted = {
601
- ...straight,
602
- values: { ...straight.values, twistInDegrees: 90, topScale: 0.6, levels: 6 },
603
- }
604
- const parsed = MassParametersRecord.safeParse(twisted)
605
- assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
606
- assert.deepEqual(parsed.data, twisted)
607
- // Absent keys are still the shipped straight extrusion, byte for byte.
608
- assert.deepEqual(MassParametersRecord.parse(straight), straight)
609
-
610
- const values = (extra) => ({ ...straight, values: { ...straight.values, ...extra } })
611
- assert.equal(MassParametersRecord.safeParse(values({ levels: MAX_TWIST_LEVELS })).success, true)
612
- assert.equal(
613
- MassParametersRecord.safeParse(values({ levels: MAX_TWIST_LEVELS + 1 })).success,
614
- false,
615
- )
616
- assert.equal(MassParametersRecord.safeParse(values({ levels: 1 })).success, false)
617
- assert.equal(MassParametersRecord.safeParse(values({ topScale: -1 })).success, false)
618
- assert.equal(MassParametersRecord.safeParse(values({ twistInDegrees: "90" })).success, false)
619
- })
620
-
621
- test("a loft record chooses its smoothing", () => {
622
- const record = {
623
- kind: "loft",
624
- version: 1,
625
- values: { sections: [squareContour, squareContour], options: { smoothing: "smooth" } },
626
- }
627
- assert.deepEqual(MassParametersRecord.parse(record), record)
628
- assert.equal(
629
- MassParametersRecord.safeParse({
630
- ...record,
631
- values: { ...record.values, options: { smoothing: "curvy" } },
632
- }).success,
633
- false,
634
- )
635
- })
636
-
637
- test("selectors carry an optional normalised position", () => {
638
- const face = { index: 2, kind: "spline", edgeCount: 4, at: { x: 0.13, y: 1, z: 0.87 } }
639
- assert.deepEqual(FaceSelector.parse(face), face)
640
- const edge = { ...edgeSel, at: { x: 0.5, y: 0, z: 0.5 } }
641
- assert.deepEqual(EdgeSelector.parse(edge), edge)
642
- // A selector recorded before `at` existed still parses unchanged — the key
643
- // only widens.
644
- assert.deepEqual(EdgeSelector.parse(edgeSel), edgeSel)
645
- assert.equal(FaceSelector.safeParse({ ...face, at: { x: 0, y: 0 } }).success, false)
646
- assert.equal(EdgeSelector.safeParse({ ...edgeSel, at: [0, 0, 0] }).success, false)
647
- })
648
-
649
- test("a variable fillet operation names one edge and one law", () => {
650
- const constant = { op: "fillet", edges: [edgeSel], radius: 0.5, input: topology }
651
- const tapered = { ...constant, endRadius: 1.5 }
652
- const sampled = {
653
- ...constant,
654
- radii: [
655
- { position: 0, radius: 0.5 },
656
- { position: 0.5, radius: 2 },
657
- { position: 1, radius: 0.5 },
658
- ],
659
- }
660
- assert.deepEqual(OperationRecord.parse(constant), constant)
661
- assert.deepEqual(OperationRecord.parse(tapered), tapered)
662
- assert.deepEqual(OperationRecord.parse(sampled), sampled)
663
-
664
- // Both laws at once, more than one edge, a table that does not run forward,
665
- // a single row, a position outside 0..1, and an over-long table are refused.
666
- const twoEdges = [edgeSel, { ...edgeSel, index: 4 }]
667
- for (const operation of [
668
- { ...tapered, radii: sampled.radii },
669
- { ...tapered, edges: twoEdges },
670
- { ...sampled, edges: twoEdges },
671
- {
672
- ...constant,
673
- radii: [
674
- { position: 0, radius: 1 },
675
- { position: 0, radius: 2 },
676
- ],
677
- },
678
- { ...constant, radii: [{ position: 0, radius: 1 }] },
679
- {
680
- ...constant,
681
- radii: [
682
- { position: 0, radius: 1 },
683
- { position: 1.5, radius: 2 },
684
- ],
685
- },
686
- {
687
- ...constant,
688
- radii: Array.from({ length: MAX_FILLET_LAW_SAMPLES + 1 }, (_, i) => ({
689
- position: i / MAX_FILLET_LAW_SAMPLES,
690
- radius: 1,
691
- })),
692
- },
693
- ]) {
694
- assert.equal(OperationRecord.safeParse(operation).success, false, JSON.stringify(operation))
695
- }
696
- // A full table of the maximum length still parses.
697
- assert.equal(
698
- OperationRecord.safeParse({
699
- ...constant,
700
- radii: Array.from({ length: MAX_FILLET_LAW_SAMPLES }, (_, i) => ({
701
- position: i / (MAX_FILLET_LAW_SAMPLES - 1),
702
- radius: 1,
703
- })),
704
- }).success,
705
- true,
706
- )
707
- })
708
-
709
- test("the patch union widens to the new option bag and curve paths", () => {
710
- assert.equal(ParameterPatchValue.safeParse({ orientation: "frenet" }).success, true)
711
- assert.equal(ParameterPatchValue.safeParse([line(p(0, 0, 0), p(1, 0, 0))]).success, true)
712
- assert.equal(ParameterPatchValue.safeParse({ smoothing: "smooth" }).success, true)
713
- assert.equal(ParameterPatchValue.safeParse(90).success, true)
714
- // A radius law is a key INSIDE an operation, never a top-level `values` key.
715
- assert.equal(ParameterPatchValue.safeParse([{ position: 0, radius: 1 }]).success, false)
716
-
717
- assert.equal(
718
- MassParametersPatch.safeParse({
719
- path: [line(p(0, 0, 0), p(0, 10, 0))],
720
- options: { orientation: "fixed" },
721
- }).success,
722
- true,
723
- )
724
- assert.equal(MassParametersPatch.safeParse({ twistInDegrees: 120, topScale: 0.5 }).success, true)
725
- assert.equal(
726
- MassParametersPatch.safeParse({
727
- operations: [{ op: "fillet", edges: [edgeSel], radius: 0.5, endRadius: 2, input: topology }],
728
- }).success,
729
- true,
730
- )
731
- assert.equal(MassParametersPatch.safeParse({ orientation: "sideways" }).success, false)
732
- })
733
-
734
- test("the freeform call args parse", () => {
735
- // brepFromExtrusion keeps its three-positional form and gains a trailing bag.
736
- const extrusion = { contour: "contour_1", direction: p(0, 1, 0), amount: 40 }
737
- assert.equal(PluginGeomCreateBrepFromExtrusionArgs.safeParse(extrusion).success, true)
738
- assert.equal(
739
- PluginGeomCreateBrepFromExtrusionArgs.safeParse({
740
- ...extrusion,
741
- options: { twistInDegrees: 90, topScale: 0.6, levels: 6 },
742
- }).success,
743
- true,
744
- )
745
- assert.equal(
746
- PluginGeomCreateBrepFromExtrusionArgs.safeParse({ ...extrusion, options: { levels: 1 } })
747
- .success,
748
- false,
749
- )
750
-
751
- // brepFromLoft gains smoothing beside the two shipped switches.
752
- const loft = { bottomContour: "contour_1", topContour: "contour_2" }
753
- assert.equal(PluginGeomCreateBrepFromLoftArgs.safeParse(loft).success, true)
754
- assert.equal(
755
- PluginGeomCreateBrepFromLoftArgs.safeParse({ ...loft, options: { smoothing: "smooth" } })
756
- .success,
757
- true,
758
- )
759
- assert.equal(
760
- PluginGeomCreateBrepFromLoftArgs.safeParse({ ...loft, options: { smoothing: "silky" } })
761
- .success,
762
- false,
763
- )
764
-
765
- // brepFromFillet: the shipped three-argument call is untouched; a variable
766
- // blend takes exactly one edge and exactly one law.
767
- const fillet = { brep: "brep_1", edges: ["edge_1"], radius: 0.5 }
768
- assert.equal(PluginGeomCreateBrepFromFilletArgs.safeParse(fillet).success, true)
769
- assert.equal(
770
- PluginGeomCreateBrepFromFilletArgs.safeParse({ ...fillet, edges: ["edge_1", "edge_2"] })
771
- .success,
772
- true,
773
- )
774
- assert.equal(
775
- PluginGeomCreateBrepFromFilletArgs.safeParse({ ...fillet, options: { endRadius: 2 } }).success,
776
- true,
777
- )
778
- assert.equal(
779
- PluginGeomCreateBrepFromFilletArgs.safeParse({
780
- ...fillet,
781
- options: {
782
- radii: [
783
- { position: 0, radius: 0.5 },
784
- { position: 1, radius: 2 },
785
- ],
786
- },
787
- }).success,
788
- true,
789
- )
790
- for (const bad of [
791
- { ...fillet, edges: ["edge_1", "edge_2"], options: { endRadius: 2 } },
792
- {
793
- ...fillet,
794
- options: {
795
- endRadius: 2,
796
- radii: [
797
- { position: 0, radius: 1 },
798
- { position: 1, radius: 2 },
799
- ],
800
- },
801
- },
802
- {
803
- ...fillet,
804
- options: {
805
- radii: [
806
- { position: 1, radius: 1 },
807
- { position: 0, radius: 2 },
808
- ],
809
- },
810
- },
811
- { ...fillet, options: { radii: [{ position: 0, radius: 1 }] } },
812
- ]) {
813
- assert.equal(
814
- PluginGeomCreateBrepFromFilletArgs.safeParse(bad).success,
815
- false,
816
- JSON.stringify(bad),
817
- )
818
- }
819
-
820
- // brepFromPatch: curves, owned edges, and the tangent rule.
821
- const boundary = ["line_1", "arc_1", { edge: "edge_1", brep: "brep_1" }]
822
- assert.equal(
823
- PluginGeomCreateBrepFromPatchArgs.safeParse({ boundary, thickness: 0.25 }).success,
824
- true,
825
- )
826
- assert.equal(
827
- PluginGeomCreateBrepFromPatchArgs.safeParse({
828
- boundary,
829
- thickness: 0.25,
830
- options: { interiorPoints: [p(1, 1, 1)], side: "above" },
831
- }).success,
832
- true,
833
- )
834
- assert.equal(
835
- PluginGeomCreateBrepFromPatchArgs.safeParse({
836
- boundary: [
837
- { edge: "edge_1", brep: "brep_1", face: "face_1" },
838
- { edge: "edge_2", brep: "brep_2", face: "face_2" },
839
- { edge: "edge_3", brep: "brep_1", face: "face_3" },
840
- ],
841
- thickness: 0.25,
842
- options: { continuity: "tangent" },
843
- }).success,
844
- true,
845
- )
846
- for (const bad of [
847
- // tangent with a curve item, and with an edge item that names no face
848
- { boundary, thickness: 0.25, options: { continuity: "tangent" } },
849
- {
850
- boundary: [
851
- { edge: "edge_1", brep: "brep_1" },
852
- { edge: "edge_2", brep: "brep_1" },
853
- { edge: "edge_3", brep: "brep_1" },
854
- ],
855
- thickness: 0.25,
856
- options: { continuity: "tangent" },
857
- },
858
- { boundary: ["line_1", "line_2"], thickness: 0.25 },
859
- { boundary, thickness: 0 },
860
- {
861
- boundary: Array.from({ length: MAX_PATCH_BOUNDARY_CURVES + 1 }, () => "line_1"),
862
- thickness: 0.25,
863
- },
864
- {
865
- boundary,
866
- thickness: 0.25,
867
- options: {
868
- interiorPoints: Array.from({ length: MAX_PATCH_INTERIOR_POINTS + 1 }, () => p(0, 0, 0)),
869
- },
870
- },
871
- ]) {
872
- assert.equal(
873
- PluginGeomCreateBrepFromPatchArgs.safeParse(bad).success,
874
- false,
875
- JSON.stringify(bad),
876
- )
877
- }
878
-
879
- // brepFromSweepAlongCurve: 1..64 curve handles, and the creator's own rail.
880
- const sweep = { profile: "contour_1", path: ["spline_1"] }
881
- assert.equal(PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse(sweep).success, true)
882
- assert.equal(
883
- PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse({
884
- ...sweep,
885
- options: { orientation: "fixed", rail: "spline_2" },
886
- }).success,
887
- true,
888
- )
889
- for (const bad of [
890
- { ...sweep, path: [] },
891
- { ...sweep, path: Array.from({ length: MAX_PATH_CURVES + 1 }, () => "spline_1") },
892
- { ...sweep, path: [p(0, 0, 0)] },
893
- { ...sweep, options: { orientation: "banking" } },
894
- ]) {
895
- assert.equal(
896
- PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse(bad).success,
897
- false,
898
- JSON.stringify(bad),
899
- )
900
- }
901
-
902
- // The two owner-last members.
903
- assert.equal(
904
- PluginGeomCreateBrepsFromSplitByFaceArgs.safeParse({
905
- brep: "brep_1",
906
- toolFace: "face_1",
907
- toolBrep: "brep_2",
908
- }).success,
909
- true,
910
- )
911
- assert.equal(
912
- PluginGeomCreateBrepsFromSplitByFaceArgs.safeParse({
913
- brep: "brep_1",
914
- toolFace: "brep_2",
915
- toolBrep: "brep_2",
916
- }).success,
917
- false,
918
- )
919
- assert.equal(
920
- PluginGeomCreateCurvesFromProjectionArgs.safeParse({
921
- curve: "spline_1",
922
- face: "face_1",
923
- faceBrep: "brep_1",
924
- }).success,
925
- true,
926
- )
927
- assert.equal(
928
- PluginGeomCreateCurvesFromProjectionArgs.safeParse({
929
- curve: "circle_1",
930
- face: "face_1",
931
- faceBrep: "brep_1",
932
- }).success,
933
- false,
934
- )
935
-
936
- // The two face reads that need the owning solid for their coordinates.
937
- for (const args of [PluginGeomQueryFaceGetAreaArgs, PluginGeomQueryFaceGetCentroidArgs]) {
938
- assert.equal(args.safeParse({ face: "face_1", brep: "brep_1" }).success, true)
939
- assert.equal(args.safeParse({ face: "face_1", brep: "face_1" }).success, false)
940
- }
941
- })
942
-
943
- test("both record modules stay inside the one-shot read budget", () => {
944
- // The agent reads a one-shot module whole; 14,000 B is the ceiling asserted
945
- // over ONE_SHOT_MODULES in the agent runtime (readApiFullMaxChars). It was
946
- // 12,000 B until the grid and dimension handles took handles.d.ts past it on
947
- // their own; both sides moved in the same change.
948
- for (const name of ["handles", "massParameters", "massParameters.ops"]) {
949
- const bytes = readFileSync(new URL(`../dist/${name}.d.ts`, import.meta.url)).byteLength
950
- assert.ok(bytes < 14_000, `dist/${name}.d.ts is ${bytes} B, over the 14,000 B one-shot ceiling`)
951
- }
952
- })
1
+ // Node built-in test runner suite for the shared mass-parameter record schemas.
2
+ // Run: node --test test/
3
+ // Imports the BUILT package (dist) — run `pnpm build` first.
4
+ import test from "node:test"
5
+ import assert from "node:assert/strict"
6
+ import { readFileSync } from "node:fs"
7
+ import {
8
+ MassParametersRecord,
9
+ MassParametersPatch,
10
+ OperationRecord,
11
+ ContourRecord,
12
+ ProfileRecord,
13
+ CurveRecord,
14
+ EdgeSelector,
15
+ FaceSelector,
16
+ ParameterPatchValue,
17
+ SweepAlongCurveRecordOptions,
18
+ MAX_PROFILE_CURVES,
19
+ MAX_CONTOUR_HOLES,
20
+ MAX_SPLINE_THROUGH_POINTS,
21
+ MAX_SWEEP_PATH_POINTS,
22
+ MAX_LOFT_SECTIONS,
23
+ MAX_OPERATIONS,
24
+ MAX_OPERATION_EDGES,
25
+ MAX_OPERATION_FACES,
26
+ MAX_PATH_CURVES,
27
+ MAX_TWIST_LEVELS,
28
+ MAX_FILLET_LAW_SAMPLES,
29
+ MAX_PATCH_BOUNDARY_CURVES,
30
+ MAX_PATCH_INTERIOR_POINTS,
31
+ CurveHandle,
32
+ SplineHandle,
33
+ ArenaKind,
34
+ PluginHandlesReleaseAllArgs,
35
+ PluginGeomCreateBrepFromSweepArgs,
36
+ PluginGeomCreateBrepFromLoftArgs,
37
+ PluginGeomCreateBrepFromFilletArgs,
38
+ PluginGeomCreateBrepFromChamferArgs,
39
+ PluginGeomCreateBrepFromShellArgs,
40
+ PluginGeomCreateBrepFromExtrusionArgs,
41
+ PluginGeomCreateBrepFromPatchArgs,
42
+ PluginGeomCreateBrepFromSweepAlongCurveArgs,
43
+ PluginGeomCreateBrepsFromSplitByFaceArgs,
44
+ PluginGeomCreateCurvesFromProjectionArgs,
45
+ PluginGeomCreateSplineFromControlPointsArgs,
46
+ PluginDesignUpdateParametersArgs,
47
+ PluginGeomQueryFaceGetAreaArgs,
48
+ PluginGeomQueryFaceGetCentroidArgs,
49
+ } from "../dist/index.js"
50
+
51
+ const p = (x, y, z) => ({ x, y, z })
52
+
53
+ /** A square outer loop, no holes — the simplest legal contour record. */
54
+ const squareContour = {
55
+ outer: {
56
+ curves: [
57
+ { type: "line", start: p(0, 0, 0), end: p(4, 0, 0) },
58
+ { type: "line", start: p(4, 0, 0), end: p(4, 0, 4) },
59
+ { type: "line", start: p(4, 0, 4), end: p(0, 0, 4) },
60
+ { type: "line", start: p(0, 0, 4), end: p(0, 0, 0) },
61
+ ],
62
+ },
63
+ holes: [],
64
+ }
65
+
66
+ test("accepts one record of every kind", () => {
67
+ const records = [
68
+ { kind: "sphere", version: 1, values: { centre: p(0, 5, 0), radius: 5 } },
69
+ {
70
+ kind: "cylinder",
71
+ version: 1,
72
+ values: { base: p(0, 0, 0), axis: p(0, 1, 0), radius: 1, height: 4 },
73
+ },
74
+ {
75
+ kind: "cone",
76
+ version: 1,
77
+ values: { base: p(0, 0, 0), axis: p(0, 1, 0), baseRadius: 2, topRadius: 0, height: 3 },
78
+ },
79
+ {
80
+ kind: "torus",
81
+ version: 1,
82
+ values: { centre: p(0, 6, 0), axis: p(0, 1, 0), majorRadius: 5, minorRadius: 1 },
83
+ },
84
+ {
85
+ kind: "extrusion",
86
+ version: 1,
87
+ values: { contour: squareContour, direction: p(0, 1, 0), amount: 3 },
88
+ },
89
+ {
90
+ kind: "revolution",
91
+ version: 1,
92
+ values: {
93
+ contour: squareContour,
94
+ axisOrigin: p(0, 0, 0),
95
+ axisDirection: p(0, 1, 0),
96
+ angleInDegrees: 360,
97
+ },
98
+ },
99
+ {
100
+ kind: "loft",
101
+ version: 1,
102
+ values: { sections: [squareContour, squareContour], options: { compatibility: "auto" } },
103
+ },
104
+ {
105
+ kind: "sweep",
106
+ version: 1,
107
+ values: {
108
+ profile: squareContour,
109
+ path: [p(0, 0, 0), p(0, 3, 0)],
110
+ options: { transition: "round" },
111
+ },
112
+ },
113
+ ]
114
+ for (const record of records) {
115
+ const parsed = MassParametersRecord.safeParse(record)
116
+ assert.equal(parsed.success, true, `${record.kind}: ${JSON.stringify(parsed.error?.issues)}`)
117
+ assert.deepEqual(parsed.data, record)
118
+ }
119
+ })
120
+
121
+ test("both spline curve forms round-trip inside a contour", () => {
122
+ const contour = {
123
+ outer: {
124
+ curves: [
125
+ { type: "splineThrough", fitVersion: 1, throughPoints: [p(0, 0, 0), p(1, 0, 2), p(3, 0, 1)] },
126
+ {
127
+ type: "splinePoles",
128
+ degree: 3,
129
+ knots: [0, 1],
130
+ mults: [4, 4],
131
+ poles: [p(3, 0, 1), p(2, 0, 0), p(1, 0, -1), p(0, 0, 0)],
132
+ },
133
+ ],
134
+ },
135
+ holes: [],
136
+ }
137
+ assert.equal(ContourRecord.safeParse(contour).success, true)
138
+ })
139
+
140
+ test("rejects a record that is not the truth", () => {
141
+ // Unknown key — the host names it back to the caller, never silently drops it.
142
+ assert.equal(
143
+ MassParametersRecord.safeParse({
144
+ kind: "torus",
145
+ version: 1,
146
+ values: { centre: p(0, 0, 0), axis: p(0, 1, 0), majorRadius: 5, minorRadius: 1, radius: 5 },
147
+ }).success,
148
+ false,
149
+ )
150
+ // Unknown schema version — restore degrades the mass to non-parametric.
151
+ assert.equal(
152
+ MassParametersRecord.safeParse({
153
+ kind: "sphere",
154
+ version: 2,
155
+ values: { centre: p(0, 0, 0), radius: 5 },
156
+ }).success,
157
+ false,
158
+ )
159
+ // Non-finite / non-positive values.
160
+ for (const radius of [0, -1, Number.NaN, Number.POSITIVE_INFINITY]) {
161
+ assert.equal(
162
+ MassParametersRecord.safeParse({
163
+ kind: "sphere",
164
+ version: 1,
165
+ values: { centre: p(0, 0, 0), radius },
166
+ }).success,
167
+ false,
168
+ `radius ${radius} must be rejected`,
169
+ )
170
+ }
171
+ // A spline record with neither form's fields.
172
+ assert.equal(CurveRecord.safeParse({ type: "splineThrough", throughPoints: [] }).success, false)
173
+ })
174
+
175
+ test("array caps are contract, not truncation", () => {
176
+ const points = (n) => Array.from({ length: n }, (_, i) => p(i, 0, 0))
177
+ const spline = (n) => ({ type: "splineThrough", fitVersion: 1, throughPoints: points(n) })
178
+ assert.equal(CurveRecord.safeParse(spline(MAX_SPLINE_THROUGH_POINTS)).success, true)
179
+ assert.equal(CurveRecord.safeParse(spline(MAX_SPLINE_THROUGH_POINTS + 1)).success, false)
180
+
181
+ const sweep = (n) => ({
182
+ kind: "sweep",
183
+ version: 1,
184
+ values: { profile: squareContour, path: points(n) },
185
+ })
186
+ assert.equal(MassParametersRecord.safeParse(sweep(MAX_SWEEP_PATH_POINTS)).success, true)
187
+ assert.equal(MassParametersRecord.safeParse(sweep(MAX_SWEEP_PATH_POINTS + 1)).success, false)
188
+
189
+ const loft = (n) => ({
190
+ kind: "loft",
191
+ version: 1,
192
+ values: { sections: Array.from({ length: n }, () => squareContour) },
193
+ })
194
+ assert.equal(MassParametersRecord.safeParse(loft(MAX_LOFT_SECTIONS)).success, true)
195
+ assert.equal(MassParametersRecord.safeParse(loft(MAX_LOFT_SECTIONS + 1)).success, false)
196
+
197
+ // The caps bind the RECORD only. The creator args (shipped in 0.9.6 without
198
+ // any cap) keep accepting an over-cap call — the host builds it and mints no
199
+ // record (the recipe-less fallback). See "wire schemas keep 0.9.6 acceptance".
200
+ const sweepArgs = (n) => ({ profile: "contour_1", path: points(n) })
201
+ assert.equal(
202
+ PluginGeomCreateBrepFromSweepArgs.safeParse(sweepArgs(MAX_SWEEP_PATH_POINTS)).success,
203
+ true,
204
+ )
205
+ const loftArgs = (n) => ({
206
+ bottomContour: "contour_1",
207
+ topContour: "contour_2",
208
+ intermediateContours: Array.from({ length: n }, (_, i) => `contour_${i + 3}`),
209
+ })
210
+ assert.equal(
211
+ PluginGeomCreateBrepFromLoftArgs.safeParse(loftArgs(MAX_LOFT_SECTIONS - 2)).success,
212
+ true,
213
+ )
214
+
215
+ // A contour's own arrays are bounded too — a 20 000-curve loop or a
216
+ // 5 000-hole contour must not ride the save/collab payload as a record.
217
+ const loop = (n) => ({
218
+ curves: Array.from({ length: n }, (_, i) => ({
219
+ type: "line",
220
+ start: p(i, 0, 0),
221
+ end: p(i + 1, 0, 0),
222
+ })),
223
+ })
224
+ assert.equal(ProfileRecord.safeParse(loop(MAX_PROFILE_CURVES)).success, true)
225
+ assert.equal(ProfileRecord.safeParse(loop(MAX_PROFILE_CURVES + 1)).success, false)
226
+
227
+ const holed = (n) => ({
228
+ outer: squareContour.outer,
229
+ holes: Array.from({ length: n }, () => squareContour.outer),
230
+ })
231
+ assert.equal(ContourRecord.safeParse(holed(MAX_CONTOUR_HOLES)).success, true)
232
+ assert.equal(ContourRecord.safeParse(holed(MAX_CONTOUR_HOLES + 1)).success, false)
233
+ })
234
+
235
+ /** sphere → shell → fillet: the chained record of binding-design-4b §5.2. */
236
+ const chainedRecord = {
237
+ kind: "sphere",
238
+ version: 1,
239
+ values: { centre: p(0, 23.6, 0), radius: 23.622 },
240
+ operations: [
241
+ {
242
+ op: "shell",
243
+ openFaces: [{ index: 3, kind: "plane", edgeCount: 2 }],
244
+ thickness: 1.181,
245
+ input: { faceCount: 4, edgeCount: 6 },
246
+ },
247
+ {
248
+ op: "fillet",
249
+ edges: [
250
+ { index: 2, faces: [0, 3], curve: "arc" },
251
+ { index: 5, faces: [1, 3], curve: "arc" },
252
+ ],
253
+ radius: 0.787,
254
+ input: { faceCount: 8, edgeCount: 14 },
255
+ },
256
+ ],
257
+ }
258
+
259
+ test("a chained operations record round-trips", () => {
260
+ const parsed = MassParametersRecord.safeParse(chainedRecord)
261
+ assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
262
+ assert.deepEqual(parsed.data, chainedRecord)
263
+
264
+ // `operations` is additive and optional — a 4A-era record still parses.
265
+ const { operations, ...base } = chainedRecord
266
+ assert.equal(MassParametersRecord.safeParse(base).success, true)
267
+
268
+ // Every op kind on its own.
269
+ const chamfer = {
270
+ op: "chamfer",
271
+ edges: [{ index: 0, faces: [0, 1], curve: "line" }],
272
+ distance: 0.1,
273
+ input: { faceCount: 6, edgeCount: 12 },
274
+ }
275
+ const offset = { op: "offset", distance: -0.4, input: { faceCount: 4, edgeCount: 6 } }
276
+ for (const op of [operations[0], operations[1], chamfer, offset]) {
277
+ assert.deepEqual(OperationRecord.parse(op), op)
278
+ }
279
+ })
280
+
281
+ test("operation records reject what cannot be replayed", () => {
282
+ const counts = { faceCount: 4, edgeCount: 6 }
283
+ // A selector list is never empty — an op that selected nothing is not a record.
284
+ assert.equal(OperationRecord.safeParse({ op: "fillet", edges: [], radius: 1, input: counts }).success, false)
285
+ // Fractional / negative indices cannot address a face or an edge.
286
+ assert.equal(
287
+ OperationRecord.safeParse({
288
+ op: "fillet",
289
+ edges: [{ index: 1.5, faces: [0, 1], curve: "arc" }],
290
+ radius: 1,
291
+ input: counts,
292
+ }).success,
293
+ false,
294
+ )
295
+ assert.equal(
296
+ OperationRecord.safeParse({
297
+ op: "fillet",
298
+ edges: [{ index: 0, faces: [0, -1], curve: "arc" }],
299
+ radius: 1,
300
+ input: counts,
301
+ }).success,
302
+ false,
303
+ )
304
+ // A face pair is exactly two faces.
305
+ assert.equal(
306
+ OperationRecord.safeParse({
307
+ op: "fillet",
308
+ edges: [{ index: 0, faces: [0, 1, 2], curve: "arc" }],
309
+ radius: 1,
310
+ input: counts,
311
+ }).success,
312
+ false,
313
+ )
314
+ // Unknown surface / curve kinds, unknown op, and a zero offset.
315
+ assert.equal(
316
+ OperationRecord.safeParse({
317
+ op: "shell",
318
+ openFaces: [{ index: 0, kind: "nurbs", edgeCount: 2 }],
319
+ thickness: 1,
320
+ input: counts,
321
+ }).success,
322
+ false,
323
+ )
324
+ assert.equal(OperationRecord.safeParse({ op: "draft", angle: 3, input: counts }).success, false)
325
+ assert.equal(OperationRecord.safeParse({ op: "offset", distance: 0, input: counts }).success, false)
326
+ // Non-positive scalars.
327
+ for (const radius of [0, -1, Number.NaN]) {
328
+ assert.equal(
329
+ OperationRecord.safeParse({
330
+ op: "fillet",
331
+ edges: [{ index: 0, faces: [0, 1], curve: "arc" }],
332
+ radius,
333
+ input: counts,
334
+ }).success,
335
+ false,
336
+ `radius ${radius} must be rejected`,
337
+ )
338
+ }
339
+ })
340
+
341
+ test("operation caps are contract, not truncation", () => {
342
+ const counts = { faceCount: 4, edgeCount: 6 }
343
+ const fillet = (n) => ({
344
+ op: "fillet",
345
+ edges: Array.from({ length: n }, (_, i) => ({ index: i, faces: [0, 1], curve: "line" })),
346
+ radius: 1,
347
+ input: counts,
348
+ })
349
+ assert.equal(OperationRecord.safeParse(fillet(MAX_OPERATION_EDGES)).success, true)
350
+ assert.equal(OperationRecord.safeParse(fillet(MAX_OPERATION_EDGES + 1)).success, false)
351
+
352
+ const shell = (n) => ({
353
+ op: "shell",
354
+ openFaces: Array.from({ length: n }, (_, i) => ({ index: i, kind: "plane", edgeCount: 4 })),
355
+ thickness: 1,
356
+ input: counts,
357
+ })
358
+ assert.equal(OperationRecord.safeParse(shell(MAX_OPERATION_FACES)).success, true)
359
+ assert.equal(OperationRecord.safeParse(shell(MAX_OPERATION_FACES + 1)).success, false)
360
+
361
+ const chain = (n) => ({ ...chainedRecord, operations: Array.from({ length: n }, () => fillet(1)) })
362
+ assert.equal(MassParametersRecord.safeParse(chain(MAX_OPERATIONS)).success, true)
363
+ assert.equal(MassParametersRecord.safeParse(chain(MAX_OPERATIONS + 1)).success, false)
364
+
365
+ // The creator args are NOT capped (see "wire schemas keep 0.9.6 acceptance").
366
+ const edges = (n) => Array.from({ length: n }, (_, i) => `edge_${i}`)
367
+ const faces = (n) => Array.from({ length: n }, (_, i) => `face_${i}`)
368
+ const filletArgs = (n) => ({ brep: "brep_1", edges: edges(n), radius: 1 })
369
+ assert.equal(PluginGeomCreateBrepFromFilletArgs.safeParse(filletArgs(MAX_OPERATION_EDGES)).success, true)
370
+ const shellArgs = (n) => ({ brep: "brep_1", openFaces: faces(n), thickness: 1 })
371
+ assert.equal(PluginGeomCreateBrepFromShellArgs.safeParse(shellArgs(MAX_OPERATION_FACES)).success, true)
372
+ })
373
+
374
+ test("wire schemas keep 0.9.6 acceptance: record caps never reach the call", () => {
375
+ // The five inputs the external review (2026-09-06, #5) ran against the
376
+ // published 0.9.6 schemas: all accepted there, so all must stay accepted
377
+ // (feedback-no-breaking-api-changes — a shipped member never narrows). The
378
+ // record caps above still hold; the host answers an over-cap call with a
379
+ // solid and no record, never with VALIDATION.
380
+ const pts = (n) => Array.from({ length: n }, (_, i) => p(i, 0, 0))
381
+ const ids = (prefix, n) => Array.from({ length: n }, (_, i) => `${prefix}_${i}`)
382
+ const accepted = [
383
+ [
384
+ "loft with 31 intermediates",
385
+ PluginGeomCreateBrepFromLoftArgs.safeParse({
386
+ bottomContour: "contour_1",
387
+ topContour: "contour_2",
388
+ intermediateContours: ids("contour", MAX_LOFT_SECTIONS - 1),
389
+ }),
390
+ ],
391
+ [
392
+ "sweep with 513 path points",
393
+ PluginGeomCreateBrepFromSweepArgs.safeParse({
394
+ profile: "contour_1",
395
+ path: pts(MAX_SWEEP_PATH_POINTS + 1),
396
+ }),
397
+ ],
398
+ [
399
+ "fillet with 65 edges",
400
+ PluginGeomCreateBrepFromFilletArgs.safeParse({
401
+ brep: "brep_1",
402
+ edges: ids("edge", MAX_OPERATION_EDGES + 1),
403
+ radius: 1,
404
+ }),
405
+ ],
406
+ [
407
+ "chamfer with 65 edges",
408
+ PluginGeomCreateBrepFromChamferArgs.safeParse({
409
+ brep: "brep_1",
410
+ edges: ids("edge", MAX_OPERATION_EDGES + 1),
411
+ distance: 1,
412
+ }),
413
+ ],
414
+ [
415
+ "shell with 33 open faces",
416
+ PluginGeomCreateBrepFromShellArgs.safeParse({
417
+ brep: "brep_1",
418
+ openFaces: ids("face", MAX_OPERATION_FACES + 1),
419
+ thickness: 1,
420
+ }),
421
+ ],
422
+ ]
423
+ for (const [what, result] of accepted) {
424
+ assert.equal(result.success, true, `${what} must stay schema-valid: ${JSON.stringify(result.error?.issues)}`)
425
+ }
426
+ })
427
+
428
+ test("MassParametersPatch takes both a partial and a full record", () => {
429
+ // Partial forms: a scalar, a point, a whole array, and the empty op list.
430
+ for (const patch of [
431
+ { radius: 31.496 },
432
+ { centre: p(1, 2, 3) },
433
+ { path: [p(0, 0, 0), p(0, 3, 0)] },
434
+ { sections: [squareContour, squareContour] },
435
+ { contour: squareContour },
436
+ { options: { seamAlignment: "authored" } },
437
+ { options: { startScale: 1, endScale: 2, transition: { bevel: 0.2 } } },
438
+ { operations: chainedRecord.operations },
439
+ { operations: [] },
440
+ ]) {
441
+ const parsed = MassParametersPatch.safeParse(patch)
442
+ assert.equal(parsed.success, true, `${JSON.stringify(patch)}: ${JSON.stringify(parsed.error?.issues)}`)
443
+ assert.deepEqual(parsed.data, patch)
444
+ }
445
+
446
+ // Full-record form: read → edit → send the whole thing back.
447
+ const echoed = MassParametersPatch.safeParse(chainedRecord)
448
+ assert.equal(echoed.success, true)
449
+ assert.deepEqual(echoed.data, chainedRecord)
450
+
451
+ // …including the record edited to drop every operation. `{ operations: [] }`
452
+ // is the documented drop idiom, so the whole-record route must take it too
453
+ // (the host normalises the empty list to an absent key before storing).
454
+ const dropped = { ...chainedRecord, operations: [] }
455
+ const droppedParse = MassParametersPatch.safeParse(dropped)
456
+ assert.equal(droppedParse.success, true, JSON.stringify(droppedParse.error?.issues))
457
+ assert.deepEqual(droppedParse.data, dropped)
458
+ assert.equal(MassParametersRecord.safeParse(dropped).success, true)
459
+
460
+ // Values outside the closed union are refused at the wire, not written on.
461
+ for (const patch of [
462
+ { radius: "8" },
463
+ { radius: Number.POSITIVE_INFINITY },
464
+ { centre: { x: 0, y: 0 } },
465
+ { operations: [{ op: "fillet", edges: [], radius: 1, input: { faceCount: 4, edgeCount: 6 } }] },
466
+ { nested: { deeply: { wrong: true } } },
467
+ ]) {
468
+ assert.equal(MassParametersPatch.safeParse(patch).success, false, JSON.stringify(patch))
469
+ }
470
+ })
471
+
472
+ test("the 4B call args parse", () => {
473
+ assert.equal(
474
+ PluginDesignUpdateParametersArgs.safeParse({ component: "component_1", patch: { radius: 8 } })
475
+ .success,
476
+ true,
477
+ )
478
+ assert.equal(
479
+ PluginDesignUpdateParametersArgs.safeParse({ component: "component_1", patch: { radius: null } })
480
+ .success,
481
+ false,
482
+ )
483
+
484
+ const poles = (n) => Array.from({ length: n }, (_, i) => `vec3_${i}`)
485
+ assert.equal(
486
+ PluginGeomCreateSplineFromControlPointsArgs.safeParse({ controlPoints: poles(4) }).success,
487
+ true,
488
+ )
489
+ assert.equal(
490
+ PluginGeomCreateSplineFromControlPointsArgs.safeParse({
491
+ controlPoints: poles(4),
492
+ options: { degree: 2 },
493
+ }).success,
494
+ true,
495
+ )
496
+ // degree is 2 or 3 — 1 is a polyline, 4+ is not offered.
497
+ for (const degree of [1, 4, 2.5]) {
498
+ assert.equal(
499
+ PluginGeomCreateSplineFromControlPointsArgs.safeParse({
500
+ controlPoints: poles(4),
501
+ options: { degree },
502
+ }).success,
503
+ false,
504
+ `degree ${degree} must be rejected`,
505
+ )
506
+ }
507
+ assert.equal(
508
+ PluginGeomCreateSplineFromControlPointsArgs.safeParse({ controlPoints: poles(2) }).success,
509
+ false,
510
+ )
511
+ })
512
+
513
+ test("CurveHandle accepts a spline handle", () => {
514
+ assert.equal(SplineHandle.parse("spline_1").id, "spline_1")
515
+ for (const id of ["line_1", "arc_1", "spline_1"]) {
516
+ assert.equal(CurveHandle.parse(id).id, id)
517
+ }
518
+ assert.equal(CurveHandle.safeParse("circle_1").success, false)
519
+ })
520
+
521
+ test("a spline handle is releasable like every other value handle", () => {
522
+ // ArenaKind types release / endScope(retain) and validates releaseAll's kind.
523
+ assert.equal(ArenaKind.safeParse("spline").success, true)
524
+ assert.equal(PluginHandlesReleaseAllArgs.safeParse({ kind: "spline" }).success, true)
525
+ })
526
+
527
+ test("CurveHandle keeps the union type that 0.9.13 shipped, so .options still compiles", () => {
528
+ const dts = readFileSync(new URL("../dist/handles.d.ts", import.meta.url), "utf8")
529
+ assert.ok(/declare const CurveHandle: z\.ZodUnion</.test(dts), "CurveHandle is not declared as a ZodUnion")
530
+ assert.equal(CurveHandle.options.length, 3)
531
+ })
532
+
533
+ // ── Freeform surfaces, sweeps, patches and variable blends ──────────────────
534
+
535
+ const line = (a, b) => ({ type: "line", start: a, end: b })
536
+ const topology = { faceCount: 6, edgeCount: 12 }
537
+ const edgeSel = { index: 3, faces: [0, 1], curve: "spline" }
538
+
539
+ test("a sweep-along-a-curve record round-trips, and its path is curves", () => {
540
+ const record = {
541
+ kind: "sweepAlongCurve",
542
+ version: 1,
543
+ values: {
544
+ profile: squareContour,
545
+ path: [
546
+ line(p(0, 0, 0), p(0, 10, 0)),
547
+ {
548
+ type: "arc",
549
+ start: p(0, 10, 0),
550
+ end: p(5, 15, 0),
551
+ centre: p(5, 10, 0),
552
+ axis: p(0, 0, 1),
553
+ },
554
+ ],
555
+ options: { orientation: "upright" },
556
+ },
557
+ }
558
+ const parsed = MassParametersRecord.safeParse(record)
559
+ assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
560
+ assert.deepEqual(parsed.data, record)
561
+
562
+ // The options bag is the RECORD's: pure JSON, no rail handle.
563
+ assert.equal(SweepAlongCurveRecordOptions.safeParse({ orientation: "fixed" }).success, true)
564
+ assert.equal(SweepAlongCurveRecordOptions.safeParse({ orientation: "banking" }).success, false)
565
+ assert.equal(SweepAlongCurveRecordOptions.safeParse({ rail: "line_1" }).success, false)
566
+
567
+ // Caps and vocabulary are contract: 1..64 curves, no XYZ path.
568
+ const withPath = (path) => ({ ...record, values: { ...record.values, path } })
569
+ assert.equal(
570
+ MassParametersRecord.safeParse(
571
+ withPath(Array.from({ length: MAX_PATH_CURVES }, () => line(p(0, 0, 0), p(1, 0, 0)))),
572
+ ).success,
573
+ true,
574
+ )
575
+ assert.equal(
576
+ MassParametersRecord.safeParse(
577
+ withPath(Array.from({ length: MAX_PATH_CURVES + 1 }, () => line(p(0, 0, 0), p(1, 0, 0)))),
578
+ ).success,
579
+ false,
580
+ )
581
+ assert.equal(MassParametersRecord.safeParse(withPath([])).success, false)
582
+ assert.equal(MassParametersRecord.safeParse(withPath([p(0, 0, 0), p(1, 0, 0)])).success, false)
583
+ // The shipped polyline `sweep` kind is untouched — its path is still points.
584
+ assert.equal(
585
+ MassParametersRecord.safeParse({
586
+ kind: "sweep",
587
+ version: 1,
588
+ values: { profile: squareContour, path: [p(0, 0, 0), p(0, 3, 0)] },
589
+ }).success,
590
+ true,
591
+ )
592
+ })
593
+
594
+ test("an extrusion record carries twist, taper and levels", () => {
595
+ const straight = {
596
+ kind: "extrusion",
597
+ version: 1,
598
+ values: { contour: squareContour, direction: p(0, 1, 0), amount: 40 },
599
+ }
600
+ const twisted = {
601
+ ...straight,
602
+ values: { ...straight.values, twistInDegrees: 90, topScale: 0.6, levels: 6 },
603
+ }
604
+ const parsed = MassParametersRecord.safeParse(twisted)
605
+ assert.equal(parsed.success, true, JSON.stringify(parsed.error?.issues))
606
+ assert.deepEqual(parsed.data, twisted)
607
+ // Absent keys are still the shipped straight extrusion, byte for byte.
608
+ assert.deepEqual(MassParametersRecord.parse(straight), straight)
609
+
610
+ const values = (extra) => ({ ...straight, values: { ...straight.values, ...extra } })
611
+ assert.equal(MassParametersRecord.safeParse(values({ levels: MAX_TWIST_LEVELS })).success, true)
612
+ assert.equal(
613
+ MassParametersRecord.safeParse(values({ levels: MAX_TWIST_LEVELS + 1 })).success,
614
+ false,
615
+ )
616
+ assert.equal(MassParametersRecord.safeParse(values({ levels: 1 })).success, false)
617
+ assert.equal(MassParametersRecord.safeParse(values({ topScale: -1 })).success, false)
618
+ assert.equal(MassParametersRecord.safeParse(values({ twistInDegrees: "90" })).success, false)
619
+ })
620
+
621
+ test("a loft record chooses its smoothing", () => {
622
+ const record = {
623
+ kind: "loft",
624
+ version: 1,
625
+ values: { sections: [squareContour, squareContour], options: { smoothing: "smooth" } },
626
+ }
627
+ assert.deepEqual(MassParametersRecord.parse(record), record)
628
+ assert.equal(
629
+ MassParametersRecord.safeParse({
630
+ ...record,
631
+ values: { ...record.values, options: { smoothing: "curvy" } },
632
+ }).success,
633
+ false,
634
+ )
635
+ })
636
+
637
+ test("selectors carry an optional normalised position", () => {
638
+ const face = { index: 2, kind: "spline", edgeCount: 4, at: { x: 0.13, y: 1, z: 0.87 } }
639
+ assert.deepEqual(FaceSelector.parse(face), face)
640
+ const edge = { ...edgeSel, at: { x: 0.5, y: 0, z: 0.5 } }
641
+ assert.deepEqual(EdgeSelector.parse(edge), edge)
642
+ // A selector recorded before `at` existed still parses unchanged — the key
643
+ // only widens.
644
+ assert.deepEqual(EdgeSelector.parse(edgeSel), edgeSel)
645
+ assert.equal(FaceSelector.safeParse({ ...face, at: { x: 0, y: 0 } }).success, false)
646
+ assert.equal(EdgeSelector.safeParse({ ...edgeSel, at: [0, 0, 0] }).success, false)
647
+ })
648
+
649
+ test("a variable fillet operation names one edge and one law", () => {
650
+ const constant = { op: "fillet", edges: [edgeSel], radius: 0.5, input: topology }
651
+ const tapered = { ...constant, endRadius: 1.5 }
652
+ const sampled = {
653
+ ...constant,
654
+ radii: [
655
+ { position: 0, radius: 0.5 },
656
+ { position: 0.5, radius: 2 },
657
+ { position: 1, radius: 0.5 },
658
+ ],
659
+ }
660
+ assert.deepEqual(OperationRecord.parse(constant), constant)
661
+ assert.deepEqual(OperationRecord.parse(tapered), tapered)
662
+ assert.deepEqual(OperationRecord.parse(sampled), sampled)
663
+
664
+ // Both laws at once, more than one edge, a table that does not run forward,
665
+ // a single row, a position outside 0..1, and an over-long table are refused.
666
+ const twoEdges = [edgeSel, { ...edgeSel, index: 4 }]
667
+ for (const operation of [
668
+ { ...tapered, radii: sampled.radii },
669
+ { ...tapered, edges: twoEdges },
670
+ { ...sampled, edges: twoEdges },
671
+ {
672
+ ...constant,
673
+ radii: [
674
+ { position: 0, radius: 1 },
675
+ { position: 0, radius: 2 },
676
+ ],
677
+ },
678
+ { ...constant, radii: [{ position: 0, radius: 1 }] },
679
+ {
680
+ ...constant,
681
+ radii: [
682
+ { position: 0, radius: 1 },
683
+ { position: 1.5, radius: 2 },
684
+ ],
685
+ },
686
+ {
687
+ ...constant,
688
+ radii: Array.from({ length: MAX_FILLET_LAW_SAMPLES + 1 }, (_, i) => ({
689
+ position: i / MAX_FILLET_LAW_SAMPLES,
690
+ radius: 1,
691
+ })),
692
+ },
693
+ ]) {
694
+ assert.equal(OperationRecord.safeParse(operation).success, false, JSON.stringify(operation))
695
+ }
696
+ // A full table of the maximum length still parses.
697
+ assert.equal(
698
+ OperationRecord.safeParse({
699
+ ...constant,
700
+ radii: Array.from({ length: MAX_FILLET_LAW_SAMPLES }, (_, i) => ({
701
+ position: i / (MAX_FILLET_LAW_SAMPLES - 1),
702
+ radius: 1,
703
+ })),
704
+ }).success,
705
+ true,
706
+ )
707
+ })
708
+
709
+ test("the patch union widens to the new option bag and curve paths", () => {
710
+ assert.equal(ParameterPatchValue.safeParse({ orientation: "frenet" }).success, true)
711
+ assert.equal(ParameterPatchValue.safeParse([line(p(0, 0, 0), p(1, 0, 0))]).success, true)
712
+ assert.equal(ParameterPatchValue.safeParse({ smoothing: "smooth" }).success, true)
713
+ assert.equal(ParameterPatchValue.safeParse(90).success, true)
714
+ // A radius law is a key INSIDE an operation, never a top-level `values` key.
715
+ assert.equal(ParameterPatchValue.safeParse([{ position: 0, radius: 1 }]).success, false)
716
+
717
+ assert.equal(
718
+ MassParametersPatch.safeParse({
719
+ path: [line(p(0, 0, 0), p(0, 10, 0))],
720
+ options: { orientation: "fixed" },
721
+ }).success,
722
+ true,
723
+ )
724
+ assert.equal(MassParametersPatch.safeParse({ twistInDegrees: 120, topScale: 0.5 }).success, true)
725
+ assert.equal(
726
+ MassParametersPatch.safeParse({
727
+ operations: [{ op: "fillet", edges: [edgeSel], radius: 0.5, endRadius: 2, input: topology }],
728
+ }).success,
729
+ true,
730
+ )
731
+ assert.equal(MassParametersPatch.safeParse({ orientation: "sideways" }).success, false)
732
+ })
733
+
734
+ test("the freeform call args parse", () => {
735
+ // brepFromExtrusion keeps its three-positional form and gains a trailing bag.
736
+ const extrusion = { contour: "contour_1", direction: p(0, 1, 0), amount: 40 }
737
+ assert.equal(PluginGeomCreateBrepFromExtrusionArgs.safeParse(extrusion).success, true)
738
+ assert.equal(
739
+ PluginGeomCreateBrepFromExtrusionArgs.safeParse({
740
+ ...extrusion,
741
+ options: { twistInDegrees: 90, topScale: 0.6, levels: 6 },
742
+ }).success,
743
+ true,
744
+ )
745
+ assert.equal(
746
+ PluginGeomCreateBrepFromExtrusionArgs.safeParse({ ...extrusion, options: { levels: 1 } })
747
+ .success,
748
+ false,
749
+ )
750
+
751
+ // brepFromLoft gains smoothing beside the two shipped switches.
752
+ const loft = { bottomContour: "contour_1", topContour: "contour_2" }
753
+ assert.equal(PluginGeomCreateBrepFromLoftArgs.safeParse(loft).success, true)
754
+ assert.equal(
755
+ PluginGeomCreateBrepFromLoftArgs.safeParse({ ...loft, options: { smoothing: "smooth" } })
756
+ .success,
757
+ true,
758
+ )
759
+ assert.equal(
760
+ PluginGeomCreateBrepFromLoftArgs.safeParse({ ...loft, options: { smoothing: "silky" } })
761
+ .success,
762
+ false,
763
+ )
764
+
765
+ // brepFromFillet: the shipped three-argument call is untouched; a variable
766
+ // blend takes exactly one edge and exactly one law.
767
+ const fillet = { brep: "brep_1", edges: ["edge_1"], radius: 0.5 }
768
+ assert.equal(PluginGeomCreateBrepFromFilletArgs.safeParse(fillet).success, true)
769
+ assert.equal(
770
+ PluginGeomCreateBrepFromFilletArgs.safeParse({ ...fillet, edges: ["edge_1", "edge_2"] })
771
+ .success,
772
+ true,
773
+ )
774
+ assert.equal(
775
+ PluginGeomCreateBrepFromFilletArgs.safeParse({ ...fillet, options: { endRadius: 2 } }).success,
776
+ true,
777
+ )
778
+ assert.equal(
779
+ PluginGeomCreateBrepFromFilletArgs.safeParse({
780
+ ...fillet,
781
+ options: {
782
+ radii: [
783
+ { position: 0, radius: 0.5 },
784
+ { position: 1, radius: 2 },
785
+ ],
786
+ },
787
+ }).success,
788
+ true,
789
+ )
790
+ for (const bad of [
791
+ { ...fillet, edges: ["edge_1", "edge_2"], options: { endRadius: 2 } },
792
+ {
793
+ ...fillet,
794
+ options: {
795
+ endRadius: 2,
796
+ radii: [
797
+ { position: 0, radius: 1 },
798
+ { position: 1, radius: 2 },
799
+ ],
800
+ },
801
+ },
802
+ {
803
+ ...fillet,
804
+ options: {
805
+ radii: [
806
+ { position: 1, radius: 1 },
807
+ { position: 0, radius: 2 },
808
+ ],
809
+ },
810
+ },
811
+ { ...fillet, options: { radii: [{ position: 0, radius: 1 }] } },
812
+ ]) {
813
+ assert.equal(
814
+ PluginGeomCreateBrepFromFilletArgs.safeParse(bad).success,
815
+ false,
816
+ JSON.stringify(bad),
817
+ )
818
+ }
819
+
820
+ // brepFromPatch: curves, owned edges, and the tangent rule.
821
+ const boundary = ["line_1", "arc_1", { edge: "edge_1", brep: "brep_1" }]
822
+ assert.equal(
823
+ PluginGeomCreateBrepFromPatchArgs.safeParse({ boundary, thickness: 0.25 }).success,
824
+ true,
825
+ )
826
+ assert.equal(
827
+ PluginGeomCreateBrepFromPatchArgs.safeParse({
828
+ boundary,
829
+ thickness: 0.25,
830
+ options: { interiorPoints: [p(1, 1, 1)], side: "above" },
831
+ }).success,
832
+ true,
833
+ )
834
+ assert.equal(
835
+ PluginGeomCreateBrepFromPatchArgs.safeParse({
836
+ boundary: [
837
+ { edge: "edge_1", brep: "brep_1", face: "face_1" },
838
+ { edge: "edge_2", brep: "brep_2", face: "face_2" },
839
+ { edge: "edge_3", brep: "brep_1", face: "face_3" },
840
+ ],
841
+ thickness: 0.25,
842
+ options: { continuity: "tangent" },
843
+ }).success,
844
+ true,
845
+ )
846
+ for (const bad of [
847
+ // tangent with a curve item, and with an edge item that names no face
848
+ { boundary, thickness: 0.25, options: { continuity: "tangent" } },
849
+ {
850
+ boundary: [
851
+ { edge: "edge_1", brep: "brep_1" },
852
+ { edge: "edge_2", brep: "brep_1" },
853
+ { edge: "edge_3", brep: "brep_1" },
854
+ ],
855
+ thickness: 0.25,
856
+ options: { continuity: "tangent" },
857
+ },
858
+ { boundary: ["line_1", "line_2"], thickness: 0.25 },
859
+ { boundary, thickness: 0 },
860
+ {
861
+ boundary: Array.from({ length: MAX_PATCH_BOUNDARY_CURVES + 1 }, () => "line_1"),
862
+ thickness: 0.25,
863
+ },
864
+ {
865
+ boundary,
866
+ thickness: 0.25,
867
+ options: {
868
+ interiorPoints: Array.from({ length: MAX_PATCH_INTERIOR_POINTS + 1 }, () => p(0, 0, 0)),
869
+ },
870
+ },
871
+ ]) {
872
+ assert.equal(
873
+ PluginGeomCreateBrepFromPatchArgs.safeParse(bad).success,
874
+ false,
875
+ JSON.stringify(bad),
876
+ )
877
+ }
878
+
879
+ // brepFromSweepAlongCurve: 1..64 curve handles, and the creator's own rail.
880
+ const sweep = { profile: "contour_1", path: ["spline_1"] }
881
+ assert.equal(PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse(sweep).success, true)
882
+ assert.equal(
883
+ PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse({
884
+ ...sweep,
885
+ options: { orientation: "fixed", rail: "spline_2" },
886
+ }).success,
887
+ true,
888
+ )
889
+ for (const bad of [
890
+ { ...sweep, path: [] },
891
+ { ...sweep, path: Array.from({ length: MAX_PATH_CURVES + 1 }, () => "spline_1") },
892
+ { ...sweep, path: [p(0, 0, 0)] },
893
+ { ...sweep, options: { orientation: "banking" } },
894
+ ]) {
895
+ assert.equal(
896
+ PluginGeomCreateBrepFromSweepAlongCurveArgs.safeParse(bad).success,
897
+ false,
898
+ JSON.stringify(bad),
899
+ )
900
+ }
901
+
902
+ // The two owner-last members.
903
+ assert.equal(
904
+ PluginGeomCreateBrepsFromSplitByFaceArgs.safeParse({
905
+ brep: "brep_1",
906
+ toolFace: "face_1",
907
+ toolBrep: "brep_2",
908
+ }).success,
909
+ true,
910
+ )
911
+ assert.equal(
912
+ PluginGeomCreateBrepsFromSplitByFaceArgs.safeParse({
913
+ brep: "brep_1",
914
+ toolFace: "brep_2",
915
+ toolBrep: "brep_2",
916
+ }).success,
917
+ false,
918
+ )
919
+ assert.equal(
920
+ PluginGeomCreateCurvesFromProjectionArgs.safeParse({
921
+ curve: "spline_1",
922
+ face: "face_1",
923
+ faceBrep: "brep_1",
924
+ }).success,
925
+ true,
926
+ )
927
+ assert.equal(
928
+ PluginGeomCreateCurvesFromProjectionArgs.safeParse({
929
+ curve: "circle_1",
930
+ face: "face_1",
931
+ faceBrep: "brep_1",
932
+ }).success,
933
+ false,
934
+ )
935
+
936
+ // The two face reads that need the owning solid for their coordinates.
937
+ for (const args of [PluginGeomQueryFaceGetAreaArgs, PluginGeomQueryFaceGetCentroidArgs]) {
938
+ assert.equal(args.safeParse({ face: "face_1", brep: "brep_1" }).success, true)
939
+ assert.equal(args.safeParse({ face: "face_1", brep: "face_1" }).success, false)
940
+ }
941
+ })
942
+
943
+ test("both record modules stay inside the one-shot read budget", () => {
944
+ // The agent reads a one-shot module whole; 14,000 B is the ceiling asserted
945
+ // over ONE_SHOT_MODULES in the agent runtime (readApiFullMaxChars). It was
946
+ // 12,000 B until the grid and dimension handles took handles.d.ts past it on
947
+ // their own; both sides moved in the same change.
948
+ for (const name of ["handles", "massParameters", "massParameters.ops"]) {
949
+ const bytes = readFileSync(new URL(`../dist/${name}.d.ts`, import.meta.url)).byteLength
950
+ assert.ok(bytes < 14_000, `dist/${name}.d.ts is ${bytes} B, over the 14,000 B one-shot ceiling`)
951
+ }
952
+ })