@things-factory/board-ai 10.0.0-beta.108

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 (329) hide show
  1. package/PLAN-styling-tools.md +1253 -0
  2. package/client/components/board-ai-chat.test.ts +149 -0
  3. package/client/components/board-ai-chat.ts +2574 -0
  4. package/client/components/chat-echo-dedup.test.ts +116 -0
  5. package/client/components/chat-echo-dedup.ts +89 -0
  6. package/client/components/chat-input-builder.ts +47 -0
  7. package/client/components/default-catalog-entries.test.ts +59 -0
  8. package/client/components/default-catalog-entries.ts +100 -0
  9. package/client/components/default-slash-templates.test.ts +55 -0
  10. package/client/components/default-slash-templates.ts +129 -0
  11. package/client/components/markdown.test.ts +518 -0
  12. package/client/components/markdown.ts +508 -0
  13. package/client/components/mention-popup-helpers.ts +433 -0
  14. package/client/components/mention-popup.test.ts +588 -0
  15. package/client/components/mention-popup.ts +238 -0
  16. package/client/index.ts +11 -0
  17. package/client/tsconfig.json +13 -0
  18. package/client/utils/board-edit-patch.ts +374 -0
  19. package/config/config.development.js +43 -0
  20. package/config/config.production.js +15 -0
  21. package/dist-client/client/components/board-ai-chat.d.ts +277 -0
  22. package/dist-client/client/components/board-ai-chat.js +2444 -0
  23. package/dist-client/client/components/board-ai-chat.js.map +1 -0
  24. package/dist-client/client/components/board-ai-chat.test.d.ts +1 -0
  25. package/dist-client/client/components/board-ai-chat.test.js +138 -0
  26. package/dist-client/client/components/board-ai-chat.test.js.map +1 -0
  27. package/dist-client/client/components/chat-echo-dedup.d.ts +45 -0
  28. package/dist-client/client/components/chat-echo-dedup.js +58 -0
  29. package/dist-client/client/components/chat-echo-dedup.js.map +1 -0
  30. package/dist-client/client/components/chat-echo-dedup.test.d.ts +1 -0
  31. package/dist-client/client/components/chat-echo-dedup.test.js +101 -0
  32. package/dist-client/client/components/chat-echo-dedup.test.js.map +1 -0
  33. package/dist-client/client/components/chat-input-builder.d.ts +39 -0
  34. package/dist-client/client/components/chat-input-builder.js +26 -0
  35. package/dist-client/client/components/chat-input-builder.js.map +1 -0
  36. package/dist-client/client/components/default-catalog-entries.d.ts +15 -0
  37. package/dist-client/client/components/default-catalog-entries.js +86 -0
  38. package/dist-client/client/components/default-catalog-entries.js.map +1 -0
  39. package/dist-client/client/components/default-catalog-entries.test.d.ts +1 -0
  40. package/dist-client/client/components/default-catalog-entries.test.js +52 -0
  41. package/dist-client/client/components/default-catalog-entries.test.js.map +1 -0
  42. package/dist-client/client/components/default-slash-templates.d.ts +17 -0
  43. package/dist-client/client/components/default-slash-templates.js +113 -0
  44. package/dist-client/client/components/default-slash-templates.js.map +1 -0
  45. package/dist-client/client/components/default-slash-templates.test.d.ts +1 -0
  46. package/dist-client/client/components/default-slash-templates.test.js +51 -0
  47. package/dist-client/client/components/default-slash-templates.test.js.map +1 -0
  48. package/dist-client/client/components/markdown.d.ts +60 -0
  49. package/dist-client/client/components/markdown.js +422 -0
  50. package/dist-client/client/components/markdown.js.map +1 -0
  51. package/dist-client/client/components/markdown.test.d.ts +1 -0
  52. package/dist-client/client/components/markdown.test.js +418 -0
  53. package/dist-client/client/components/markdown.test.js.map +1 -0
  54. package/dist-client/client/components/mention-popup-helpers.d.ts +165 -0
  55. package/dist-client/client/components/mention-popup-helpers.js +330 -0
  56. package/dist-client/client/components/mention-popup-helpers.js.map +1 -0
  57. package/dist-client/client/components/mention-popup.d.ts +35 -0
  58. package/dist-client/client/components/mention-popup.js +223 -0
  59. package/dist-client/client/components/mention-popup.js.map +1 -0
  60. package/dist-client/client/components/mention-popup.test.d.ts +1 -0
  61. package/dist-client/client/components/mention-popup.test.js +474 -0
  62. package/dist-client/client/components/mention-popup.test.js.map +1 -0
  63. package/dist-client/client/index.d.ts +11 -0
  64. package/dist-client/client/index.js +12 -0
  65. package/dist-client/client/index.js.map +1 -0
  66. package/dist-client/client/utils/board-edit-patch.d.ts +157 -0
  67. package/dist-client/client/utils/board-edit-patch.js +284 -0
  68. package/dist-client/client/utils/board-edit-patch.js.map +1 -0
  69. package/dist-client/server/service/agentic-loop.d.ts +87 -0
  70. package/dist-client/server/service/agentic-loop.js +276 -0
  71. package/dist-client/server/service/agentic-loop.js.map +1 -0
  72. package/dist-client/server/service/assistant.d.ts +84 -0
  73. package/dist-client/server/service/assistant.js +2220 -0
  74. package/dist-client/server/service/assistant.js.map +1 -0
  75. package/dist-client/server/service/catalog-resolver.d.ts +18 -0
  76. package/dist-client/server/service/catalog-resolver.js +93 -0
  77. package/dist-client/server/service/catalog-resolver.js.map +1 -0
  78. package/dist-client/server/service/component-tree.d.ts +68 -0
  79. package/dist-client/server/service/component-tree.js +68 -0
  80. package/dist-client/server/service/component-tree.js.map +1 -0
  81. package/dist-client/server/service/mention-resolver.d.ts +96 -0
  82. package/dist-client/server/service/mention-resolver.js +172 -0
  83. package/dist-client/server/service/mention-resolver.js.map +1 -0
  84. package/dist-client/server/service/prefab-registry.d.ts +73 -0
  85. package/dist-client/server/service/prefab-registry.js +252 -0
  86. package/dist-client/server/service/prefab-registry.js.map +1 -0
  87. package/dist-client/server/service/styling/clear-tools.d.ts +15 -0
  88. package/dist-client/server/service/styling/clear-tools.js +50 -0
  89. package/dist-client/server/service/styling/clear-tools.js.map +1 -0
  90. package/dist-client/server/service/styling/color-utils.d.ts +43 -0
  91. package/dist-client/server/service/styling/color-utils.js +205 -0
  92. package/dist-client/server/service/styling/color-utils.js.map +1 -0
  93. package/dist-client/server/service/styling/copy-tools.d.ts +27 -0
  94. package/dist-client/server/service/styling/copy-tools.js +78 -0
  95. package/dist-client/server/service/styling/copy-tools.js.map +1 -0
  96. package/dist-client/server/service/styling/effect-tools.d.ts +45 -0
  97. package/dist-client/server/service/styling/effect-tools.js +85 -0
  98. package/dist-client/server/service/styling/effect-tools.js.map +1 -0
  99. package/dist-client/server/service/styling/fill-tools.d.ts +60 -0
  100. package/dist-client/server/service/styling/fill-tools.js +226 -0
  101. package/dist-client/server/service/styling/fill-tools.js.map +1 -0
  102. package/dist-client/server/service/styling/material-tools.d.ts +32 -0
  103. package/dist-client/server/service/styling/material-tools.js +88 -0
  104. package/dist-client/server/service/styling/material-tools.js.map +1 -0
  105. package/dist-client/server/service/styling/read-tools.d.ts +34 -0
  106. package/dist-client/server/service/styling/read-tools.js +42 -0
  107. package/dist-client/server/service/styling/read-tools.js.map +1 -0
  108. package/dist-client/server/service/styling/registry.d.ts +91 -0
  109. package/dist-client/server/service/styling/registry.js +345 -0
  110. package/dist-client/server/service/styling/registry.js.map +1 -0
  111. package/dist-client/server/service/styling/stroke-tools.d.ts +35 -0
  112. package/dist-client/server/service/styling/stroke-tools.js +94 -0
  113. package/dist-client/server/service/styling/stroke-tools.js.map +1 -0
  114. package/dist-client/server/service/styling/text-tools.d.ts +32 -0
  115. package/dist-client/server/service/styling/text-tools.js +101 -0
  116. package/dist-client/server/service/styling/text-tools.js.map +1 -0
  117. package/dist-client/server/service/types.d.ts +265 -0
  118. package/dist-client/server/service/types.js +2 -0
  119. package/dist-client/server/service/types.js.map +1 -0
  120. package/dist-client/server/service/validation/board-model-schema.d.ts +8311 -0
  121. package/dist-client/server/service/validation/board-model-schema.js +402 -0
  122. package/dist-client/server/service/validation/board-model-schema.js.map +1 -0
  123. package/dist-client/server/service/validation/tool-validation.d.ts +31 -0
  124. package/dist-client/server/service/validation/tool-validation.js +193 -0
  125. package/dist-client/server/service/validation/tool-validation.js.map +1 -0
  126. package/dist-client/tsconfig.tsbuildinfo +1 -0
  127. package/dist-server/index.d.ts +23 -0
  128. package/dist-server/index.js +27 -0
  129. package/dist-server/index.js.map +1 -0
  130. package/dist-server/service/agentic-loop.d.ts +87 -0
  131. package/dist-server/service/agentic-loop.js +280 -0
  132. package/dist-server/service/agentic-loop.js.map +1 -0
  133. package/dist-server/service/apply-patch.d.ts +47 -0
  134. package/dist-server/service/apply-patch.js +369 -0
  135. package/dist-server/service/apply-patch.js.map +1 -0
  136. package/dist-server/service/assistant.d.ts +84 -0
  137. package/dist-server/service/assistant.js +2237 -0
  138. package/dist-server/service/assistant.js.map +1 -0
  139. package/dist-server/service/board-ai-resolver.d.ts +54 -0
  140. package/dist-server/service/board-ai-resolver.js +409 -0
  141. package/dist-server/service/board-ai-resolver.js.map +1 -0
  142. package/dist-server/service/catalog-resolver.d.ts +18 -0
  143. package/dist-server/service/catalog-resolver.js +98 -0
  144. package/dist-server/service/catalog-resolver.js.map +1 -0
  145. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  146. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  147. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  148. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  149. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  150. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  151. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  152. package/dist-server/service/chat-message/chat-message-subscription.js +53 -0
  153. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  154. package/dist-server/service/chat-message/chat-message.d.ts +56 -0
  155. package/dist-server/service/chat-message/chat-message.js +149 -0
  156. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  157. package/dist-server/service/chat-message/index.d.ts +5 -0
  158. package/dist-server/service/chat-message/index.js +12 -0
  159. package/dist-server/service/chat-message/index.js.map +1 -0
  160. package/dist-server/service/chat-session/chat-session.d.ts +31 -0
  161. package/dist-server/service/chat-session/chat-session.js +131 -0
  162. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  163. package/dist-server/service/chat-session/index.d.ts +3 -0
  164. package/dist-server/service/chat-session/index.js +7 -0
  165. package/dist-server/service/chat-session/index.js.map +1 -0
  166. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +30 -0
  167. package/dist-server/service/chat-session-participant/chat-session-participant.js +112 -0
  168. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  169. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  170. package/dist-server/service/chat-session-participant/index.js +10 -0
  171. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  172. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  173. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  174. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  175. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  176. package/dist-server/service/chat-session-participant/presence.js +22 -0
  177. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  178. package/dist-server/service/chat-session-resolver.d.ts +20 -0
  179. package/dist-server/service/chat-session-resolver.js +356 -0
  180. package/dist-server/service/chat-session-resolver.js.map +1 -0
  181. package/dist-server/service/component-tree.d.ts +68 -0
  182. package/dist-server/service/component-tree.js +73 -0
  183. package/dist-server/service/component-tree.js.map +1 -0
  184. package/dist-server/service/index.d.ts +20 -0
  185. package/dist-server/service/index.js +34 -0
  186. package/dist-server/service/index.js.map +1 -0
  187. package/dist-server/service/mention-marker.d.ts +78 -0
  188. package/dist-server/service/mention-marker.js +162 -0
  189. package/dist-server/service/mention-marker.js.map +1 -0
  190. package/dist-server/service/mention-resolver.d.ts +96 -0
  191. package/dist-server/service/mention-resolver.js +177 -0
  192. package/dist-server/service/mention-resolver.js.map +1 -0
  193. package/dist-server/service/patch-entry/board-patch-broadcast.d.ts +13 -0
  194. package/dist-server/service/patch-entry/board-patch-broadcast.js +13 -0
  195. package/dist-server/service/patch-entry/board-patch-broadcast.js.map +1 -0
  196. package/dist-server/service/patch-entry/board-patch-subscription.d.ts +12 -0
  197. package/dist-server/service/patch-entry/board-patch-subscription.js +63 -0
  198. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -0
  199. package/dist-server/service/patch-entry/index.d.ts +4 -0
  200. package/dist-server/service/patch-entry/index.js +12 -0
  201. package/dist-server/service/patch-entry/index.js.map +1 -0
  202. package/dist-server/service/patch-entry/patch-entry.d.ts +16 -0
  203. package/dist-server/service/patch-entry/patch-entry.js +96 -0
  204. package/dist-server/service/patch-entry/patch-entry.js.map +1 -0
  205. package/dist-server/service/prefab-registry.d.ts +73 -0
  206. package/dist-server/service/prefab-registry.js +258 -0
  207. package/dist-server/service/prefab-registry.js.map +1 -0
  208. package/dist-server/service/styling/clear-tools.d.ts +15 -0
  209. package/dist-server/service/styling/clear-tools.js +53 -0
  210. package/dist-server/service/styling/clear-tools.js.map +1 -0
  211. package/dist-server/service/styling/color-utils.d.ts +43 -0
  212. package/dist-server/service/styling/color-utils.js +210 -0
  213. package/dist-server/service/styling/color-utils.js.map +1 -0
  214. package/dist-server/service/styling/copy-tools.d.ts +27 -0
  215. package/dist-server/service/styling/copy-tools.js +81 -0
  216. package/dist-server/service/styling/copy-tools.js.map +1 -0
  217. package/dist-server/service/styling/effect-tools.d.ts +45 -0
  218. package/dist-server/service/styling/effect-tools.js +90 -0
  219. package/dist-server/service/styling/effect-tools.js.map +1 -0
  220. package/dist-server/service/styling/fill-tools.d.ts +60 -0
  221. package/dist-server/service/styling/fill-tools.js +229 -0
  222. package/dist-server/service/styling/fill-tools.js.map +1 -0
  223. package/dist-server/service/styling/material-tools.d.ts +32 -0
  224. package/dist-server/service/styling/material-tools.js +92 -0
  225. package/dist-server/service/styling/material-tools.js.map +1 -0
  226. package/dist-server/service/styling/read-tools.d.ts +34 -0
  227. package/dist-server/service/styling/read-tools.js +45 -0
  228. package/dist-server/service/styling/read-tools.js.map +1 -0
  229. package/dist-server/service/styling/registry.d.ts +91 -0
  230. package/dist-server/service/styling/registry.js +352 -0
  231. package/dist-server/service/styling/registry.js.map +1 -0
  232. package/dist-server/service/styling/stroke-tools.d.ts +35 -0
  233. package/dist-server/service/styling/stroke-tools.js +97 -0
  234. package/dist-server/service/styling/stroke-tools.js.map +1 -0
  235. package/dist-server/service/styling/text-tools.d.ts +32 -0
  236. package/dist-server/service/styling/text-tools.js +105 -0
  237. package/dist-server/service/styling/text-tools.js.map +1 -0
  238. package/dist-server/service/subscribe-auth.d.ts +14 -0
  239. package/dist-server/service/subscribe-auth.js +33 -0
  240. package/dist-server/service/subscribe-auth.js.map +1 -0
  241. package/dist-server/service/types.d.ts +265 -0
  242. package/dist-server/service/types.js +3 -0
  243. package/dist-server/service/types.js.map +1 -0
  244. package/dist-server/service/validation/board-model-schema.d.ts +8311 -0
  245. package/dist-server/service/validation/board-model-schema.js +409 -0
  246. package/dist-server/service/validation/board-model-schema.js.map +1 -0
  247. package/dist-server/service/validation/tool-validation.d.ts +31 -0
  248. package/dist-server/service/validation/tool-validation.js +196 -0
  249. package/dist-server/service/validation/tool-validation.js.map +1 -0
  250. package/dist-server/tsconfig.tsbuildinfo +1 -0
  251. package/package.json +48 -0
  252. package/server/index.ts +23 -0
  253. package/server/service/agentic-loop.test.ts +705 -0
  254. package/server/service/agentic-loop.ts +431 -0
  255. package/server/service/apply-patch-drift.test.ts +829 -0
  256. package/server/service/apply-patch.test.ts +757 -0
  257. package/server/service/apply-patch.ts +411 -0
  258. package/server/service/assistant-integration.test.ts +922 -0
  259. package/server/service/assistant-llm-smoke.test.ts +296 -0
  260. package/server/service/assistant.test.ts +2034 -0
  261. package/server/service/assistant.ts +2378 -0
  262. package/server/service/board-ai-resolver.ts +407 -0
  263. package/server/service/catalog-resolver.test.ts +137 -0
  264. package/server/service/catalog-resolver.ts +102 -0
  265. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  266. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  267. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  268. package/server/service/chat-message/chat-message-publish.ts +22 -0
  269. package/server/service/chat-message/chat-message-subscription.ts +41 -0
  270. package/server/service/chat-message/chat-message.ts +169 -0
  271. package/server/service/chat-message/index.ts +12 -0
  272. package/server/service/chat-session/chat-session.ts +130 -0
  273. package/server/service/chat-session/index.ts +5 -0
  274. package/server/service/chat-session-participant/chat-session-participant.ts +110 -0
  275. package/server/service/chat-session-participant/index.ts +6 -0
  276. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  277. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  278. package/server/service/chat-session-participant/presence.test.ts +39 -0
  279. package/server/service/chat-session-participant/presence.ts +21 -0
  280. package/server/service/chat-session-resolver.ts +312 -0
  281. package/server/service/component-tree.test.ts +248 -0
  282. package/server/service/component-tree.ts +135 -0
  283. package/server/service/index.ts +32 -0
  284. package/server/service/mention-marker.extract.test.ts +61 -0
  285. package/server/service/mention-marker.test.ts +228 -0
  286. package/server/service/mention-marker.ts +184 -0
  287. package/server/service/mention-resolver.test.ts +273 -0
  288. package/server/service/mention-resolver.ts +264 -0
  289. package/server/service/patch-entry/board-patch-broadcast.ts +24 -0
  290. package/server/service/patch-entry/board-patch-subscription.test.ts +40 -0
  291. package/server/service/patch-entry/board-patch-subscription.ts +50 -0
  292. package/server/service/patch-entry/index.ts +12 -0
  293. package/server/service/patch-entry/patch-entry.ts +89 -0
  294. package/server/service/prefab-registry.test.ts +172 -0
  295. package/server/service/prefab-registry.ts +332 -0
  296. package/server/service/styling/clear-tools.test.ts +164 -0
  297. package/server/service/styling/clear-tools.ts +69 -0
  298. package/server/service/styling/color-utils.test.ts +130 -0
  299. package/server/service/styling/color-utils.ts +226 -0
  300. package/server/service/styling/copy-tools.test.ts +152 -0
  301. package/server/service/styling/copy-tools.ts +115 -0
  302. package/server/service/styling/effect-tools.test.ts +277 -0
  303. package/server/service/styling/effect-tools.ts +124 -0
  304. package/server/service/styling/fill-tools.test.ts +331 -0
  305. package/server/service/styling/fill-tools.ts +293 -0
  306. package/server/service/styling/material-tools.test.ts +294 -0
  307. package/server/service/styling/material-tools.ts +114 -0
  308. package/server/service/styling/read-tools.test.ts +78 -0
  309. package/server/service/styling/read-tools.ts +67 -0
  310. package/server/service/styling/registry.test.ts +203 -0
  311. package/server/service/styling/registry.ts +423 -0
  312. package/server/service/styling/stroke-tools.test.ts +245 -0
  313. package/server/service/styling/stroke-tools.ts +145 -0
  314. package/server/service/styling/text-tools.test.ts +253 -0
  315. package/server/service/styling/text-tools.ts +131 -0
  316. package/server/service/styling/tier1-tools.test.ts +300 -0
  317. package/server/service/subscribe-auth.test.ts +57 -0
  318. package/server/service/subscribe-auth.ts +34 -0
  319. package/server/service/types.ts +235 -0
  320. package/server/service/validation/board-model-schema.test.ts +778 -0
  321. package/server/service/validation/board-model-schema.ts +446 -0
  322. package/server/service/validation/tool-validation.ts +228 -0
  323. package/things-factory.config.js +1 -0
  324. package/translations/en.json +49 -0
  325. package/translations/ja.json +48 -0
  326. package/translations/ko.json +49 -0
  327. package/translations/ms.json +48 -0
  328. package/translations/zh.json +48 -0
  329. package/tsconfig.json +9 -0
