@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,369 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyBoardEditPatch = applyBoardEditPatch;
4
+ exports.applyBoardEditPatchVerbose = applyBoardEditPatchVerbose;
5
+ exports.computeInverseOp = computeInverseOp;
6
+ exports.applyOp = applyOp;
7
+ exports.mergeComponent = mergeComponent;
8
+ const EMPTY_BOARD = {
9
+ width: 1000,
10
+ height: 600,
11
+ components: []
12
+ };
13
+ function applyBoardEditPatch(board, patch) {
14
+ return applyBoardEditPatchVerbose(board, patch).board;
15
+ }
16
+ /**
17
+ * Verbose 변형 — 각 op 의 적용 여부를 보고.
18
+ *
19
+ * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의
20
+ * 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 "수정했습니다"
21
+ * 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를
22
+ * 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.
23
+ */
24
+ function applyBoardEditPatchVerbose(board, patch) {
25
+ let result = board ?? EMPTY_BOARD;
26
+ const applied = [];
27
+ const missed = [];
28
+ for (const op of patch.ops) {
29
+ if (SCENE_ONLY_OPS.has(op.op)) {
30
+ // model 차원 noop 이지만 scene 차원 실제 적용 — applied 로 분류.
31
+ applied.push(op);
32
+ continue;
33
+ }
34
+ const next = applyOp(result, op);
35
+ // componentsUnchanged 는 components 배열 동일 + width/height/fillStyle 동일 만 본다.
36
+ // modify / remove 의 silent no-op (ghost refid) 검출 전용 — modifyBoard 가 sky
37
+ // 등 다른 root 키만 바꿀 때 false negative 로 missed 분류되는 회귀 방지.
38
+ const componentMutating = op.op === 'modify' || op.op === 'remove';
39
+ if (next === result || (componentMutating && componentsUnchanged(result, next))) {
40
+ missed.push(op);
41
+ }
42
+ else {
43
+ applied.push(op);
44
+ result = next;
45
+ }
46
+ }
47
+ return { board: result, applied, missed };
48
+ }
49
+ function componentsUnchanged(prev, next) {
50
+ // applyOp 는 항상 새 객체를 만든다 (`{ ...board, components: ... }`). 따라서 reference
51
+ // 비교가 안 되고 내용 비교가 필요. components 는 map/filter 결과 reference 도 다를 수
52
+ // 있으므로 length + JSON 깊이 비교.
53
+ const a = prev.components ?? [];
54
+ const b = next.components ?? [];
55
+ if (a.length !== b.length)
56
+ return false;
57
+ for (let i = 0; i < a.length; i++) {
58
+ if (a[i] !== b[i])
59
+ return false;
60
+ }
61
+ // root meta (width/height/...) 비교
62
+ return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle;
63
+ }
64
+ /**
65
+ * 주어진 board 상태에서 op 의 inverse 를 계산.
66
+ *
67
+ * Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을
68
+ * 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에
69
+ * 역순 실행만으로 복원 가능.
70
+ *
71
+ * 반환:
72
+ * - inverse op 또는 null (unsupported / 데이터 부족)
73
+ * - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →
74
+ * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied
75
+ * board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)
76
+ */
77
+ function computeInverseOp(board, op) {
78
+ if (!board)
79
+ return null;
80
+ const components = board.components ?? [];
81
+ switch (op.op) {
82
+ case 'add':
83
+ // add 의 inverse 는 새로 발급될 refid 를 알아야 → 호스트 측에서 처리
84
+ return null;
85
+ case 'remove': {
86
+ // 자식 refid 도 매칭 (deep search).
87
+ const target = findComponentDeep(components, op.refid);
88
+ if (!target)
89
+ return null; // 매칭 실패 — silent no-op 이라 inverse 도 없음
90
+ return { op: 'add', component: JSON.parse(JSON.stringify(target)) };
91
+ }
92
+ case 'modify': {
93
+ // 자식 refid 도 매칭 (deep search).
94
+ const target = findComponentDeep(components, op.refid);
95
+ if (!target)
96
+ return null;
97
+ // patch 가 건드린 키만 보관. nested 는 통째로.
98
+ const oldValues = {};
99
+ for (const k of Object.keys(op.patch || {})) {
100
+ const v = target[k];
101
+ oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
102
+ }
103
+ return { op: 'modify', refid: op.refid, patch: oldValues };
104
+ }
105
+ case 'modifyBoard': {
106
+ const oldValues = {};
107
+ const patch = op.patch || {};
108
+ for (const k of Object.keys(patch)) {
109
+ if (k === 'components')
110
+ continue;
111
+ const v = board[k];
112
+ oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
113
+ }
114
+ return { op: 'modifyBoard', patch: oldValues };
115
+ }
116
+ case 'replace':
117
+ // 이전 보드 통째로 보관 — replace 의 자연스러운 inverse 는 또 다른 replace
118
+ return { op: 'replace', board: JSON.parse(JSON.stringify(board)) };
119
+ case 'zorder': {
120
+ // forward ↔ backward / front ↔ back — 단순 반전. front/back 의 inverse 는
121
+ // 정확하지 않을 수 있음 (front 했다가 back 하면 원래 위치 보장 X), 호스트가
122
+ // 정확한 inverse 를 만들려면 zorder 직전 index 를 캡처해서 명시 modify 시퀀스로.
123
+ // 여기서는 best-effort 만.
124
+ const opp = {
125
+ forward: 'backward',
126
+ backward: 'forward',
127
+ front: 'back',
128
+ back: 'front'
129
+ };
130
+ const dir = opp[op.direction];
131
+ if (!dir)
132
+ return null;
133
+ return { op: 'zorder', refid: op.refid, direction: dir };
134
+ }
135
+ case 'align':
136
+ case 'distribute':
137
+ case 'group':
138
+ case 'ungroup':
139
+ case 'arrange':
140
+ // 좌표 변경 / 부모 재구성 / 새 group refid 발급 등은 scene 호출 후에야
141
+ // 알 수 있음. 호스트가 직접 inverse 만든다 (변경 컴포넌트 좌표 백업 →
142
+ // modify 시퀀스, group 의 새 refid 캡처 → ungroup 등).
143
+ return null;
144
+ default:
145
+ return null;
146
+ }
147
+ }
148
+ /**
149
+ * Scene 조작 op 들 — model 차원 시뮬레이션이 things-scene 의 결과와 일치 보장
150
+ * 어려움 (things-scene 의 정확한 좌표 계산 / refid 발급 / 부모 재구성 로직). 따라서
151
+ * apply-patch 에서는 noop. 호스트가 things-scene 의 정본 API (scene.align,
152
+ * scene.distribute, scene.group/ungroup, scene.zorder) 를 직접 호출.
153
+ *
154
+ * verbose 분류에서는 무조건 applied 로 — model 변화가 안 보여도 scene 차원 실제 적용됨.
155
+ */
156
+ const SCENE_ONLY_OPS = new Set([
157
+ 'align',
158
+ 'distribute',
159
+ 'group',
160
+ 'ungroup',
161
+ 'zorder',
162
+ 'arrange'
163
+ ]);
164
+ function applyOp(board, op) {
165
+ const components = board.components ?? [];
166
+ switch (op.op) {
167
+ case 'replace':
168
+ return op.board;
169
+ case 'add':
170
+ // top-level 추가 — 본 op 는 자식 컴포넌트로의 추가는 지원 안 함 (별도 op 필요)
171
+ return { ...board, components: [...components, op.component] };
172
+ case 'remove':
173
+ // 자식 (group/container 안) refid 도 매칭 — deep search.
174
+ return { ...board, components: removeComponentDeep(components, op.refid) };
175
+ case 'modify':
176
+ // 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.
177
+ return { ...board, components: modifyComponentDeep(components, op.refid, op.patch) };
178
+ case 'modifyBoard': {
179
+ // 루트 속성 (fillStyle / width / height / name 등) 만 deep merge.
180
+ // components 키는 무시 — 자식 변경은 add/remove/modify 별도 op 로.
181
+ const patch = { ...op.patch };
182
+ delete patch.components;
183
+ return mergeBoardRoot(board, patch);
184
+ }
185
+ case 'align':
186
+ case 'distribute':
187
+ case 'group':
188
+ case 'ungroup':
189
+ case 'zorder':
190
+ case 'arrange':
191
+ // scene-only — model 차원 noop. 정본은 things-scene API 가.
192
+ return board;
193
+ default:
194
+ return board;
195
+ }
196
+ }
197
+ /**
198
+ * patch 내 `null` 의 시맨틱 — **"이 키를 제거하라"**.
199
+ *
200
+ * 이유:
201
+ * 1. fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는
202
+ * null 로 set 하면 typeof null === 'object' 함정에 빠져 다운스트림이 크래시.
203
+ * 2. computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null
204
+ * 로 발급 — "키 제거" 의도와 정확히 일치. 새 시맨틱 하에서 inverse 가
205
+ * 자연스럽게 올바로 동작.
206
+ * 3. mode 전환 시 stale 필드 정리도 새 patch 에 `oldKey: null` 만 끼워서 처리 가능.
207
+ *
208
+ * 호출자가 명시적으로 "null 값 자체를 set" 하고 싶으면 → 별도 op 를 도입해야 하나,
209
+ * 현재 보드 모델에서 의미 있는 사용처가 없음.
210
+ */
211
+ function mergeBoardRoot(board, patch) {
212
+ const out = { ...board };
213
+ for (const key of Object.keys(patch)) {
214
+ const bv = board[key];
215
+ const pv = patch[key];
216
+ if (pv === null) {
217
+ delete out[key];
218
+ }
219
+ else if (bv !== null &&
220
+ typeof bv === 'object' &&
221
+ typeof pv === 'object' &&
222
+ !Array.isArray(bv) &&
223
+ !Array.isArray(pv)) {
224
+ out[key] = deepMergeRoot(bv, pv);
225
+ }
226
+ else {
227
+ out[key] = pv;
228
+ }
229
+ }
230
+ return out;
231
+ }
232
+ function deepMergeRoot(a, b) {
233
+ const out = { ...a };
234
+ for (const key of Object.keys(b)) {
235
+ const av = a[key];
236
+ const bv = b[key];
237
+ if (bv === null) {
238
+ delete out[key];
239
+ }
240
+ else if (av !== null &&
241
+ typeof av === 'object' &&
242
+ typeof bv === 'object' &&
243
+ !Array.isArray(av) &&
244
+ !Array.isArray(bv)) {
245
+ out[key] = deepMergeRoot(av, bv);
246
+ }
247
+ else {
248
+ out[key] = bv;
249
+ }
250
+ }
251
+ return out;
252
+ }
253
+ /**
254
+ * 컴포넌트에 부분 patch 를 적용.
255
+ * threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
256
+ * patchVal === null 이면 해당 키를 제거한다 (위 시맨틱 참조).
257
+ */
258
+ function mergeComponent(base, patch) {
259
+ const out = { ...base };
260
+ for (const key of Object.keys(patch)) {
261
+ const baseVal = base[key];
262
+ const patchVal = patch[key];
263
+ if (patchVal === null) {
264
+ delete out[key];
265
+ }
266
+ else if (isPlainObject(baseVal) && isPlainObject(patchVal)) {
267
+ out[key] = deepMerge(baseVal, patchVal);
268
+ }
269
+ else {
270
+ out[key] = patchVal;
271
+ }
272
+ }
273
+ return out;
274
+ }
275
+ function deepMerge(a, b) {
276
+ const out = { ...a };
277
+ for (const key of Object.keys(b)) {
278
+ const av = a[key];
279
+ const bv = b[key];
280
+ if (bv === null) {
281
+ delete out[key];
282
+ }
283
+ else if (isPlainObject(av) && isPlainObject(bv)) {
284
+ out[key] = deepMerge(av, bv);
285
+ }
286
+ else {
287
+ out[key] = bv;
288
+ }
289
+ }
290
+ return out;
291
+ }
292
+ function isPlainObject(v) {
293
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
294
+ }
295
+ // ── 자식 컴포넌트 (group/container 안) 까지 매칭 — deep search ─
296
+ /**
297
+ * 보드 트리 안에서 refid 로 컴포넌트 찾기 — 깊은 search.
298
+ * top-level 만 보던 옛 동작 → group/container 자식도 매칭.
299
+ */
300
+ function findComponentDeep(components, refid) {
301
+ for (const c of components) {
302
+ if (!c)
303
+ continue;
304
+ if (c.refid === refid)
305
+ return c;
306
+ const children = c.components;
307
+ if (Array.isArray(children) && children.length > 0) {
308
+ const sub = findComponentDeep(children, refid);
309
+ if (sub)
310
+ return sub;
311
+ }
312
+ }
313
+ return undefined;
314
+ }
315
+ /**
316
+ * modify op 의 deep 적용 — 자식 트리까지 walk 하며 매칭되는 컴포넌트만 mergeComponent.
317
+ * Pure function — 변경 없는 가지는 같은 reference 반환 (structural sharing).
318
+ */
319
+ function modifyComponentDeep(components, refid, patch) {
320
+ let changed = false;
321
+ const out = components.map(c => {
322
+ if (!c)
323
+ return c;
324
+ if (c.refid === refid) {
325
+ changed = true;
326
+ return mergeComponent(c, patch);
327
+ }
328
+ const children = c.components;
329
+ if (Array.isArray(children) && children.length > 0) {
330
+ const updated = modifyComponentDeep(children, refid, patch);
331
+ if (updated !== children) {
332
+ changed = true;
333
+ return { ...c, components: updated };
334
+ }
335
+ }
336
+ return c;
337
+ });
338
+ return changed ? out : components;
339
+ }
340
+ /**
341
+ * remove op 의 deep 적용 — 자식 트리까지 walk 하며 해당 refid 의 컴포넌트 제거.
342
+ * 부모는 보존, 빈 group 도 보존 (group 이 비더라도 사용자가 의도적으로 만든 컨테이너일 수 있음).
343
+ */
344
+ function removeComponentDeep(components, refid) {
345
+ let changed = false;
346
+ const out = [];
347
+ for (const c of components) {
348
+ if (!c) {
349
+ out.push(c);
350
+ continue;
351
+ }
352
+ if (c.refid === refid) {
353
+ changed = true;
354
+ continue; // 이 컴포넌트 자체를 제거
355
+ }
356
+ const children = c.components;
357
+ if (Array.isArray(children) && children.length > 0) {
358
+ const updated = removeComponentDeep(children, refid);
359
+ if (updated !== children) {
360
+ changed = true;
361
+ out.push({ ...c, components: updated });
362
+ continue;
363
+ }
364
+ }
365
+ out.push(c);
366
+ }
367
+ return changed ? out : components;
368
+ }
369
+ //# sourceMappingURL=apply-patch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"apply-patch.js","sourceRoot":"","sources":["../../server/service/apply-patch.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * BoardEditPatch 를 BoardModel 에 적용하는 표준 helper.\n *\n * 호출자가 patch 응답을 받아 보드에 반영할 때 사용.\n * Pure function — 입력을 mutate 하지 않음.\n */\nimport type { BoardComponent, BoardModel } from '@things-factory/board-import'\nimport type { BoardEditOp, BoardEditPatch } from './types.js'\n\nconst EMPTY_BOARD: BoardModel = {\n width: 1000,\n height: 600,\n components: []\n}\n\nexport interface PatchApplyReport {\n /** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */\n board: BoardModel\n /** 실제로 보드를 바꾼 op 들. */\n applied: BoardEditOp[]\n /** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */\n missed: BoardEditOp[]\n}\n\nexport function applyBoardEditPatch(\n board: BoardModel | undefined,\n patch: BoardEditPatch\n): BoardModel {\n return applyBoardEditPatchVerbose(board, patch).board\n}\n\n/**\n * Verbose 변형 — 각 op 의 적용 여부를 보고.\n *\n * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다 (patch 함수의\n * 의도적 단순화). LLM 이 잘못된 id 를 만들어 보내면 사용자에게 \"수정했습니다\"\n * 라고 답하지만 실제로는 아무 변화도 없는 상황이 발생 — 호스트가 missed 를\n * 보고 사용자에게 경고할 수 있도록 별도 entry point 제공.\n */\nexport function applyBoardEditPatchVerbose(\n board: BoardModel | undefined,\n patch: BoardEditPatch\n): PatchApplyReport {\n let result: BoardModel = board ?? EMPTY_BOARD\n const applied: BoardEditOp[] = []\n const missed: BoardEditOp[] = []\n\n for (const op of patch.ops) {\n if (SCENE_ONLY_OPS.has(op.op)) {\n // model 차원 noop 이지만 scene 차원 실제 적용 — applied 로 분류.\n applied.push(op)\n continue\n }\n const next = applyOp(result, op)\n // componentsUnchanged 는 components 배열 동일 + width/height/fillStyle 동일 만 본다.\n // modify / remove 의 silent no-op (ghost refid) 검출 전용 — modifyBoard 가 sky\n // 등 다른 root 키만 바꿀 때 false negative 로 missed 분류되는 회귀 방지.\n const componentMutating = op.op === 'modify' || op.op === 'remove'\n if (next === result || (componentMutating && componentsUnchanged(result, next))) {\n missed.push(op)\n } else {\n applied.push(op)\n result = next\n }\n }\n\n return { board: result, applied, missed }\n}\n\nfunction componentsUnchanged(prev: BoardModel, next: BoardModel): boolean {\n // applyOp 는 항상 새 객체를 만든다 (`{ ...board, components: ... }`). 따라서 reference\n // 비교가 안 되고 내용 비교가 필요. components 는 map/filter 결과 reference 도 다를 수\n // 있으므로 length + JSON 깊이 비교.\n const a = prev.components ?? []\n const b = next.components ?? []\n if (a.length !== b.length) return false\n for (let i = 0; i < a.length; i++) {\n if (a[i] !== b[i]) return false\n }\n // root meta (width/height/...) 비교\n return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle\n}\n\n/**\n * 주어진 board 상태에서 op 의 inverse 를 계산.\n *\n * Revert 기능의 코어 — patch 적용 직전 board 와 op 만 알면 그 op 의 역연산을\n * 만들 수 있으므로, 호스트가 in-place 적용하면서 함께 누적해 두면 나중에\n * 역순 실행만으로 복원 가능.\n *\n * 반환:\n * - inverse op 또는 null (unsupported / 데이터 부족)\n * - add 의 inverse 는 add 후 발급되는 refid 가 필요해서 모델 단계에서 계산 불가 →\n * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것 (이 함수는 pre-applied\n * board 만 보고 만들 수 있는 종류만 처리: remove / modify / modifyBoard / replace)\n */\nexport function computeInverseOp(\n board: BoardModel | undefined,\n op: BoardEditOp\n): BoardEditOp | null {\n if (!board) return null\n const components = board.components ?? []\n\n switch (op.op) {\n case 'add':\n // add 의 inverse 는 새로 발급될 refid 를 알아야 → 호스트 측에서 처리\n return null\n\n case 'remove': {\n // 자식 refid 도 매칭 (deep search).\n const target = findComponentDeep(components, op.refid)\n if (!target) return null // 매칭 실패 — silent no-op 이라 inverse 도 없음\n return { op: 'add', component: JSON.parse(JSON.stringify(target)) }\n }\n\n case 'modify': {\n // 자식 refid 도 매칭 (deep search).\n const target = findComponentDeep(components, op.refid)\n if (!target) return null\n // patch 가 건드린 키만 보관. nested 는 통째로.\n const oldValues: any = {}\n for (const k of Object.keys(op.patch || {})) {\n const v = (target as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modify', refid: op.refid, patch: oldValues }\n }\n\n case 'modifyBoard': {\n const oldValues: any = {}\n const patch = op.patch || {}\n for (const k of Object.keys(patch)) {\n if (k === 'components') continue\n const v = (board as any)[k]\n oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v))\n }\n return { op: 'modifyBoard', patch: oldValues }\n }\n\n case 'replace':\n // 이전 보드 통째로 보관 — replace 의 자연스러운 inverse 는 또 다른 replace\n return { op: 'replace', board: JSON.parse(JSON.stringify(board)) }\n\n case 'zorder': {\n // forward ↔ backward / front ↔ back — 단순 반전. front/back 의 inverse 는\n // 정확하지 않을 수 있음 (front 했다가 back 하면 원래 위치 보장 X), 호스트가\n // 정확한 inverse 를 만들려면 zorder 직전 index 를 캡처해서 명시 modify 시퀀스로.\n // 여기서는 best-effort 만.\n const opp: Record<string, 'front' | 'back' | 'forward' | 'backward'> = {\n forward: 'backward',\n backward: 'forward',\n front: 'back',\n back: 'front'\n }\n const dir = opp[op.direction]\n if (!dir) return null\n return { op: 'zorder', refid: op.refid, direction: dir }\n }\n\n case 'align':\n case 'distribute':\n case 'group':\n case 'ungroup':\n case 'arrange':\n // 좌표 변경 / 부모 재구성 / 새 group refid 발급 등은 scene 호출 후에야\n // 알 수 있음. 호스트가 직접 inverse 만든다 (변경 컴포넌트 좌표 백업 →\n // modify 시퀀스, group 의 새 refid 캡처 → ungroup 등).\n return null\n\n default:\n return null\n }\n}\n\n/**\n * Scene 조작 op 들 — model 차원 시뮬레이션이 things-scene 의 결과와 일치 보장\n * 어려움 (things-scene 의 정확한 좌표 계산 / refid 발급 / 부모 재구성 로직). 따라서\n * apply-patch 에서는 noop. 호스트가 things-scene 의 정본 API (scene.align,\n * scene.distribute, scene.group/ungroup, scene.zorder) 를 직접 호출.\n *\n * verbose 분류에서는 무조건 applied 로 — model 변화가 안 보여도 scene 차원 실제 적용됨.\n */\nconst SCENE_ONLY_OPS = new Set<BoardEditOp['op']>([\n 'align',\n 'distribute',\n 'group',\n 'ungroup',\n 'zorder',\n 'arrange'\n])\n\nexport function applyOp(board: BoardModel, op: BoardEditOp): BoardModel {\n const components = board.components ?? []\n switch (op.op) {\n case 'replace':\n return op.board\n\n case 'add':\n // top-level 추가 — 본 op 는 자식 컴포넌트로의 추가는 지원 안 함 (별도 op 필요)\n return { ...board, components: [...components, op.component] }\n\n case 'remove':\n // 자식 (group/container 안) refid 도 매칭 — deep search.\n return { ...board, components: removeComponentDeep(components, op.refid) }\n\n case 'modify':\n // 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.\n return { ...board, components: modifyComponentDeep(components, op.refid, op.patch) }\n\n case 'modifyBoard': {\n // 루트 속성 (fillStyle / width / height / name 등) 만 deep merge.\n // components 키는 무시 — 자식 변경은 add/remove/modify 별도 op 로.\n const patch = { ...op.patch } as any\n delete patch.components\n return mergeBoardRoot(board, patch)\n }\n\n case 'align':\n case 'distribute':\n case 'group':\n case 'ungroup':\n case 'zorder':\n case 'arrange':\n // scene-only — model 차원 noop. 정본은 things-scene API 가.\n return board\n\n default:\n return board\n }\n}\n\n/**\n * patch 내 `null` 의 시맨틱 — **\"이 키를 제거하라\"**.\n *\n * 이유:\n * 1. fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는\n * null 로 set 하면 typeof null === 'object' 함정에 빠져 다운스트림이 크래시.\n * 2. computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null\n * 로 발급 — \"키 제거\" 의도와 정확히 일치. 새 시맨틱 하에서 inverse 가\n * 자연스럽게 올바로 동작.\n * 3. mode 전환 시 stale 필드 정리도 새 patch 에 `oldKey: null` 만 끼워서 처리 가능.\n *\n * 호출자가 명시적으로 \"null 값 자체를 set\" 하고 싶으면 → 별도 op 를 도입해야 하나,\n * 현재 보드 모델에서 의미 있는 사용처가 없음.\n */\n\nfunction mergeBoardRoot(board: BoardModel, patch: any): BoardModel {\n const out: any = { ...board }\n for (const key of Object.keys(patch)) {\n const bv = (board as any)[key]\n const pv = patch[key]\n if (pv === null) {\n delete out[key]\n } else if (\n bv !== null &&\n typeof bv === 'object' &&\n typeof pv === 'object' &&\n !Array.isArray(bv) &&\n !Array.isArray(pv)\n ) {\n out[key] = deepMergeRoot(bv, pv)\n } else {\n out[key] = pv\n }\n }\n return out\n}\n\nfunction deepMergeRoot(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n const av = a[key]\n const bv = b[key]\n if (bv === null) {\n delete out[key]\n } else if (\n av !== null &&\n typeof av === 'object' &&\n typeof bv === 'object' &&\n !Array.isArray(av) &&\n !Array.isArray(bv)\n ) {\n out[key] = deepMergeRoot(av, bv)\n } else {\n out[key] = bv\n }\n }\n return out\n}\n\n/**\n * 컴포넌트에 부분 patch 를 적용.\n * threeD 등 nested object 는 deep merge — 호출자가 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.\n * patchVal === null 이면 해당 키를 제거한다 (위 시맨틱 참조).\n */\nexport function mergeComponent(base: BoardComponent, patch: Partial<BoardComponent>): BoardComponent {\n const out: any = { ...base }\n for (const key of Object.keys(patch)) {\n const baseVal = (base as any)[key]\n const patchVal = (patch as any)[key]\n if (patchVal === null) {\n delete out[key]\n } else if (isPlainObject(baseVal) && isPlainObject(patchVal)) {\n out[key] = deepMerge(baseVal, patchVal)\n } else {\n out[key] = patchVal\n }\n }\n return out as BoardComponent\n}\n\nfunction deepMerge(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n const av = a[key]\n const bv = b[key]\n if (bv === null) {\n delete out[key]\n } else if (isPlainObject(av) && isPlainObject(bv)) {\n out[key] = deepMerge(av, bv)\n } else {\n out[key] = bv\n }\n }\n return out\n}\n\nfunction isPlainObject(v: any): boolean {\n return v !== null && typeof v === 'object' && !Array.isArray(v)\n}\n\n// ── 자식 컴포넌트 (group/container 안) 까지 매칭 — deep search ─\n\n/**\n * 보드 트리 안에서 refid 로 컴포넌트 찾기 — 깊은 search.\n * top-level 만 보던 옛 동작 → group/container 자식도 매칭.\n */\nfunction findComponentDeep(components: BoardComponent[], refid: number): BoardComponent | undefined {\n for (const c of components) {\n if (!c) continue\n if ((c as any).refid === refid) return c\n const children = (c as any).components\n if (Array.isArray(children) && children.length > 0) {\n const sub = findComponentDeep(children, refid)\n if (sub) return sub\n }\n }\n return undefined\n}\n\n/**\n * modify op 의 deep 적용 — 자식 트리까지 walk 하며 매칭되는 컴포넌트만 mergeComponent.\n * Pure function — 변경 없는 가지는 같은 reference 반환 (structural sharing).\n */\nfunction modifyComponentDeep(\n components: BoardComponent[],\n refid: number,\n patch: any\n): BoardComponent[] {\n let changed = false\n const out = components.map(c => {\n if (!c) return c\n if ((c as any).refid === refid) {\n changed = true\n return mergeComponent(c, patch)\n }\n const children = (c as any).components\n if (Array.isArray(children) && children.length > 0) {\n const updated = modifyComponentDeep(children, refid, patch)\n if (updated !== children) {\n changed = true\n return { ...c, components: updated }\n }\n }\n return c\n })\n return changed ? out : components\n}\n\n/**\n * remove op 의 deep 적용 — 자식 트리까지 walk 하며 해당 refid 의 컴포넌트 제거.\n * 부모는 보존, 빈 group 도 보존 (group 이 비더라도 사용자가 의도적으로 만든 컨테이너일 수 있음).\n */\nfunction removeComponentDeep(\n components: BoardComponent[],\n refid: number\n): BoardComponent[] {\n let changed = false\n const out: BoardComponent[] = []\n for (const c of components) {\n if (!c) {\n out.push(c)\n continue\n }\n if ((c as any).refid === refid) {\n changed = true\n continue // 이 컴포넌트 자체를 제거\n }\n const children = (c as any).components\n if (Array.isArray(children) && children.length > 0) {\n const updated = removeComponentDeep(children, refid)\n if (updated !== children) {\n changed = true\n out.push({ ...c, components: updated })\n continue\n }\n }\n out.push(c)\n }\n return changed ? out : components\n}\n"]}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * DefaultBoardAIAssistant — base AIClient 위에 LLM 프롬프트로 chat() 구현.
3
+ */
4
+ import type { AIClient, AITool, AIToolCall, AIToolChatResult } from '@things-factory/ai-client-base';
5
+ import { type BoardModel, type ComponentCategory } from '@things-factory/board-import';
6
+ import type { BoardAIAssistant, BoardEditOp, ChatOptions, ChatResponse, LLMMessage } from './types.js';
7
+ import { type AgenticLoopResult } from './agentic-loop';
8
+ export interface BoardAIAssistantOptions {
9
+ /** 기본 scope (chat 호출 시 옵션이 없으면 사용) */
10
+ scopes?: string[];
11
+ knownTypes?: string[];
12
+ categories?: ComponentCategory[];
13
+ }
14
+ export declare class DefaultBoardAIAssistant implements BoardAIAssistant {
15
+ private base;
16
+ private options;
17
+ readonly id: string;
18
+ constructor(base: AIClient, options?: BoardAIAssistantOptions);
19
+ chat(messages: LLMMessage[], currentBoard: BoardModel | undefined, options?: ChatOptions): Promise<ChatResponse>;
20
+ private chatViaTools;
21
+ private resolveTypesAndCategories;
22
+ private buildSystemPromptForTools;
23
+ }
24
+ /**
25
+ * 4 op 직교 set 을 4 개의 도구로 정의.
26
+ *
27
+ * 핵심: addComponent 의 `type` 파라미터가 knownTypes 의 enum 으로 강제 →
28
+ * LLM 이 등록 안 된 type 을 만들 수 없음 (provider 단계에서 차단).
29
+ *
30
+ * 좌표/크기 키들은 모두 optional 로 허용 (type 별로 사용 키 다름).
31
+ * type-specific 추가 속성은 additionalProperties 로 허용.
32
+ */
33
+ export declare function buildBoardEditTools(knownTypes: string[]): AITool[];
34
+ /**
35
+ * Read tool 의 서버측 실행기 — currentBoard / selectedRefids 에 대해 즉시 실행해 결과 반환.
36
+ * LLM 의 다음 turn 에 tool_result 로 회신된다.
37
+ *
38
+ * 식별자 정책:
39
+ * - 컴포넌트 targeting 은 항상 refid (universal numeric handle).
40
+ * - id 는 데이터 바인딩 이름이며 unique 가 아니다 — filter 용도로만 사용 (findComponents).
41
+ */
42
+ export declare function executeReadTool(call: AIToolCall, currentBoard: BoardModel | null | undefined, selectedRefids: number[]): any;
43
+ export declare function isReadTool(name: string): boolean;
44
+ /**
45
+ * 등록된 색 팔레트 이름 목록 — catalog-resolver / 클라이언트 popup 데이터 source.
46
+ * 호스트 패키지가 추가 팔레트 등록은 현재 미지원 (향후 별도 registry 패턴 필요).
47
+ */
48
+ export declare function listColorPaletteNames(): string[];
49
+ /**
50
+ * 등록된 레이아웃 템플릿 이름 목록 — 위와 동일.
51
+ */
52
+ export declare function listLayoutTemplateNames(): string[];
53
+ /**
54
+ * agentic loop 의 비정상 종료 사유 → 사용자 메시지에 부기할 한국어 한 줄.
55
+ *
56
+ * 정상 종료 (abortReason undefined) 면 빈 문자열 — concat 해도 무해.
57
+ * 사용자에게 "왜 중간에 멈췄는지" 단서 제공이 목적 — 침묵 종료 금지.
58
+ */
59
+ export declare function formatAbortNotice(abortReason: AgenticLoopResult['abortReason']): string;
60
+ /**
61
+ * tool 결과를 UI fold-able 박스에 보여줄 만한 크기로 요약.
62
+ *
63
+ * 원본 그대로면 findComponents 50개 / getAtmosphereGuide 같은 큰 객체가 ChatMessage
64
+ * 영속까지 부담 — 핵심 모양만 유지하고 나머지는 marker.
65
+ */
66
+ export declare function summarizeToolResult(value: any, depth?: number): any;
67
+ export declare function isWriteTool(name: string): boolean;
68
+ export declare function isActionTool(name: string): boolean;
69
+ /**
70
+ * Tool call → BoardActionOp 변환 (C-1 ephemeral scene 조작).
71
+ * 잘못된 인자는 null — agentic loop 가 Unknown tool 비슷하게 처리.
72
+ */
73
+ export declare function toolCallToBoardActionOp(tc: AIToolCall): import('./types.js').BoardActionOp | null;
74
+ /**
75
+ * AIToolChatResult → ChatResponse.
76
+ *
77
+ * 정책 — followUp 항상 undefined: reply 와 동일 텍스트를 followUp 에 복제하면 클라이언트
78
+ * 에서 .md-body 와 .followup 두 박스에 같은 내용이 표시되어 사용자에게 "동일 응답이
79
+ * 두 번 반복" 으로 보인다. 명확화 질문이 필요한 경우 LLM 이 reply 자체에 포함시키도록
80
+ * 설계.
81
+ */
82
+ export declare function toolResultToChatResponse(result: AIToolChatResult): ChatResponse;
83
+ export declare function toolCallToBoardEditOp(tc: AIToolCall, components?: ReadonlyArray<any>): BoardEditOp | BoardEditOp[] | null;
84
+ export declare function summarizeBoard(board: BoardModel | null | undefined): any;