@@ -0,0 +1,446 @@
1
+ /**
2
+ * BoardModel zod schema — 영속 / patch 적용 boundary 에서 데이터 무결성 검증.
3
+ *
4
+ * 핵심 정책:
5
+ * - things-scene 의 실제 drawer / setter 구현을 진실로 봄 (인터페이스 declaration X)
6
+ * - Component schema 는 `.passthrough()` — 알려진 필드 검증 + 미지 필드 통과
7
+ * (사용자 정의 / 데이터 바인딩 / 컴포넌트별 specific 속성 허용)
8
+ * - 그러나 styling sub-objects (FillStyle / Shadow 등) 는 `.strict()` — unknown key
9
+ * 거부. stale 잔재 필드 (예: colorStops + colors 공존) 자동 발견.
10
+ * - validation 실패 시 silent 누락 X — caller 가 명시적 에러 / 로그
11
+ *
12
+ * 적용 지점:
13
+ * 1. server `UpdateBoard` mutation 진입 → DB 영속 직전 마지막 방어선
14
+ * 2. server `BoardAIChat` mutation 응답 직전 — AI 가 만든 patch 의 안전성
15
+ * 3. (선택) client apply-patch 적용 후 — 메모리 상태 무결성
16
+ */
17
+ import { z } from 'zod'
18
+
19
+ // ── Color ────────────────────────────────────────────────────────
20
+
21
+ /**
22
+ * 색상 — hex (#rgb / #rgba / #rrggbb / #rrggbbaa) / rgb / rgba / CSS named / 'transparent'.
23
+ * 정규식은 느슨 — 정확한 검증은 normalizeColor 가, 여기선 형식만.
24
+ */
25
+ const ColorSchema = z.string().min(1).max(50)
26
+
27
+ // ── FillStyle (state.fillStyle) ──────────────────────────────────
28
+ //
29
+ // 실제 형식 (drawer/fill.ts 검증):
30
+ // - solid: 색상 string 그대로
31
+ // - gradient: { type: 'gradient', gradientType, colorStops: [{position, color}], rotation?, opacity? }
32
+ // - pattern: { type: 'pattern', image, ...옵션들 }
33
+ // - none: undefined / null
34
+
35
+ const GradientStopSchema = z
36
+ .object({
37
+ position: z.number().min(0).max(1),
38
+ color: ColorSchema
39
+ })
40
+ .strict()
41
+
42
+ const FillStyleGradientSchema = z
43
+ .object({
44
+ type: z.literal('gradient'),
45
+ gradientType: z.enum(['linear', 'radial']).optional(),
46
+ colorStops: z.array(GradientStopSchema).min(2),
47
+ rotation: z.number().optional(),
48
+ opacity: z.number().min(0).max(1).optional(),
49
+ center: z
50
+ .enum([
51
+ 'center',
52
+ 'left-top',
53
+ 'left-bottom',
54
+ 'right-top',
55
+ 'right-bottom',
56
+ 'left',
57
+ 'right',
58
+ 'top',
59
+ 'bottom'
60
+ ])
61
+ .optional()
62
+ })
63
+ .strict()
64
+
65
+ const FillStylePatternSchema = z
66
+ .object({
67
+ type: z.literal('pattern'),
68
+ image: z.union([z.string(), z.unknown()]),
69
+ color: ColorSchema.optional(),
70
+ fitPattern: z.boolean().optional(),
71
+ noRepeat: z.boolean().optional(),
72
+ align: z
73
+ .enum([
74
+ 'center',
75
+ 'left-top',
76
+ 'left-bottom',
77
+ 'right-top',
78
+ 'right-bottom',
79
+ 'left',
80
+ 'right',
81
+ 'top',
82
+ 'bottom'
83
+ ])
84
+ .optional(),
85
+ offsetX: z.number().optional(),
86
+ offsetY: z.number().optional(),
87
+ width: z.number().optional(),
88
+ height: z.number().optional(),
89
+ opacity: z.number().min(0).max(1).optional(),
90
+ guideOnly: z.boolean().optional()
91
+ })
92
+ .strict()
93
+
94
+ /** 'none' type — fillStyle 제거 의도 표현 (state.fillStyle = {type:'none'} 도 허용). */
95
+ const FillStyleNoneSchema = z.object({ type: z.literal('none') }).strict()
96
+
97
+ export const FillStyleSchema = z.union([
98
+ ColorSchema, // solid: '#xxx'
99
+ FillStyleGradientSchema,
100
+ FillStylePatternSchema,
101
+ FillStyleNoneSchema,
102
+ z.null()
103
+ ])
104
+
105
+ // ── Shadow (state.shadow) ────────────────────────────────────────
106
+ //
107
+ // Canonical (CSS box-shadow / Canvas API 표준): { color, offsetX, offsetY, blur }
108
+ // Backwards-compat aliases (옛 이름, drawer 가 fallback 으로 읽음): left / top / blurSize.
109
+ // 영속된 옛 데이터 호환을 위해 두 이름 다 schema 통과 — 새 코드는 canonical 사용.
110
+
111
+ export const ShadowSchema = z
112
+ .object({
113
+ color: ColorSchema.optional(),
114
+ // canonical (CSS / Canvas 표준)
115
+ offsetX: z.number().optional(),
116
+ offsetY: z.number().optional(),
117
+ blur: z.number().min(0).optional(),
118
+ // legacy aliases — 옛 things-scene 데이터 호환용. 새 코드는 위 canonical 사용.
119
+ left: z.number().optional(),
120
+ top: z.number().optional(),
121
+ blurSize: z.number().min(0).optional()
122
+ })
123
+ .strict()
124
+
125
+ // ── Material3D (state.material3d) ────────────────────────────────
126
+ //
127
+ // `things-scene/src/threed/material-3d.ts` 의 Material3D 인터페이스와 일치.
128
+
129
+ export const Material3DSchema = z
130
+ .object({
131
+ preset: z
132
+ .enum(['default', 'metal', 'glass', 'plastic', 'wood', 'ceramic', 'rubber', 'custom'])
133
+ .optional(),
134
+ metalness: z.number().min(0).max(1).optional(),
135
+ roughness: z.number().min(0).max(1).optional(),
136
+ emissive: ColorSchema.optional(),
137
+ emissiveIntensity: z.number().min(0).max(5).optional(),
138
+ opacity: z.number().min(0).max(1).optional(),
139
+ envMapIntensity: z.number().min(0).max(3).optional(),
140
+ side: z.enum(['double', 'front', 'back']).optional(),
141
+ castShadow: z.boolean().optional(),
142
+ receiveShadow: z.boolean().optional()
143
+ })
144
+ .strict()
145
+
146
+ // ── DataMapping (state.mappings) ─────────────────────────────────
147
+ //
148
+ // 데이터 바인딩 — `interfaces/data.ts` Mapping. rule / target / accessor / converter / param.
149
+
150
+ export const MappingSchema = z
151
+ .object({
152
+ rule: z.string().optional(),
153
+ target: z.string(),
154
+ property: z.string().optional(),
155
+ accessor: z.string().optional(),
156
+ source: z.unknown().optional(),
157
+ converter: z.union([z.string(), z.unknown()]).optional(),
158
+ param: z.unknown().optional()
159
+ })
160
+ .passthrough() // mapping 의 자유 필드 (converter args 등) 허용
161
+
162
+ // ── Component (one component in board.components) ────────────────
163
+ //
164
+ // 핵심 styling 필드는 명시 검증, 나머지는 passthrough — 컴포넌트별 specific 속성 자유.
165
+
166
+ const LineDashSchema = z.enum([
167
+ 'solid',
168
+ 'round-dot',
169
+ 'square-dot',
170
+ 'dash',
171
+ 'dash-dot',
172
+ 'long-dash',
173
+ 'long-dash-dot',
174
+ 'long-dash-dot-dot'
175
+ ])
176
+
177
+ /**
178
+ * 컴포넌트가 *어떤 형태든* 유효한 geometry 를 가졌는지 확인.
179
+ *
180
+ * things-scene 의 컴포넌트 type 별로 geometry 필드가 다양함 (rect 는 box,
181
+ * line 은 endpoints, polyline 은 vertices/path 등). type → 필드 매핑을 strict
182
+ * 하게 enumerate 하면 신규 type 등록 시 schema 도 동시 업데이트 필요 → forward
183
+ * compatibility 손해.
184
+ *
185
+ * 대신 "최소한 어떤 geometry 형식이라도 있어야 한다" 는 약한 보장 — 빈 컴포넌트
186
+ * (`{type:'rect'}` 만) 또는 type 에 안 맞는 geometry 구성 (`{type:'rect', cx:10, cy:20}`)
187
+ * 같은 명백한 오류만 차단. 실제 렌더링 정합성은 things-scene 자체가 검증.
188
+ */
189
+ function componentHasValidGeometry(component: any): boolean {
190
+ if (!component || typeof component !== 'object') return false
191
+
192
+ // Box geometry — left/top 둘 다 있어야 (width/height 는 optional)
193
+ const hasBox = typeof component.left === 'number' && typeof component.top === 'number'
194
+
195
+ // Circle/ellipse geometry — cx/cy 둘 다 있어야
196
+ const hasCircle = typeof component.cx === 'number' && typeof component.cy === 'number'
197
+
198
+ // Line endpoints
199
+ const hasLineEndpoints =
200
+ typeof component.x1 === 'number' &&
201
+ typeof component.y1 === 'number' &&
202
+ typeof component.x2 === 'number' &&
203
+ typeof component.y2 === 'number'
204
+
205
+ // Path / points / vertices 계열
206
+ const hasPath = typeof component.path === 'string' || Array.isArray(component.path)
207
+ const hasPoints = Array.isArray(component.points) && component.points.length > 0
208
+ const hasVertices =
209
+ (Array.isArray(component.vertices) && component.vertices.length > 0) ||
210
+ (Array.isArray(component.verticles) && component.verticles.length > 0) // legacy 오타
211
+
212
+ // line connector — startPoint / endPoint 형태 (anchor 기반 line)
213
+ const hasConnector =
214
+ component.from && typeof component.from === 'object' &&
215
+ component.to && typeof component.to === 'object'
216
+
217
+ return hasBox || hasCircle || hasLineEndpoints || hasPath || hasPoints || hasVertices || hasConnector
218
+ }
219
+
220
+ /** type 이 'gauge' 또는 'gauge-*' 면 value 필드 필요 (선택: min/max). */
221
+ function isGaugeType(type: string): boolean {
222
+ return type === 'gauge' || type.startsWith('gauge-')
223
+ }
224
+
225
+ /** type 이 'chart' 또는 'chart-*' 면 data 필드 필요. */
226
+ function isChartType(type: string): boolean {
227
+ return type === 'chart' || type.startsWith('chart-')
228
+ }
229
+
230
+ const ComponentBaseSchema = z
231
+ .object({
232
+ type: z.string().min(1),
233
+ refid: z.number().int().optional(), // 신규 컴포넌트 추가 시점엔 미할당 가능
234
+ // 위치 / 크기 — 컴포넌트 타입에 따라 다름. 본 schema 는 optional 로 두고
235
+ // refinement 가 *어떤 형태든* 있는지만 검사.
236
+ left: z.number().optional(),
237
+ top: z.number().optional(),
238
+ width: z.number().optional(),
239
+ height: z.number().optional(),
240
+ cx: z.number().optional(),
241
+ cy: z.number().optional(),
242
+ rx: z.number().optional(),
243
+ ry: z.number().optional(),
244
+ radius: z.number().optional(),
245
+ x1: z.number().optional(),
246
+ y1: z.number().optional(),
247
+ x2: z.number().optional(),
248
+ y2: z.number().optional(),
249
+ rotation: z.number().optional(),
250
+ // styling
251
+ fillStyle: FillStyleSchema.optional(),
252
+ strokeStyle: ColorSchema.optional(), // top-level string
253
+ lineWidth: z.number().min(0).optional(),
254
+ lineDash: LineDashSchema.optional(),
255
+ lineCap: z.enum(['butt', 'round', 'square']).optional(),
256
+ lineJoin: z.enum(['miter', 'round', 'bevel']).optional(),
257
+ shadow: ShadowSchema.optional(),
258
+ material3d: Material3DSchema.optional(),
259
+ // text / font — top-level
260
+ text: z.string().optional(),
261
+ fontFamily: z.string().optional(),
262
+ fontSize: z.number().positive().optional(),
263
+ fontWeight: z.union([z.string(), z.number()]).optional(),
264
+ fontStyle: z.string().optional(),
265
+ fontColor: ColorSchema.optional(),
266
+ textAlign: z.enum(['left', 'center', 'right', 'start', 'end', 'justify']).optional(),
267
+ textBaseline: z
268
+ .enum(['top', 'middle', 'bottom', 'hanging', 'alphabetic', 'ideographic'])
269
+ .optional(),
270
+ lineHeight: z.number().positive().optional(),
271
+ // visibility / opacity
272
+ alpha: z.number().min(0).max(1).optional(),
273
+ hidden: z.boolean().optional(),
274
+ // misc — 검증 안 함
275
+ id: z.string().optional(),
276
+ class: z.string().optional(),
277
+ tag: z.string().optional(),
278
+ // 데이터 바인딩
279
+ mappings: z.array(MappingSchema).optional()
280
+ })
281
+ .passthrough() // 컴포넌트별 specific 속성 (vehicle wheels, gauge needle, points/path 등) 허용
282
+
283
+ /**
284
+ * Component schema — base 위에 type-aware refinement 두 가지:
285
+ *
286
+ * 1. 모든 컴포넌트는 *어떤 형태든* 유효한 geometry 가 있어야
287
+ * (left+top / cx+cy / x1y1x2y2 / path / points / vertices / from+to)
288
+ * 2. gauge-* type 은 value 필요, chart-* type 은 data 필요 (soft check —
289
+ * 일부 prefab 이 미리 채워주는 케이스도 있어 strict 거부보다 미리 잡기 목적)
290
+ *
291
+ * 이 refinement 들이 LLM 의 잘못된 geometry (rect 에 cx/cy만, line 에 left/top만)
292
+ * 를 영속 직전 차단.
293
+ */
294
+ export const ComponentSchema = ComponentBaseSchema.superRefine((component, ctx) => {
295
+ if (!componentHasValidGeometry(component)) {
296
+ ctx.addIssue({
297
+ code: z.ZodIssueCode.custom,
298
+ message:
299
+ `Component '${component.type}' has no valid geometry — provide left+top (box), cx+cy (circle/ellipse), x1+y1+x2+y2 (line), path / points / vertices, or from+to (connector).`
300
+ })
301
+ }
302
+
303
+ if (isGaugeType(component.type) && component.value === undefined) {
304
+ ctx.addIssue({
305
+ code: z.ZodIssueCode.custom,
306
+ path: ['value'],
307
+ message: `Component '${component.type}' requires 'value' (current sensor reading or display number).`
308
+ })
309
+ }
310
+
311
+ if (isChartType(component.type) && component.data === undefined) {
312
+ ctx.addIssue({
313
+ code: z.ZodIssueCode.custom,
314
+ path: ['data'],
315
+ message: `Component '${component.type}' requires 'data' (chart datapoints array or object).`
316
+ })
317
+ }
318
+ })
319
+
320
+ // ── BoardModel (top-level) ───────────────────────────────────────
321
+
322
+ /**
323
+ * BoardModel — things-scene 의 model-layer (보드 root).
324
+ *
325
+ * 인식되는 root 속성 (model-layer 의 NATURE.properties + 실제 코드에서 state.* 로
326
+ * 참조되는 키):
327
+ * • Layout: width, height, fitMode, translate, scale
328
+ * • 2D 배경: fillStyle
329
+ * • 3D Sky: sky, skyColor
330
+ * • 3D Tone: exposure
331
+ * • 3D Hemisphere: hemiSkyColor, hemiGroundColor, hemiIntensity
332
+ * • 3D Directional light: dirLightEnabled, dirLightColor, dirLightIntensity,
333
+ * dirLightFollowCamera, dirLightAzimuth, dirLightElevation
334
+ * • 3D Shadow: dirShadowEnabled, dirShadowMapSize, dirShadowBias
335
+ * • 3D Camera: cameraX, cameraY, cameraZ, cameraAutoPlay, autoRotate, initialCameraView
336
+ * • 식별: id, refid, type
337
+ *
338
+ * ⚠ `name` 은 things-scene model-layer 가 인식 안 함 — Board *엔티티* 의 컬럼이지
339
+ * scene MODEL 의 필드가 아님. AI 가 modifyBoard 로 name 을 보내도 죽은 필드. 검증
340
+ * 시에도 제거 / 무시 정책.
341
+ *
342
+ * `passthrough` 인 이유 — 3D 광원/카메라/sky 등 키가 너무 많아 strict 로는 회귀 위험
343
+ * 큼. 대신 패키지 외부 (operato 등) 가 임의 root 키 추가하는 걸 막지 않음.
344
+ */
345
+ export const BoardModelSchema = z
346
+ .object({
347
+ width: z.number().positive(),
348
+ height: z.number().positive(),
349
+ fillStyle: FillStyleSchema.optional(), // 보드 배경
350
+ components: z.array(ComponentSchema).optional()
351
+ })
352
+ .passthrough() // 보드 레벨 자유 필드 (3D atmosphere / sky / camera 등)
353
+
354
+ // ── Patch op schemas — modify op 의 patch 가 invalid 이면 거부 ───
355
+
356
+ /**
357
+ * modify op 의 patch — 컴포넌트 partial. 모든 필드 optional. styling sub-object
358
+ * 만 strict 하게 검증해 stale key 잔존 차단.
359
+ */
360
+ export const ComponentPatchSchema = z
361
+ .object({
362
+ fillStyle: FillStyleSchema.nullable().optional(),
363
+ strokeStyle: ColorSchema.nullable().optional(),
364
+ lineWidth: z.number().min(0).optional(),
365
+ lineDash: LineDashSchema.optional(),
366
+ lineCap: z.enum(['butt', 'round', 'square']).optional(),
367
+ lineJoin: z.enum(['miter', 'round', 'bevel']).optional(),
368
+ shadow: ShadowSchema.nullable().optional(),
369
+ material3d: Material3DSchema.nullable().optional(),
370
+ text: z.string().optional(),
371
+ fontFamily: z.string().optional(),
372
+ fontSize: z.number().positive().optional(),
373
+ fontWeight: z.union([z.string(), z.number()]).optional(),
374
+ fontStyle: z.string().optional(),
375
+ fontColor: ColorSchema.optional(),
376
+ textAlign: z.enum(['left', 'center', 'right', 'start', 'end', 'justify']).optional(),
377
+ textBaseline: z
378
+ .enum(['top', 'middle', 'bottom', 'hanging', 'alphabetic', 'ideographic'])
379
+ .optional(),
380
+ lineHeight: z.number().positive().optional(),
381
+ alpha: z.number().min(0).max(1).optional(),
382
+ hidden: z.boolean().optional()
383
+ })
384
+ .passthrough() // 위치 / 크기 / 컴포넌트 specific 통과
385
+
386
+ /**
387
+ * modifyBoard op 의 patch — 보드 root partial.
388
+ *
389
+ * 모든 root 필드 optional (BoardModelSchema 와 달리 width/height 도 optional).
390
+ * passthrough 로 알려지지 않은 root 키 (3D atmosphere, sky, camera 등) 통과.
391
+ * 다만 *알려진 nested object* (fillStyle 등) 는 strict schema 재사용 — 그 안에
392
+ * unknown key 가 들어오면 거부 (예: fillStyle 안 gradientStops 같은 stale 이름).
393
+ *
394
+ * AI 가 setFill 등에서 학습한 vocabulary 를 modifyBoard 로 echo 시킬 때 stale
395
+ * key 가 fillStyle / shadow 같은 sub-object 안에 끼면 본 schema 가 catch.
396
+ */
397
+ export const BoardPatchSchema = z
398
+ .object({
399
+ width: z.number().positive().optional(),
400
+ height: z.number().positive().optional(),
401
+ fillStyle: FillStyleSchema.nullable().optional(),
402
+ components: z.array(ComponentSchema).optional()
403
+ })
404
+ .passthrough() // root 의 자유 필드 (sky / exposure / hemi*/ dirLight* / camera* 등)
405
+
406
+ // ── 검증 헬퍼 ────────────────────────────────────────────────────
407
+
408
+ export interface ValidationResult {
409
+ valid: boolean
410
+ errors?: Array<{ path: string; message: string }>
411
+ }
412
+
413
+ function formatIssues(error: z.ZodError): Array<{ path: string; message: string }> {
414
+ return error.issues.map(issue => ({
415
+ path: issue.path.join('.'),
416
+ message: issue.message
417
+ }))
418
+ }
419
+
420
+ /** BoardModel 전체 검증. 영속 / 로드 시 사용. */
421
+ export function validateBoardModel(model: unknown): ValidationResult {
422
+ const r = BoardModelSchema.safeParse(model)
423
+ if (r.success) return { valid: true }
424
+ return { valid: false, errors: formatIssues(r.error) }
425
+ }
426
+
427
+ /** modify patch 검증. AI 가 만든 patch 가 styling spec 에 맞는지. */
428
+ export function validateComponentPatch(patch: unknown): ValidationResult {
429
+ const r = ComponentPatchSchema.safeParse(patch)
430
+ if (r.success) return { valid: true }
431
+ return { valid: false, errors: formatIssues(r.error) }
432
+ }
433
+
434
+ /** 한 컴포넌트 검증. addComponent 시 사용. */
435
+ export function validateComponent(component: unknown): ValidationResult {
436
+ const r = ComponentSchema.safeParse(component)
437
+ if (r.success) return { valid: true }
438
+ return { valid: false, errors: formatIssues(r.error) }
439
+ }
440
+
441
+ /** modifyBoard op 의 patch 검증. 보드 root partial. */
442
+ export function validateBoardPatch(patch: unknown): ValidationResult {
443
+ const r = BoardPatchSchema.safeParse(patch)
444
+ if (r.success) return { valid: true }
445
+ return { valid: false, errors: formatIssues(r.error) }
446
+ }
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Write tool 의 사전 검증 — agentic loop 의 retry 메커니즘.
3
+ *
4
+ * 흐름:
5
+ * 1. LLM 이 write tool 호출
6
+ * 2. 본 함수가 args 검증 (styling tool 의 build* 함수 또는 schema 직접)
7
+ * 3. 실패 시 errors 반환 → caller 가 tool_result 에 error 메시지 담아 LLM 한테
8
+ * 4. LLM 다음 turn 에서 정정 후 재호출 (자기 회복)
9
+ *
10
+ * 이게 silent 데이터 손상을 막는 마지막 능동 방어선. 검증 통과한 args 만 누적
11
+ * → patch 로 변환되므로 깨진 데이터가 영속까지 못 감.
12
+ */
13
+ import type { AIToolCall } from '@things-factory/ai-client-base'
14
+ import {
15
+ validateComponentPatch,
16
+ validateComponent,
17
+ validateBoardPatch,
18
+ type ValidationResult
19
+ } from './board-model-schema'
20
+ import { buildSetFillOps } from '../styling/fill-tools'
21
+ import { buildSetStrokeOps } from '../styling/stroke-tools'
22
+ import { buildSetTextOps, buildSetFontOps } from '../styling/text-tools'
23
+ import {
24
+ buildSetShadowOps,
25
+ buildSetOpacityOps,
26
+ buildSetVisibleOps
27
+ } from '../styling/effect-tools'
28
+ import {
29
+ buildSetMaterialOps,
30
+ buildSetShadowPropertiesOps
31
+ } from '../styling/material-tools'
32
+ import { buildClearStyleOps } from '../styling/clear-tools'
33
+ import { buildCopyStyleOps } from '../styling/copy-tools'
34
+
35
+ export interface ToolValidationResult {
36
+ /** 검증 통과 — 누적 진행 가능 */
37
+ valid: boolean
38
+ /** 실패 시 LLM 한테 보일 에러 메시지 (자기 회복 정보 포함) */
39
+ errors?: string[]
40
+ /** LLM 한테 정정 가이드 — 가능한 경우 어떤 형식이 맞는지 */
41
+ suggestion?: string
42
+ }
43
+
44
+ /**
45
+ * write tool 의 args 사전 검증.
46
+ *
47
+ * 정책:
48
+ * - styling tool: 해당 build* 함수가 errors 반환 — 그대로 LLM 한테
49
+ * - modifyComponentByRefid: ComponentPatchSchema 로 검증 (raw fallback 영역)
50
+ * - addComponent: ComponentSchema 로 검증
51
+ * - 그 외: 기존 dispatcher 의 자체 검증에 위임 (재시도 보장 X)
52
+ */
53
+ export function validateWriteToolCall(
54
+ tc: AIToolCall,
55
+ components: ReadonlyArray<any> = []
56
+ ): ToolValidationResult {
57
+ const args = tc.arguments ?? {}
58
+
59
+ // Styling tool 들 — 각 build 함수가 detailed errors 반환
60
+ switch (tc.name) {
61
+ case 'setFill': {
62
+ const r = buildSetFillOps(args as any)
63
+ if (r.errors.length > 0)
64
+ return {
65
+ valid: false,
66
+ errors: r.errors,
67
+ suggestion: setFillSuggestion()
68
+ }
69
+ return { valid: true }
70
+ }
71
+ case 'setStroke': {
72
+ const r = buildSetStrokeOps(args as any)
73
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
74
+ return { valid: true }
75
+ }
76
+ case 'setText': {
77
+ const r = buildSetTextOps(args as any)
78
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
79
+ return { valid: true }
80
+ }
81
+ case 'setFont': {
82
+ const r = buildSetFontOps(args as any)
83
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
84
+ return { valid: true }
85
+ }
86
+ case 'setShadow': {
87
+ const r = buildSetShadowOps(args as any)
88
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
89
+ return { valid: true }
90
+ }
91
+ case 'setOpacity': {
92
+ const r = buildSetOpacityOps(args as any)
93
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
94
+ return { valid: true }
95
+ }
96
+ case 'setVisible': {
97
+ const r = buildSetVisibleOps(args as any)
98
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
99
+ return { valid: true }
100
+ }
101
+ case 'setMaterial': {
102
+ const r = buildSetMaterialOps(args as any)
103
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
104
+ return { valid: true }
105
+ }
106
+ case 'setShadowProperties': {
107
+ const r = buildSetShadowPropertiesOps(args as any)
108
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
109
+ return { valid: true }
110
+ }
111
+ case 'clearStyle': {
112
+ const r = buildClearStyleOps(args as any)
113
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
114
+ return { valid: true }
115
+ }
116
+ case 'copyStyle': {
117
+ const r = buildCopyStyleOps(args as any, components)
118
+ if (r.errors.length > 0) return { valid: false, errors: r.errors }
119
+ return { valid: true }
120
+ }
121
+
122
+ // raw fallback — schema 검증
123
+ case 'modifyComponentByRefid': {
124
+ if (typeof args.refid !== 'number') {
125
+ return { valid: false, errors: ['refid (number) is required'] }
126
+ }
127
+ if (!args.patch || typeof args.patch !== 'object') {
128
+ return { valid: false, errors: ['patch (object) is required'] }
129
+ }
130
+ const v = validateComponentPatch(args.patch)
131
+ if (!v.valid) {
132
+ return {
133
+ valid: false,
134
+ errors: formatValidationErrors(v),
135
+ suggestion: modifyPatchSuggestion()
136
+ }
137
+ }
138
+ return { valid: true }
139
+ }
140
+ case 'addComponent': {
141
+ const v = validateComponent(args)
142
+ if (!v.valid) {
143
+ return {
144
+ valid: false,
145
+ errors: formatValidationErrors(v)
146
+ }
147
+ }
148
+ return { valid: true }
149
+ }
150
+ case 'modifyBoard': {
151
+ // 보드 root partial 검증 — fillStyle/shadow 같은 nested 의 strict schema 가
152
+ // unknown key (e.g. fillStyle.gradientStops 같은 stale 이름) 를 거부.
153
+ // 이게 마지막 방어선 — 다른 styling tool 에서 학습된 vocabulary 가 leak 되면 catch.
154
+ if (!args.patch || typeof args.patch !== 'object' || Array.isArray(args.patch)) {
155
+ return { valid: false, errors: ['patch (object) is required'] }
156
+ }
157
+ const v = validateBoardPatch(args.patch)
158
+ if (!v.valid) {
159
+ return {
160
+ valid: false,
161
+ errors: formatValidationErrors(v),
162
+ suggestion: modifyBoardSuggestion()
163
+ }
164
+ }
165
+ return { valid: true }
166
+ }
167
+ case 'replaceBoard': {
168
+ // 보드 통째 교체 — 영속 직전 가장 강력한 검증 필요. components 까지 한 번에
169
+ // 검사. 자식 컴포넌트의 fillStyle.gradientStops 같은 stale 키도 즉시 catch.
170
+ // (BoardPatchSchema 는 component 도 array 로 통과시키되 ComponentSchema 가
171
+ // strict styling sub-objects 로 stale 차단.)
172
+ const v = validateBoardPatch(args)
173
+ if (!v.valid) {
174
+ return {
175
+ valid: false,
176
+ errors: formatValidationErrors(v),
177
+ suggestion: modifyBoardSuggestion()
178
+ }
179
+ }
180
+ return { valid: true }
181
+ }
182
+
183
+ // 그 외 — 기존 dispatcher 검증에 위임
184
+ default:
185
+ return { valid: true }
186
+ }
187
+ }
188
+
189
+ function modifyBoardSuggestion(): string {
190
+ return (
191
+ 'modifyBoard 의 patch 는 things-scene 의 model-layer 가 인식하는 root key 만 사용. ' +
192
+ 'fillStyle / strokeStyle / shadow 같은 nested object 의 안쪽 필드는 canonical 이름 (colorStops 등) 정확히 사용. ' +
193
+ 'rect 등 자식 컴포넌트 변경은 modifyComponentByRefid 로 분리.'
194
+ )
195
+ }
196
+
197
+ function formatValidationErrors(v: ValidationResult): string[] {
198
+ if (v.valid || !v.errors) return []
199
+ return v.errors.map(e => `${e.path || '<root>'}: ${e.message}`)
200
+ }
201
+
202
+ /**
203
+ * setFill 정정 가이드 — 자주 실수하는 패턴 + 올바른 예시.
204
+ */
205
+ function setFillSuggestion(): string {
206
+ return [
207
+ 'Valid setFill patterns:',
208
+ " - Solid: { refids:[35], mode:'solid', color:'#ff0000' }",
209
+ " - Gradient: { refids:[35], mode:'gradient', gradientType:'linear', " +
210
+ "colorStops:[{position:0,color:'#ff0000'},{position:1,color:'#0000ff'}], rotation:90 }",
211
+ " - Pattern: { refids:[35], mode:'pattern', image:'url' }",
212
+ " - Partial color update: { refids:[35], color:'green' } // mode 미지정",
213
+ 'Notes:',
214
+ ' - rotation 은 degrees 로 입력 (서버가 라디안으로 변환해 fillStyle.rotation 에 저장)',
215
+ ' - colorStops 는 { position: 0..1, color: hex/rgb/named } 객체 배열',
216
+ ' - 옛 이름 (colors / stops / gradientStops) 사용 금지 — engine canonical 은 colorStops'
217
+ ].join('\n')
218
+ }
219
+
220
+ function modifyPatchSuggestion(): string {
221
+ return [
222
+ 'modifyComponentByRefid is the raw fallback — prefer specific styling tools (setFill/setStroke/setText/setFont/setShadow/setMaterial/setOpacity/setVisible) for any styling change.',
223
+ 'If you must use raw modify:',
224
+ ' - fillStyle string (solid) or {type:"gradient",gradientType,colorStops:[{position,color}]} or {type:"pattern",image,...}',
225
+ ' - shadow {color, offsetX, offsetY, blur} (CSS box-shadow / Canvas 표준 이름)',
226
+ ' - lineDash STRING preset only (NOT number array)'
227
+ ].join('\n')
228
+ }
@@ -0,0 +1 @@
1
+ export default {}