@things-factory/ai-assistant 10.1.6

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 (164) hide show
  1. package/client/components/board-ai-chat.test.ts +149 -0
  2. package/client/components/board-ai-chat.ts +3228 -0
  3. package/client/components/chat-defaults.ts +27 -0
  4. package/client/components/chat-echo-dedup.test.ts +230 -0
  5. package/client/components/chat-echo-dedup.ts +155 -0
  6. package/client/components/chat-input-builder.ts +69 -0
  7. package/client/components/markdown.test.ts +518 -0
  8. package/client/components/markdown.ts +508 -0
  9. package/client/components/mention-popup-helpers.ts +433 -0
  10. package/client/components/mention-popup.test.ts +588 -0
  11. package/client/components/mention-popup.ts +238 -0
  12. package/client/index.ts +14 -0
  13. package/client/tsconfig.json +13 -0
  14. package/client/utils/board-edit-patch.ts +374 -0
  15. package/dist-client/components/board-ai-chat.d.ts +423 -0
  16. package/dist-client/components/board-ai-chat.js +3034 -0
  17. package/dist-client/components/board-ai-chat.js.map +1 -0
  18. package/dist-client/components/board-ai-chat.test.d.ts +1 -0
  19. package/dist-client/components/board-ai-chat.test.js +138 -0
  20. package/dist-client/components/board-ai-chat.test.js.map +1 -0
  21. package/dist-client/components/chat-defaults.d.ts +8 -0
  22. package/dist-client/components/chat-defaults.js +22 -0
  23. package/dist-client/components/chat-defaults.js.map +1 -0
  24. package/dist-client/components/chat-echo-dedup.d.ts +54 -0
  25. package/dist-client/components/chat-echo-dedup.js +115 -0
  26. package/dist-client/components/chat-echo-dedup.js.map +1 -0
  27. package/dist-client/components/chat-echo-dedup.test.d.ts +1 -0
  28. package/dist-client/components/chat-echo-dedup.test.js +203 -0
  29. package/dist-client/components/chat-echo-dedup.test.js.map +1 -0
  30. package/dist-client/components/chat-input-builder.d.ts +55 -0
  31. package/dist-client/components/chat-input-builder.js +32 -0
  32. package/dist-client/components/chat-input-builder.js.map +1 -0
  33. package/dist-client/components/markdown.d.ts +60 -0
  34. package/dist-client/components/markdown.js +422 -0
  35. package/dist-client/components/markdown.js.map +1 -0
  36. package/dist-client/components/markdown.test.d.ts +1 -0
  37. package/dist-client/components/markdown.test.js +418 -0
  38. package/dist-client/components/markdown.test.js.map +1 -0
  39. package/dist-client/components/mention-popup-helpers.d.ts +165 -0
  40. package/dist-client/components/mention-popup-helpers.js +330 -0
  41. package/dist-client/components/mention-popup-helpers.js.map +1 -0
  42. package/dist-client/components/mention-popup.d.ts +35 -0
  43. package/dist-client/components/mention-popup.js +223 -0
  44. package/dist-client/components/mention-popup.js.map +1 -0
  45. package/dist-client/components/mention-popup.test.d.ts +1 -0
  46. package/dist-client/components/mention-popup.test.js +474 -0
  47. package/dist-client/components/mention-popup.test.js.map +1 -0
  48. package/dist-client/index.d.ts +14 -0
  49. package/dist-client/index.js +15 -0
  50. package/dist-client/index.js.map +1 -0
  51. package/dist-client/tsconfig.tsbuildinfo +1 -0
  52. package/dist-client/utils/board-edit-patch.d.ts +157 -0
  53. package/dist-client/utils/board-edit-patch.js +284 -0
  54. package/dist-client/utils/board-edit-patch.js.map +1 -0
  55. package/dist-server/index.d.ts +28 -0
  56. package/dist-server/index.js +32 -0
  57. package/dist-server/index.js.map +1 -0
  58. package/dist-server/service/chat-message/chat-message-broadcast.d.ts +13 -0
  59. package/dist-server/service/chat-message/chat-message-broadcast.js +13 -0
  60. package/dist-server/service/chat-message/chat-message-broadcast.js.map +1 -0
  61. package/dist-server/service/chat-message/chat-message-publish.d.ts +6 -0
  62. package/dist-server/service/chat-message/chat-message-publish.js +27 -0
  63. package/dist-server/service/chat-message/chat-message-publish.js.map +1 -0
  64. package/dist-server/service/chat-message/chat-message-subscription.d.ts +8 -0
  65. package/dist-server/service/chat-message/chat-message-subscription.js +57 -0
  66. package/dist-server/service/chat-message/chat-message-subscription.js.map +1 -0
  67. package/dist-server/service/chat-message/chat-message.d.ts +68 -0
  68. package/dist-server/service/chat-message/chat-message.js +172 -0
  69. package/dist-server/service/chat-message/chat-message.js.map +1 -0
  70. package/dist-server/service/chat-message/fold-history.d.ts +43 -0
  71. package/dist-server/service/chat-message/fold-history.js +33 -0
  72. package/dist-server/service/chat-message/fold-history.js.map +1 -0
  73. package/dist-server/service/chat-message/history-summary.d.ts +43 -0
  74. package/dist-server/service/chat-message/history-summary.js +77 -0
  75. package/dist-server/service/chat-message/history-summary.js.map +1 -0
  76. package/dist-server/service/chat-message/index.d.ts +5 -0
  77. package/dist-server/service/chat-message/index.js +12 -0
  78. package/dist-server/service/chat-message/index.js.map +1 -0
  79. package/dist-server/service/chat-message/llm-history.d.ts +46 -0
  80. package/dist-server/service/chat-message/llm-history.js +87 -0
  81. package/dist-server/service/chat-message/llm-history.js.map +1 -0
  82. package/dist-server/service/chat-session/chat-session.d.ts +60 -0
  83. package/dist-server/service/chat-session/chat-session.js +159 -0
  84. package/dist-server/service/chat-session/chat-session.js.map +1 -0
  85. package/dist-server/service/chat-session/index.d.ts +3 -0
  86. package/dist-server/service/chat-session/index.js +7 -0
  87. package/dist-server/service/chat-session/index.js.map +1 -0
  88. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +30 -0
  89. package/dist-server/service/chat-session/session-activity-broadcast.js +34 -0
  90. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -0
  91. package/dist-server/service/chat-session/session-activity-publish.d.ts +12 -0
  92. package/dist-server/service/chat-session/session-activity-publish.js +37 -0
  93. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -0
  94. package/dist-server/service/chat-session/session-activity-subscription.d.ts +14 -0
  95. package/dist-server/service/chat-session/session-activity-subscription.js +87 -0
  96. package/dist-server/service/chat-session/session-activity-subscription.js.map +1 -0
  97. package/dist-server/service/chat-session/session-anchor.d.ts +26 -0
  98. package/dist-server/service/chat-session/session-anchor.js +100 -0
  99. package/dist-server/service/chat-session/session-anchor.js.map +1 -0
  100. package/dist-server/service/chat-session/session-inbox.d.ts +80 -0
  101. package/dist-server/service/chat-session/session-inbox.js +133 -0
  102. package/dist-server/service/chat-session/session-inbox.js.map +1 -0
  103. package/dist-server/service/chat-session-participant/chat-session-participant.d.ts +49 -0
  104. package/dist-server/service/chat-session-participant/chat-session-participant.js +141 -0
  105. package/dist-server/service/chat-session-participant/chat-session-participant.js.map +1 -0
  106. package/dist-server/service/chat-session-participant/index.d.ts +4 -0
  107. package/dist-server/service/chat-session-participant/index.js +10 -0
  108. package/dist-server/service/chat-session-participant/index.js.map +1 -0
  109. package/dist-server/service/chat-session-participant/invite-notification.d.ts +27 -0
  110. package/dist-server/service/chat-session-participant/invite-notification.js +30 -0
  111. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -0
  112. package/dist-server/service/chat-session-participant/presence.d.ts +4 -0
  113. package/dist-server/service/chat-session-participant/presence.js +22 -0
  114. package/dist-server/service/chat-session-participant/presence.js.map +1 -0
  115. package/dist-server/service/chat-session-resolver.d.ts +93 -0
  116. package/dist-server/service/chat-session-resolver.js +706 -0
  117. package/dist-server/service/chat-session-resolver.js.map +1 -0
  118. package/dist-server/service/index.d.ts +24 -0
  119. package/dist-server/service/index.js +39 -0
  120. package/dist-server/service/index.js.map +1 -0
  121. package/dist-server/service/subscribe-auth.d.ts +14 -0
  122. package/dist-server/service/subscribe-auth.js +33 -0
  123. package/dist-server/service/subscribe-auth.js.map +1 -0
  124. package/dist-server/service/types.d.ts +5 -0
  125. package/dist-server/service/types.js +12 -0
  126. package/dist-server/service/types.js.map +1 -0
  127. package/dist-server/tsconfig.tsbuildinfo +1 -0
  128. package/package.json +48 -0
  129. package/server/index.ts +28 -0
  130. package/server/service/chat-message/chat-message-broadcast.integration.test.ts +73 -0
  131. package/server/service/chat-message/chat-message-broadcast.test.ts +40 -0
  132. package/server/service/chat-message/chat-message-broadcast.ts +27 -0
  133. package/server/service/chat-message/chat-message-publish.ts +22 -0
  134. package/server/service/chat-message/chat-message-subscription.ts +45 -0
  135. package/server/service/chat-message/chat-message.ts +195 -0
  136. package/server/service/chat-message/fold-history.test.ts +98 -0
  137. package/server/service/chat-message/fold-history.ts +77 -0
  138. package/server/service/chat-message/history-summary.test.ts +127 -0
  139. package/server/service/chat-message/history-summary.ts +100 -0
  140. package/server/service/chat-message/index.ts +12 -0
  141. package/server/service/chat-message/llm-history.test.ts +194 -0
  142. package/server/service/chat-message/llm-history.ts +163 -0
  143. package/server/service/chat-session/chat-session.ts +193 -0
  144. package/server/service/chat-session/index.ts +5 -0
  145. package/server/service/chat-session/session-activity-broadcast.ts +57 -0
  146. package/server/service/chat-session/session-activity-publish.ts +44 -0
  147. package/server/service/chat-session/session-activity-subscription.ts +67 -0
  148. package/server/service/chat-session/session-anchor.ts +106 -0
  149. package/server/service/chat-session/session-inbox.test.ts +185 -0
  150. package/server/service/chat-session/session-inbox.ts +161 -0
  151. package/server/service/chat-session-participant/chat-session-participant.ts +135 -0
  152. package/server/service/chat-session-participant/index.ts +6 -0
  153. package/server/service/chat-session-participant/invite-notification.test.ts +30 -0
  154. package/server/service/chat-session-participant/invite-notification.ts +48 -0
  155. package/server/service/chat-session-participant/presence.test.ts +39 -0
  156. package/server/service/chat-session-participant/presence.ts +21 -0
  157. package/server/service/chat-session-resolver.ts +644 -0
  158. package/server/service/index.ts +37 -0
  159. package/server/service/session-anchor.test.ts +133 -0
  160. package/server/service/subscribe-auth.test.ts +57 -0
  161. package/server/service/subscribe-auth.ts +34 -0
  162. package/server/service/types.ts +15 -0
  163. package/things-factory.config.js +1 -0
  164. package/tsconfig.json +9 -0
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Client-side BoardEditPatch types + applier.
3
+ *
4
+ * NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.
5
+ * client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.
6
+ */
7
+ const EMPTY_BOARD = { width: 1000, height: 600, components: [] };
8
+ /**
9
+ * Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).
10
+ */
11
+ export function applyBoardEditPatch(board, patch) {
12
+ return applyBoardEditPatchVerbose(board, patch).board;
13
+ }
14
+ /**
15
+ * Verbose 변형 — 각 op 의 적용 여부를 보고.
16
+ *
17
+ * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된
18
+ * id 를 만들어 보내면 사용자에게 "수정했습니다" 라고 답하지만 실제로는 아무 변화도
19
+ * 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도
20
+ * entry point 제공.
21
+ */
22
+ export function applyBoardEditPatchVerbose(board, patch) {
23
+ let result = board ?? EMPTY_BOARD;
24
+ const applied = [];
25
+ const missed = [];
26
+ for (const op of patch.ops) {
27
+ if (SCENE_ONLY_OPS.has(op.op)) {
28
+ // scene-only — model 차원 noop, scene 차원 적용. applied 분류.
29
+ applied.push(op);
30
+ continue;
31
+ }
32
+ const next = applyOp(result, op);
33
+ // componentsUnchanged 는 components 배열 동일 + width/height/fillStyle 동일 만 본다.
34
+ // modify / remove 의 silent no-op (ghost refid) 검출 전용 — modifyBoard 가 sky
35
+ // 등 다른 root 키만 바꿀 때 false negative 로 missed 분류되는 회귀 방지.
36
+ const componentMutating = op.op === 'modify' || op.op === 'remove';
37
+ if (next === result || (componentMutating && componentsUnchanged(result, next))) {
38
+ missed.push(op);
39
+ }
40
+ else {
41
+ applied.push(op);
42
+ result = next;
43
+ }
44
+ }
45
+ return { board: result, applied, missed };
46
+ }
47
+ function componentsUnchanged(prev, next) {
48
+ // applyOp 는 항상 새 객체를 만든다. 따라서 reference 비교가 안 되고 내용 비교 필요.
49
+ // components 는 map/filter 결과 reference 도 다를 수 있으므로 length + 요소 ref 비교.
50
+ const a = prev.components ?? [];
51
+ const b = next.components ?? [];
52
+ if (a.length !== b.length)
53
+ return false;
54
+ for (let i = 0; i < a.length; i++) {
55
+ if (a[i] !== b[i])
56
+ return false;
57
+ }
58
+ return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle;
59
+ }
60
+ const SCENE_ONLY_OPS = new Set([
61
+ 'align',
62
+ 'distribute',
63
+ 'group',
64
+ 'ungroup',
65
+ 'zorder',
66
+ 'arrange'
67
+ ]);
68
+ function applyOp(board, op) {
69
+ switch (op.op) {
70
+ case 'replace':
71
+ return op.board;
72
+ case 'add':
73
+ return { ...board, components: [...(board.components || []), op.component] };
74
+ case 'remove':
75
+ // 자식 (group/container 안) refid 도 매칭 — deep search.
76
+ return {
77
+ ...board,
78
+ components: removeComponentDeep(board.components || [], op.refid)
79
+ };
80
+ case 'modify':
81
+ // 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.
82
+ return {
83
+ ...board,
84
+ components: modifyComponentDeep(board.components || [], op.refid, op.patch)
85
+ };
86
+ case 'modifyBoard': {
87
+ const patch = { ...(op.patch || {}) };
88
+ delete patch.components; // 자식 변경은 별도 op
89
+ return mergeComponent(board, patch); // 최상위 board 자체에 deep-merge
90
+ }
91
+ case 'align':
92
+ case 'distribute':
93
+ case 'group':
94
+ case 'ungroup':
95
+ case 'zorder':
96
+ case 'arrange':
97
+ // scene-only — model 차원 noop. things-scene API 가 정본.
98
+ return board;
99
+ default:
100
+ return board;
101
+ }
102
+ }
103
+ /**
104
+ * 주어진 board 상태에서 op 의 inverse 를 계산.
105
+ *
106
+ * Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가
107
+ * 누적해두면 나중에 역순 실행만으로 복원.
108
+ *
109
+ * add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.
110
+ * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.
111
+ */
112
+ export function computeInverseOp(board, op) {
113
+ if (!board)
114
+ return null;
115
+ const components = board.components ?? [];
116
+ switch (op.op) {
117
+ case 'add':
118
+ return null;
119
+ case 'remove': {
120
+ // 자식 refid 도 매칭 (deep search)
121
+ const target = findComponentDeep(components, op.refid);
122
+ if (!target)
123
+ return null;
124
+ return { op: 'add', component: JSON.parse(JSON.stringify(target)) };
125
+ }
126
+ case 'modify': {
127
+ // 자식 refid 도 매칭 (deep search)
128
+ const target = findComponentDeep(components, op.refid);
129
+ if (!target)
130
+ return null;
131
+ const oldValues = {};
132
+ for (const k of Object.keys(op.patch || {})) {
133
+ const v = target[k];
134
+ oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
135
+ }
136
+ return { op: 'modify', refid: op.refid, patch: oldValues };
137
+ }
138
+ case 'modifyBoard': {
139
+ const oldValues = {};
140
+ const patch = op.patch || {};
141
+ for (const k of Object.keys(patch)) {
142
+ if (k === 'components')
143
+ continue;
144
+ const v = board[k];
145
+ oldValues[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
146
+ }
147
+ return { op: 'modifyBoard', patch: oldValues };
148
+ }
149
+ case 'replace':
150
+ return { op: 'replace', board: JSON.parse(JSON.stringify(board)) };
151
+ case 'zorder': {
152
+ // forward ↔ backward / front ↔ back. front/back 의 inverse 는 정확하지
153
+ // 않을 수 있음 (best-effort) — 정확 inverse 는 호스트가 zorder 직전 index
154
+ // 를 캡처해서 명시 modify 시퀀스로 만들어야.
155
+ const opp = {
156
+ forward: 'backward',
157
+ backward: 'forward',
158
+ front: 'back',
159
+ back: 'front'
160
+ };
161
+ const dir = opp[op.direction];
162
+ if (!dir)
163
+ return null;
164
+ return { op: 'zorder', refid: op.refid, direction: dir };
165
+ }
166
+ case 'align':
167
+ case 'distribute':
168
+ case 'group':
169
+ case 'ungroup':
170
+ case 'arrange':
171
+ // scene 호출 후에야 변경 결과 알 수 있음. 호스트가 직접 inverse 만든다.
172
+ return null;
173
+ default:
174
+ return null;
175
+ }
176
+ }
177
+ /**
178
+ * 컴포넌트에 부분 patch 를 적용 (deep merge).
179
+ * threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.
180
+ *
181
+ * patch 내 `null` 의 시맨틱 — **"이 키를 제거하라"**:
182
+ * - fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는 null 로
183
+ * set 하면 typeof null === 'object' 함정에 빠져 다운스트림 (3D 텍스처 등) 이 크래시.
184
+ * - computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null 로
185
+ * 발급하므로, 이 시맨틱과 정확히 일치 — inverse 가 자연스럽게 올바로 동작.
186
+ *
187
+ * host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.
188
+ */
189
+ export function mergeComponent(base, patch) {
190
+ const out = { ...base };
191
+ for (const key of Object.keys(patch)) {
192
+ const patchVal = patch[key];
193
+ if (patchVal === null) {
194
+ delete out[key];
195
+ }
196
+ else if (isPlainObject(base[key]) && isPlainObject(patchVal)) {
197
+ out[key] = deepMerge(base[key], patchVal);
198
+ }
199
+ else {
200
+ out[key] = patchVal;
201
+ }
202
+ }
203
+ return out;
204
+ }
205
+ function deepMerge(a, b) {
206
+ const out = { ...a };
207
+ for (const key of Object.keys(b)) {
208
+ const bv = b[key];
209
+ if (bv === null) {
210
+ delete out[key];
211
+ }
212
+ else if (isPlainObject(a[key]) && isPlainObject(bv)) {
213
+ out[key] = deepMerge(a[key], bv);
214
+ }
215
+ else {
216
+ out[key] = bv;
217
+ }
218
+ }
219
+ return out;
220
+ }
221
+ function isPlainObject(v) {
222
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
223
+ }
224
+ // ── 자식 컴포넌트 (group/container 안) 까지 매칭 — deep search ─
225
+ // server/service/apply-patch.ts 와 동일 로직. drift test 가 동등성 보장.
226
+ function findComponentDeep(components, refid) {
227
+ for (const c of components) {
228
+ if (!c)
229
+ continue;
230
+ if (c.refid === refid)
231
+ return c;
232
+ if (Array.isArray(c.components) && c.components.length > 0) {
233
+ const sub = findComponentDeep(c.components, refid);
234
+ if (sub)
235
+ return sub;
236
+ }
237
+ }
238
+ return undefined;
239
+ }
240
+ function modifyComponentDeep(components, refid, patch) {
241
+ let changed = false;
242
+ const out = components.map(c => {
243
+ if (!c)
244
+ return c;
245
+ if (c.refid === refid) {
246
+ changed = true;
247
+ return mergeComponent(c, patch);
248
+ }
249
+ if (Array.isArray(c.components) && c.components.length > 0) {
250
+ const updated = modifyComponentDeep(c.components, refid, patch);
251
+ if (updated !== c.components) {
252
+ changed = true;
253
+ return { ...c, components: updated };
254
+ }
255
+ }
256
+ return c;
257
+ });
258
+ return changed ? out : components;
259
+ }
260
+ function removeComponentDeep(components, refid) {
261
+ let changed = false;
262
+ const out = [];
263
+ for (const c of components) {
264
+ if (!c) {
265
+ out.push(c);
266
+ continue;
267
+ }
268
+ if (c.refid === refid) {
269
+ changed = true;
270
+ continue;
271
+ }
272
+ if (Array.isArray(c.components) && c.components.length > 0) {
273
+ const updated = removeComponentDeep(c.components, refid);
274
+ if (updated !== c.components) {
275
+ changed = true;
276
+ out.push({ ...c, components: updated });
277
+ continue;
278
+ }
279
+ }
280
+ out.push(c);
281
+ }
282
+ return changed ? out : components;
283
+ }
284
+ //# sourceMappingURL=board-edit-patch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board-edit-patch.js","sourceRoot":"","sources":["../../client/utils/board-edit-patch.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAkFH,MAAM,WAAW,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,EAAE,EAAW,EAAE,CAAA;AAEzE;;GAEG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAsB,EAAE,KAAqB;IAC/E,OAAO,0BAA0B,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC,KAAK,CAAA;AACvD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CACxC,KAAsB,EACtB,KAAqB;IAErB,IAAI,MAAM,GAAQ,KAAK,IAAI,WAAW,CAAA;IACtC,MAAM,OAAO,GAAkB,EAAE,CAAA;IACjC,MAAM,MAAM,GAAkB,EAAE,CAAA;IAEhC,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,GAAG,EAAE,CAAC;QAC3B,IAAI,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;YAC9B,uDAAuD;YACvD,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAChB,SAAQ;QACV,CAAC;QACD,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAA;QAChC,2EAA2E;QAC3E,yEAAyE;QACzE,wDAAwD;QACxD,MAAM,iBAAiB,GAAG,EAAE,CAAC,EAAE,KAAK,QAAQ,IAAI,EAAE,CAAC,EAAE,KAAK,QAAQ,CAAA;QAClE,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,iBAAiB,IAAI,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,EAAE,CAAC;YAChF,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACjB,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;YAChB,MAAM,GAAG,IAAI,CAAA;QACf,CAAC;IACH,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAA;AAC3C,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAS,EAAE,IAAS;IAC/C,2DAA2D;IAC3D,uEAAuE;IACvE,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,IAAI,EAAE,CAAA;IAC/B,MAAM,CAAC,GAAG,IAAI,CAAC,UAAU,IAAI,EAAE,CAAA;IAC/B,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAA;IACvC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAA;IACjC,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAA;AACtG,CAAC;AAED,MAAM,cAAc,GAAG,IAAI,GAAG,CAAoB;IAChD,OAAO;IACP,YAAY;IACZ,OAAO;IACP,SAAS;IACT,QAAQ;IACR,SAAS;CACV,CAAC,CAAA;AAEF,SAAS,OAAO,CAAC,KAAU,EAAE,EAAe;IAC1C,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACd,KAAK,SAAS;YACZ,OAAO,EAAE,CAAC,KAAK,CAAA;QACjB,KAAK,KAAK;YACR,OAAO,EAAE,GAAG,KAAK,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC,EAAE,CAAA;QAC9E,KAAK,QAAQ;YACX,mDAAmD;YACnD,OAAO;gBACL,GAAG,KAAK;gBACR,UAAU,EAAE,mBAAmB,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,EAAE,CAAC,KAAK,CAAC;aAClE,CAAA;QACH,KAAK,QAAQ;YACX,iDAAiD;YACjD,OAAO;gBACL,GAAG,KAAK;gBACR,UAAU,EAAE,mBAAmB,CAAC,KAAK,CAAC,UAAU,IAAI,EAAE,EAAE,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC;aAC5E,CAAA;QACH,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,KAAK,GAAG,EAAE,GAAG,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAA;YACrC,OAAO,KAAK,CAAC,UAAU,CAAA,CAAC,eAAe;YACvC,OAAO,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA,CAAC,2BAA2B;QACjE,CAAC;QACD,KAAK,OAAO,CAAC;QACb,KAAK,YAAY,CAAC;QAClB,KAAK,OAAO,CAAC;QACb,KAAK,SAAS,CAAC;QACf,KAAK,QAAQ,CAAC;QACd,KAAK,SAAS;YACZ,qDAAqD;YACrD,OAAO,KAAK,CAAA;QACd;YACE,OAAO,KAAK,CAAA;IAChB,CAAC;AACH,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAU,EAAE,EAAe;IAC1D,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAA;IACvB,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,IAAI,EAAE,CAAA;IAEzC,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;QACd,KAAK,KAAK;YACR,OAAO,IAAI,CAAA;QAEb,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,8BAA8B;YAC9B,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,CAAA;YACtD,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,EAAE,CAAA;QACrE,CAAC;QAED,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,8BAA8B;YAC9B,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,EAAE,EAAE,CAAC,KAAK,CAAC,CAAA;YACtD,IAAI,CAAC,MAAM;gBAAE,OAAO,IAAI,CAAA;YACxB,MAAM,SAAS,GAAQ,EAAE,CAAA;YACzB,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;gBAC5C,MAAM,CAAC,GAAI,MAAc,CAAC,CAAC,CAAC,CAAA;gBAC5B,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;QAC5D,CAAC;QAED,KAAK,aAAa,CAAC,CAAC,CAAC;YACnB,MAAM,SAAS,GAAQ,EAAE,CAAA;YACzB,MAAM,KAAK,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,CAAA;YAC5B,KAAK,MAAM,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;gBACnC,IAAI,CAAC,KAAK,YAAY;oBAAE,SAAQ;gBAChC,MAAM,CAAC,GAAI,KAAa,CAAC,CAAC,CAAC,CAAA;gBAC3B,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;YACvE,CAAC;YACD,OAAO,EAAE,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,SAAS,EAAE,CAAA;QAChD,CAAC;QAED,KAAK,SAAS;YACZ,OAAO,EAAE,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,CAAA;QAEpE,KAAK,QAAQ,CAAC,CAAC,CAAC;YACd,iEAAiE;YACjE,4DAA4D;YAC5D,8BAA8B;YAC9B,MAAM,GAAG,GAA8D;gBACrE,OAAO,EAAE,UAAU;gBACnB,QAAQ,EAAE,SAAS;gBACnB,KAAK,EAAE,MAAM;gBACb,IAAI,EAAE,OAAO;aACd,CAAA;YACD,MAAM,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC,SAAS,CAAC,CAAA;YAC7B,IAAI,CAAC,GAAG;gBAAE,OAAO,IAAI,CAAA;YACrB,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE,CAAA;QAC1D,CAAC;QAED,KAAK,OAAO,CAAC;QACb,KAAK,YAAY,CAAC;QAClB,KAAK,OAAO,CAAC;QACb,KAAK,SAAS,CAAC;QACf,KAAK,SAAS;YACZ,kDAAkD;YAClD,OAAO,IAAI,CAAA;QAEb;YACE,OAAO,IAAI,CAAA;IACf,CAAC;AACH,CAAC;AAED;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,cAAc,CAAC,IAAS,EAAE,KAAU;IAClD,MAAM,GAAG,GAAQ,EAAE,GAAG,IAAI,EAAE,CAAA;IAC5B,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAA;QAC3B,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,OAAO,GAAG,CAAC,GAAG,CAAC,CAAA;QACjB,CAAC;aAAM,IAAI,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,aAAa,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC/D,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,QAAQ,CAAC,CAAA;QAC3C,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,SAAS,CAAC,CAAM,EAAE,CAAM;IAC/B,MAAM,GAAG,GAAQ,EAAE,GAAG,CAAC,EAAE,CAAA;IACzB,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,MAAM,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAA;QACjB,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;YAChB,OAAO,GAAG,CAAC,GAAG,CAAC,CAAA;QACjB,CAAC;aAAM,IAAI,aAAa,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,aAAa,CAAC,EAAE,CAAC,EAAE,CAAC;YACtD,GAAG,CAAC,GAAG,CAAC,GAAG,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;QAClC,CAAC;aAAM,CAAC;YACN,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAA;QACf,CAAC;IACH,CAAC;IACD,OAAO,GAAG,CAAA;AACZ,CAAC;AAED,SAAS,aAAa,CAAC,CAAM;IAC3B,OAAO,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAA;AACjE,CAAC;AAED,uDAAuD;AACvD,8DAA8D;AAE9D,SAAS,iBAAiB,CAAC,UAAiB,EAAE,KAAa;IACzD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC;YAAE,SAAQ;QAChB,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK;YAAE,OAAO,CAAC,CAAA;QAC/B,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3D,MAAM,GAAG,GAAG,iBAAiB,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YAClD,IAAI,GAAG;gBAAE,OAAO,GAAG,CAAA;QACrB,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AAED,SAAS,mBAAmB,CAAC,UAAiB,EAAE,KAAa,EAAE,KAAU;IACvE,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE;QAC7B,IAAI,CAAC,CAAC;YAAE,OAAO,CAAC,CAAA;QAChB,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,GAAG,IAAI,CAAA;YACd,OAAO,cAAc,CAAC,CAAC,EAAE,KAAK,CAAC,CAAA;QACjC,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3D,MAAM,OAAO,GAAG,mBAAmB,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,EAAE,KAAK,CAAC,CAAA;YAC/D,IAAI,OAAO,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;gBAC7B,OAAO,GAAG,IAAI,CAAA;gBACd,OAAO,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAA;YACtC,CAAC;QACH,CAAC;QACD,OAAO,CAAC,CAAA;IACV,CAAC,CAAC,CAAA;IACF,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAA;AACnC,CAAC;AAED,SAAS,mBAAmB,CAAC,UAAiB,EAAE,KAAa;IAC3D,IAAI,OAAO,GAAG,KAAK,CAAA;IACnB,MAAM,GAAG,GAAU,EAAE,CAAA;IACrB,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,CAAC,EAAE,CAAC;YACP,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;YACX,SAAQ;QACV,CAAC;QACD,IAAI,CAAC,CAAC,KAAK,KAAK,KAAK,EAAE,CAAC;YACtB,OAAO,GAAG,IAAI,CAAA;YACd,SAAQ;QACV,CAAC;QACD,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3D,MAAM,OAAO,GAAG,mBAAmB,CAAC,CAAC,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;YACxD,IAAI,OAAO,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;gBAC7B,OAAO,GAAG,IAAI,CAAA;gBACd,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,OAAO,EAAE,CAAC,CAAA;gBACvC,SAAQ;YACV,CAAC;QACH,CAAC;QACD,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACb,CAAC;IACD,OAAO,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,UAAU,CAAA;AACnC,CAAC","sourcesContent":["/**\n * Client-side BoardEditPatch types + applier.\n *\n * NOTE: 동일 정의가 server/service/types.ts + apply-patch.ts 에 있음.\n * client 번들이 server 코드를 import 하지 않도록 분리. 변경 시 양쪽 동기화 필요.\n */\n\n/**\n * 컴포넌트 타깃팅은 항상 `refid` (things-scene universal numeric handle).\n * `id` 는 데이터 바인딩 이름이며 unique 가 아니므로 targeting 에 사용하지 않는다.\n *\n * 보드는 최상위 부모 — 자체 속성 (fillStyle / width / height / name 등) 을 갖고,\n * `modifyBoard` 로 변경. 자식 컴포넌트 변경 (`modify`) 와 분리.\n *\n * Phase 2 — Scene 조작 op (align/distribute/group/ungroup/zorder). model 차원\n * 시뮬레이션은 noop 이고 things-scene API 가 정본.\n */\nexport type AlignDirection =\n | 'left'\n | 'right'\n | 'center'\n | 'top'\n | 'middle'\n | 'bottom'\n\nexport type DistributeAxis = 'horizontal' | 'vertical'\n\nexport type ZorderDirection = 'front' | 'back' | 'forward' | 'backward'\n\n/**\n * Sugar layout — `arrange` op 의 layout 종류.\n * Server-side types.ts 의 ArrangeLayout 와 일치 — drift test 가 검증.\n */\nexport type ArrangeLayout =\n | { type: 'grid'; cols: number; gap?: number; anchor?: { left: number; top: number } }\n | {\n type: 'row'\n gap?: number\n anchor?: { left: number; top: number }\n align?: 'start' | 'center' | 'end'\n }\n | {\n type: 'column'\n gap?: number\n anchor?: { left: number; top: number }\n align?: 'start' | 'center' | 'end'\n }\n\nexport type BoardEditOp =\n | { op: 'add'; component: any }\n | { op: 'remove'; refid: number }\n | { op: 'modify'; refid: number; patch: any }\n | { op: 'modifyBoard'; patch: any }\n | { op: 'replace'; board: any }\n | { op: 'align'; refids: number[]; direction: AlignDirection }\n | { op: 'distribute'; refids: number[]; axis: DistributeAxis }\n | { op: 'group'; refids: number[] }\n | { op: 'ungroup'; refid: number }\n | { op: 'zorder'; refid: number; direction: ZorderDirection }\n | { op: 'arrange'; refids: number[]; layout: ArrangeLayout }\n\n/**\n * C-1 — Scene 조작 action (ephemeral). 모델 변경 X, undo 영향 X.\n * 호스트가 board-action-execute 이벤트로 받아서 things-scene API 직접 호출.\n */\nexport type BoardActionOp =\n | { action: 'selectComponents'; refids: number[] }\n | { action: 'centerToComponent'; refid: number; animated?: boolean }\n | { action: 'fitToView'; mode?: 'fit' | 'ratio' | 'width' | 'height' }\n | { action: 'setSceneMode'; mode: 'edit' | 'view' }\n | { action: 'highlightComponents'; refids: number[] }\n\nexport interface BoardEditPatch {\n ops: BoardEditOp[]\n summary: string\n confidence: number\n}\n\nexport interface PatchApplyReport {\n /** 패치 적용 후 보드. 모든 op 가 noop 이어도 입력 그대로 반환. */\n board: any\n /** 실제로 보드를 바꾼 op 들. */\n applied: BoardEditOp[]\n /** id 매칭 실패 등으로 noop 이 된 op 들 — 호출자가 사용자에게 알릴 단서. */\n missed: BoardEditOp[]\n}\n\nconst EMPTY_BOARD = { width: 1000, height: 600, components: [] as any[] }\n\n/**\n * Patch 를 BoardModel 에 적용 (pure, board 를 mutate 하지 않음).\n */\nexport function applyBoardEditPatch(board: any | undefined, patch: BoardEditPatch): any {\n return applyBoardEditPatchVerbose(board, patch).board\n}\n\n/**\n * Verbose 변형 — 각 op 의 적용 여부를 보고.\n *\n * `modify` 와 `remove` 는 id 가 보드에 없으면 silent no-op 이 된다. LLM 이 잘못된\n * id 를 만들어 보내면 사용자에게 \"수정했습니다\" 라고 답하지만 실제로는 아무 변화도\n * 없는 상황이 발생 — 호스트가 missed 를 보고 사용자에게 경고할 수 있도록 별도\n * entry point 제공.\n */\nexport function applyBoardEditPatchVerbose(\n board: any | undefined,\n patch: BoardEditPatch\n): PatchApplyReport {\n let result: any = 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 // scene-only — 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: any, next: any): boolean {\n // applyOp 는 항상 새 객체를 만든다. 따라서 reference 비교가 안 되고 내용 비교 필요.\n // components 는 map/filter 결과 reference 도 다를 수 있으므로 length + 요소 ref 비교.\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 return prev.width === next.width && prev.height === next.height && prev.fillStyle === next.fillStyle\n}\n\nconst SCENE_ONLY_OPS = new Set<BoardEditOp['op']>([\n 'align',\n 'distribute',\n 'group',\n 'ungroup',\n 'zorder',\n 'arrange'\n])\n\nfunction applyOp(board: any, op: BoardEditOp): any {\n switch (op.op) {\n case 'replace':\n return op.board\n case 'add':\n return { ...board, components: [...(board.components || []), op.component] }\n case 'remove':\n // 자식 (group/container 안) refid 도 매칭 — deep search.\n return {\n ...board,\n components: removeComponentDeep(board.components || [], op.refid)\n }\n case 'modify':\n // 자식 refid 도 매칭 — group/container 안의 컴포넌트 수정 가능.\n return {\n ...board,\n components: modifyComponentDeep(board.components || [], op.refid, op.patch)\n }\n case 'modifyBoard': {\n const patch = { ...(op.patch || {}) }\n delete patch.components // 자식 변경은 별도 op\n return mergeComponent(board, patch) // 최상위 board 자체에 deep-merge\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 default:\n return board\n }\n}\n\n/**\n * 주어진 board 상태에서 op 의 inverse 를 계산.\n *\n * Revert 기능 — patch 적용 직전 board + op 로 역연산을 만든다. 호스트가\n * 누적해두면 나중에 역순 실행만으로 복원.\n *\n * add 의 inverse 는 새로 발급될 refid 를 알아야 → 모델 단계에서 계산 불가.\n * 호스트가 scene.add 직후 refid 를 캡처해 직접 만들 것.\n */\nexport function computeInverseOp(board: any, op: BoardEditOp): BoardEditOp | null {\n if (!board) return null\n const components = board.components ?? []\n\n switch (op.op) {\n case 'add':\n return null\n\n case 'remove': {\n // 자식 refid 도 매칭 (deep search)\n const target = findComponentDeep(components, op.refid)\n if (!target) return null\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 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 return { op: 'replace', board: JSON.parse(JSON.stringify(board)) }\n\n case 'zorder': {\n // forward ↔ backward / front ↔ back. front/back 의 inverse 는 정확하지\n // 않을 수 있음 (best-effort) — 정확 inverse 는 호스트가 zorder 직전 index\n // 를 캡처해서 명시 modify 시퀀스로 만들어야.\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 // scene 호출 후에야 변경 결과 알 수 있음. 호스트가 직접 inverse 만든다.\n return null\n\n default:\n return null\n }\n}\n\n/**\n * 컴포넌트에 부분 patch 를 적용 (deep merge).\n * threeD 등 nested object 는 deep merge — 색만 바꾸려고 했을 때 geometry 까지 사라지지 않도록.\n *\n * patch 내 `null` 의 시맨틱 — **\"이 키를 제거하라\"**:\n * - fillStyle/strokeStyle 처럼 `string | object | undefined` 인 필드는 null 로\n * set 하면 typeof null === 'object' 함정에 빠져 다운스트림 (3D 텍스처 등) 이 크래시.\n * - computeInverseOp 가 base 에 키가 없던 (`undefined`) 경우 inverse 를 null 로\n * 발급하므로, 이 시맨틱과 정확히 일치 — inverse 가 자연스럽게 올바로 동작.\n *\n * host (board-modeller-page) 에서 things-scene 의 component.set(merged) 호출 전에 사용.\n */\nexport function mergeComponent(base: any, patch: any): any {\n const out: any = { ...base }\n for (const key of Object.keys(patch)) {\n const patchVal = patch[key]\n if (patchVal === null) {\n delete out[key]\n } else if (isPlainObject(base[key]) && isPlainObject(patchVal)) {\n out[key] = deepMerge(base[key], patchVal)\n } else {\n out[key] = patchVal\n }\n }\n return out\n}\n\nfunction deepMerge(a: any, b: any): any {\n const out: any = { ...a }\n for (const key of Object.keys(b)) {\n const bv = b[key]\n if (bv === null) {\n delete out[key]\n } else if (isPlainObject(a[key]) && isPlainObject(bv)) {\n out[key] = deepMerge(a[key], 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// server/service/apply-patch.ts 와 동일 로직. drift test 가 동등성 보장.\n\nfunction findComponentDeep(components: any[], refid: number): any | undefined {\n for (const c of components) {\n if (!c) continue\n if (c.refid === refid) return c\n if (Array.isArray(c.components) && c.components.length > 0) {\n const sub = findComponentDeep(c.components, refid)\n if (sub) return sub\n }\n }\n return undefined\n}\n\nfunction modifyComponentDeep(components: any[], refid: number, patch: any): any[] {\n let changed = false\n const out = components.map(c => {\n if (!c) return c\n if (c.refid === refid) {\n changed = true\n return mergeComponent(c, patch)\n }\n if (Array.isArray(c.components) && c.components.length > 0) {\n const updated = modifyComponentDeep(c.components, refid, patch)\n if (updated !== c.components) {\n changed = true\n return { ...c, components: updated }\n }\n }\n return c\n })\n return changed ? out : components\n}\n\nfunction removeComponentDeep(components: any[], refid: number): any[] {\n let changed = false\n const out: any[] = []\n for (const c of components) {\n if (!c) {\n out.push(c)\n continue\n }\n if (c.refid === refid) {\n changed = true\n continue\n }\n if (Array.isArray(c.components) && c.components.length > 0) {\n const updated = removeComponentDeep(c.components, refid)\n if (updated !== c.components) {\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,28 @@
1
+ /**
2
+ * `@things-factory/ai-assistant` — **AI 대화의 집.**
3
+ *
4
+ * 세션·메시지·참가자·인박스·구독. 도메인을 모른다: 보드도, 공장도, 자산도.
5
+ *
6
+ * ── 왜 board-ai 에서 나왔나 (2026-09-10) ────────────────────────────────────
7
+ * 시작한 물음은 하나였다. **「왜 board-ai 에 종속되어 있는가.」** 공장에 「지금 몇 대 밀렸나」를
8
+ * 묻는 화면이 보드 편집 권한을 요구했고, 그 문 뒤에 스물다섯이 있는데 쓰는 것은 하나였다.
9
+ *
10
+ * 재 보니 문제는 「공통이냐 아니냐」가 아니었다 — `board-ui` 를 지나 **아홉 개 앱**이 이미 이
11
+ * 기계를 싣고 있었다. 이미 공통이었고, 이름과 게이트만 보드 편집기의 것이었다.
12
+ *
13
+ * ── 자르는 선 ───────────────────────────────────────────────────────────────
14
+ * 물음 하나로 줄였다: **「이것이 보드가 없는 제품에서도 뜻이 있나.」**
15
+ *
16
+ * 여기 세션·메시지·참가자·인박스·구독 인증·앵커
17
+ * board-ai 보드 편집기 — assistant · 패치 · 프리팹 · 컴포넌트 트리 · 스타일링
18
+ *
19
+ * `assistant.ts`(113KB)는 **안 옮겼다.** 거의 전부 보드 편집이다. 가장 큰 파일이 제자리에 있다는
20
+ * 뜻이고, 그래서 이 분리는 생각보다 작았다.
21
+ *
22
+ * ── 의존은 한쪽이다 ─────────────────────────────────────────────────────────
23
+ * `board-ai → ai-assistant`. 반대는 없다. 그것을 위해 두 변을 먼저 끊었다 — `ChatSession` 의
24
+ * 보드 패치 관계와, 리졸버 안에 섞여 있던 보드 op 여섯.
25
+ *
26
+ * 설계 정본은 `things-factory/docs/design/ai-modules.md`.
27
+ */
28
+ export * from './service/index.js';
@@ -0,0 +1,32 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ /**
5
+ * `@things-factory/ai-assistant` — **AI 대화의 집.**
6
+ *
7
+ * 세션·메시지·참가자·인박스·구독. 도메인을 모른다: 보드도, 공장도, 자산도.
8
+ *
9
+ * ── 왜 board-ai 에서 나왔나 (2026-09-10) ────────────────────────────────────
10
+ * 시작한 물음은 하나였다. **「왜 board-ai 에 종속되어 있는가.」** 공장에 「지금 몇 대 밀렸나」를
11
+ * 묻는 화면이 보드 편집 권한을 요구했고, 그 문 뒤에 스물다섯이 있는데 쓰는 것은 하나였다.
12
+ *
13
+ * 재 보니 문제는 「공통이냐 아니냐」가 아니었다 — `board-ui` 를 지나 **아홉 개 앱**이 이미 이
14
+ * 기계를 싣고 있었다. 이미 공통이었고, 이름과 게이트만 보드 편집기의 것이었다.
15
+ *
16
+ * ── 자르는 선 ───────────────────────────────────────────────────────────────
17
+ * 물음 하나로 줄였다: **「이것이 보드가 없는 제품에서도 뜻이 있나.」**
18
+ *
19
+ * 여기 세션·메시지·참가자·인박스·구독 인증·앵커
20
+ * board-ai 보드 편집기 — assistant · 패치 · 프리팹 · 컴포넌트 트리 · 스타일링
21
+ *
22
+ * `assistant.ts`(113KB)는 **안 옮겼다.** 거의 전부 보드 편집이다. 가장 큰 파일이 제자리에 있다는
23
+ * 뜻이고, 그래서 이 분리는 생각보다 작았다.
24
+ *
25
+ * ── 의존은 한쪽이다 ─────────────────────────────────────────────────────────
26
+ * `board-ai → ai-assistant`. 반대는 없다. 그것을 위해 두 변을 먼저 끊었다 — `ChatSession` 의
27
+ * 보드 패치 관계와, 리졸버 안에 섞여 있던 보드 op 여섯.
28
+ *
29
+ * 설계 정본은 `things-factory/docs/design/ai-modules.md`.
30
+ */
31
+ tslib_1.__exportStar(require("./service/index.js"), exports);
32
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../server/index.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,6DAAkC","sourcesContent":["/**\n * `@things-factory/ai-assistant` — **AI 대화의 집.**\n *\n * 세션·메시지·참가자·인박스·구독. 도메인을 모른다: 보드도, 공장도, 자산도.\n *\n * ── 왜 board-ai 에서 나왔나 (2026-09-10) ────────────────────────────────────\n * 시작한 물음은 하나였다. **「왜 board-ai 에 종속되어 있는가.」** 공장에 「지금 몇 대 밀렸나」를\n * 묻는 화면이 보드 편집 권한을 요구했고, 그 문 뒤에 스물다섯이 있는데 쓰는 것은 하나였다.\n *\n * 재 보니 문제는 「공통이냐 아니냐」가 아니었다 — `board-ui` 를 지나 **아홉 개 앱**이 이미 이\n * 기계를 싣고 있었다. 이미 공통이었고, 이름과 게이트만 보드 편집기의 것이었다.\n *\n * ── 자르는 선 ───────────────────────────────────────────────────────────────\n * 물음 하나로 줄였다: **「이것이 보드가 없는 제품에서도 뜻이 있나.」**\n *\n * 여기 세션·메시지·참가자·인박스·구독 인증·앵커\n * board-ai 보드 편집기 — assistant · 패치 · 프리팹 · 컴포넌트 트리 · 스타일링\n *\n * `assistant.ts`(113KB)는 **안 옮겼다.** 거의 전부 보드 편집이다. 가장 큰 파일이 제자리에 있다는\n * 뜻이고, 그래서 이 분리는 생각보다 작았다.\n *\n * ── 의존은 한쪽이다 ─────────────────────────────────────────────────────────\n * `board-ai → ai-assistant`. 반대는 없다. 그것을 위해 두 변을 먼저 끊었다 — `ChatSession` 의\n * 보드 패치 관계와, 리졸버 안에 섞여 있던 보드 op 여섯.\n *\n * 설계 정본은 `things-factory/docs/design/ai-modules.md`.\n */\nexport * from './service/index.js'\n"]}
@@ -0,0 +1,13 @@
1
+ import type { ChatMessage } from './chat-message.js';
2
+ /** chat-message 브로드캐스트 토픽. */
3
+ export declare const CHAT_MESSAGE_TOPIC = "board-ai-chat-message";
4
+ /** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */
5
+ export interface ChatMessagePayload {
6
+ domainId: string;
7
+ sessionId?: string;
8
+ message: ChatMessage;
9
+ }
10
+ /**
11
+ * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
12
+ */
13
+ export declare function chatMessageFilter(domainId: string, sessionId: string): (payload: ChatMessagePayload) => boolean;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CHAT_MESSAGE_TOPIC = void 0;
4
+ exports.chatMessageFilter = chatMessageFilter;
5
+ /** chat-message 브로드캐스트 토픽. */
6
+ exports.CHAT_MESSAGE_TOPIC = 'board-ai-chat-message';
7
+ /**
8
+ * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).
9
+ */
10
+ function chatMessageFilter(domainId, sessionId) {
11
+ return (payload) => !!payload && payload.domainId === domainId && payload.sessionId === sessionId;
12
+ }
13
+ //# sourceMappingURL=chat-message-broadcast.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-message-broadcast.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-broadcast.ts"],"names":[],"mappings":";;;AAuBA,8CAGC;AAhBD,8BAA8B;AACjB,QAAA,kBAAkB,GAAG,uBAAuB,CAAA;AASzD;;GAEG;AACH,SAAgB,iBAAiB,CAAC,QAAgB,EAAE,SAAiB;IACnE,OAAO,CAAC,OAA2B,EAAW,EAAE,CAC9C,CAAC,CAAC,OAAO,IAAI,OAAO,CAAC,QAAQ,KAAK,QAAQ,IAAI,OAAO,CAAC,SAAS,KAAK,SAAS,CAAA;AACjF,CAAC","sourcesContent":["/*\n * chat-message 브로드캐스트 — 순수 로직 (토픽/페이로드/필터). 데코레이터·pubsub import 없음\n * → 단위테스트 가능. 데코레이터 resolver(chat-message-subscription.ts)와 pubsub 의존\n * publishChatMessage 는 별도.\n *\n * patch spine(board-patch-broadcast.ts)과 동일 컨벤션. 다만 채팅 메시지는 board 가 아니라\n * **ChatSession 단위**로 묶이므로 필터 키가 sessionId (+ domain 격리).\n */\nimport type { ChatMessage } from './chat-message.js' // type-only — 런타임 import 아님\n\n/** chat-message 브로드캐스트 토픽. */\nexport const CHAT_MESSAGE_TOPIC = 'board-ai-chat-message'\n\n/** 방송 페이로드 — 구독 필터(domain+session)용 메타 + ChatMessage 본체. */\nexport interface ChatMessagePayload {\n domainId: string\n sessionId?: string\n message: ChatMessage\n}\n\n/**\n * 같은 domain + session 구독자에게만 통과시키는 필터 (도메인 격리 = 보안 핵심).\n */\nexport function chatMessageFilter(domainId: string, sessionId: string) {\n return (payload: ChatMessagePayload): boolean =>\n !!payload && payload.domainId === domainId && payload.sessionId === sessionId\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import { type ChatMessagePayload } from './chat-message-broadcast.js';
2
+ /**
3
+ * ChatMessage 영속 직후 호출 — 같은 세션 구독자에게 방송.
4
+ * sessionId 없으면 skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.
5
+ */
6
+ export declare function publishChatMessage(payload: ChatMessagePayload): void;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.publishChatMessage = publishChatMessage;
4
+ /*
5
+ * chat-message 방송 publisher — pubsub 의존, 데코레이터 없음.
6
+ *
7
+ * 데코레이터 resolver(chat-message-subscription.ts)와 분리한 이유: babel-jest 가 파라미터
8
+ * 데코레이터(@Root/@Arg)를 파싱 못 해 그 파일을 테스트에서 import 할 수 없다. 이 모듈은
9
+ * 데코레이터가 없어 통합 테스트가 실제 publish→방송 경로를 직접 구동할 수 있다.
10
+ */
11
+ const shell_1 = require("@things-factory/shell");
12
+ const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
13
+ /**
14
+ * ChatMessage 영속 직후 호출 — 같은 세션 구독자에게 방송.
15
+ * sessionId 없으면 skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.
16
+ */
17
+ function publishChatMessage(payload) {
18
+ if (!payload.sessionId)
19
+ return;
20
+ try {
21
+ shell_1.pubsub.publish(chat_message_broadcast_js_1.CHAT_MESSAGE_TOPIC, payload);
22
+ }
23
+ catch {
24
+ /* 방송 생략 — 영속은 이미 끝남 */
25
+ }
26
+ }
27
+ //# sourceMappingURL=chat-message-publish.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-message-publish.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-publish.ts"],"names":[],"mappings":";;AAcA,gDAOC;AArBD;;;;;;GAMG;AACH,iDAA8C;AAC9C,2EAAyF;AAEzF;;;GAGG;AACH,SAAgB,kBAAkB,CAAC,OAA2B;IAC5D,IAAI,CAAC,OAAO,CAAC,SAAS;QAAE,OAAM;IAC9B,IAAI,CAAC;QACH,cAAM,CAAC,OAAO,CAAC,8CAAkB,EAAE,OAAO,CAAC,CAAA;IAC7C,CAAC;IAAC,MAAM,CAAC;QACP,uBAAuB;IACzB,CAAC;AACH,CAAC","sourcesContent":["/*\n * chat-message 방송 publisher — pubsub 의존, 데코레이터 없음.\n *\n * 데코레이터 resolver(chat-message-subscription.ts)와 분리한 이유: babel-jest 가 파라미터\n * 데코레이터(@Root/@Arg)를 파싱 못 해 그 파일을 테스트에서 import 할 수 없다. 이 모듈은\n * 데코레이터가 없어 통합 테스트가 실제 publish→방송 경로를 직접 구동할 수 있다.\n */\nimport { pubsub } from '@things-factory/shell'\nimport { CHAT_MESSAGE_TOPIC, type ChatMessagePayload } from './chat-message-broadcast.js'\n\n/**\n * ChatMessage 영속 직후 호출 — 같은 세션 구독자에게 방송.\n * sessionId 없으면 skip. pubsub 미가용/실패해도 본 흐름(영속 완료)을 막지 않는다.\n */\nexport function publishChatMessage(payload: ChatMessagePayload): void {\n if (!payload.sessionId) return\n try {\n pubsub.publish(CHAT_MESSAGE_TOPIC, payload)\n } catch {\n /* 방송 생략 — 영속은 이미 끝남 */\n }\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import { ChatMessage } from './chat-message.js';
2
+ import { type ChatMessagePayload } from './chat-message-broadcast.js';
3
+ export { publishChatMessage } from './chat-message-publish.js';
4
+ export { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js';
5
+ export type { ChatMessagePayload } from './chat-message-broadcast.js';
6
+ export declare class ChatMessageSubscription {
7
+ chatMessageActivity(payload: ChatMessagePayload, sessionId: string): ChatMessage;
8
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChatMessageSubscription = exports.chatMessageFilter = exports.CHAT_MESSAGE_TOPIC = exports.publishChatMessage = void 0;
4
+ const tslib_1 = require("tslib");
5
+ /*
6
+ * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.
7
+ *
8
+ * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터
9
+ * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로
10
+ * 소비처는 그대로다.
11
+ *
12
+ * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →
13
+ * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.
14
+ * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).
15
+ *
16
+ * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.
17
+ * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).
18
+ * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.
19
+ */
20
+ const type_graphql_1 = require("type-graphql");
21
+ const graphql_yoga_1 = require("graphql-yoga");
22
+ const shell_1 = require("@things-factory/shell");
23
+ const chat_message_js_1 = require("./chat-message.js");
24
+ const chat_message_broadcast_js_1 = require("./chat-message-broadcast.js");
25
+ const subscribe_auth_js_1 = require("../subscribe-auth.js");
26
+ // publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).
27
+ var chat_message_publish_js_1 = require("./chat-message-publish.js");
28
+ Object.defineProperty(exports, "publishChatMessage", { enumerable: true, get: function () { return chat_message_publish_js_1.publishChatMessage; } });
29
+ var chat_message_broadcast_js_2 = require("./chat-message-broadcast.js");
30
+ Object.defineProperty(exports, "CHAT_MESSAGE_TOPIC", { enumerable: true, get: function () { return chat_message_broadcast_js_2.CHAT_MESSAGE_TOPIC; } });
31
+ Object.defineProperty(exports, "chatMessageFilter", { enumerable: true, get: function () { return chat_message_broadcast_js_2.chatMessageFilter; } });
32
+ let ChatMessageSubscription = class ChatMessageSubscription {
33
+ chatMessageActivity(payload, sessionId) {
34
+ return payload.message;
35
+ }
36
+ };
37
+ exports.ChatMessageSubscription = ChatMessageSubscription;
38
+ tslib_1.__decorate([
39
+ (0, type_graphql_1.Subscription)({
40
+ description: 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',
41
+ subscribe: async ({ args, context }) => {
42
+ const { domain, user } = context.state;
43
+ const { sessionId } = args;
44
+ await (0, subscribe_auth_js_1.assertDomainSubscribeAllowed)(domain, user, (d, u) => process.superUserGranted(d, u));
45
+ return (0, graphql_yoga_1.pipe)(shell_1.pubsub.subscribe(chat_message_broadcast_js_1.CHAT_MESSAGE_TOPIC), (0, graphql_yoga_1.filter)((0, chat_message_broadcast_js_1.chatMessageFilter)(domain.id, sessionId)));
46
+ }
47
+ }),
48
+ tslib_1.__param(0, (0, type_graphql_1.Root)()),
49
+ tslib_1.__param(1, (0, type_graphql_1.Arg)('sessionId')),
50
+ tslib_1.__metadata("design:type", Function),
51
+ tslib_1.__metadata("design:paramtypes", [Object, String]),
52
+ tslib_1.__metadata("design:returntype", chat_message_js_1.ChatMessage)
53
+ ], ChatMessageSubscription.prototype, "chatMessageActivity", null);
54
+ exports.ChatMessageSubscription = ChatMessageSubscription = tslib_1.__decorate([
55
+ (0, type_graphql_1.Resolver)(chat_message_js_1.ChatMessage)
56
+ ], ChatMessageSubscription);
57
+ //# sourceMappingURL=chat-message-subscription.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-message-subscription.js","sourceRoot":"","sources":["../../../server/service/chat-message/chat-message-subscription.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,+CAAgE;AAChE,+CAA2C;AAC3C,iDAA8C;AAC9C,uDAA+C;AAC/C,2EAA4G;AAC5G,4DAAmE;AAEnE,8EAA8E;AAC9E,qEAA8D;AAArD,6HAAA,kBAAkB,OAAA;AAC3B,yEAAmF;AAA1E,+HAAA,kBAAkB,OAAA;AAAE,8HAAA,iBAAiB,OAAA;AAIvC,IAAM,uBAAuB,GAA7B,MAAM,uBAAuB;IAalC,mBAAmB,CAAS,OAA2B,EAAoB,SAAiB;QAC1F,OAAO,OAAO,CAAC,OAAO,CAAA;IACxB,CAAC;CACF,CAAA;AAhBY,0DAAuB;AAalC;IAZC,IAAA,2BAAY,EAAC;QACZ,WAAW,EACT,gGAAgG;QAClG,SAAS,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE;YACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;YACtC,MAAM,EAAE,SAAS,EAAE,GAAG,IAAI,CAAA;YAE1B,MAAM,IAAA,gDAA4B,EAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,gBAAgB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAA;YAE1F,OAAO,IAAA,mBAAI,EAAC,cAAM,CAAC,SAAS,CAAC,8CAAkB,CAAC,EAAE,IAAA,qBAAM,EAAC,IAAA,6CAAiB,EAAC,MAAM,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC,CAAC,CAAA;QACpG,CAAC;KACF,CAAC;IACmB,mBAAA,IAAA,mBAAI,GAAE,CAAA;IAA+B,mBAAA,IAAA,kBAAG,EAAC,WAAW,CAAC,CAAA;;;4CAAqB,6BAAW;kEAEzG;kCAfU,uBAAuB;IADnC,IAAA,uBAAQ,EAAC,6BAAW,CAAC;GACT,uBAAuB,CAgBnC","sourcesContent":["/*\n * ChatMessageSubscription — 그룹챗 메시지 실시간 방송.\n *\n * 클래스 이름이 `BoardChatMessageSubscription` 이었다. 전선 이름(`chatMessageActivity`)은 처음부터\n * 중립이었고 클래스 이름만 보드였다 — 중립 패키지로 옮기면서 고쳤다(2026-09-10). 전선이 안 바뀌므로\n * 소비처는 그대로다.\n *\n * ChatMessage(사용자 / AI / system) 가 영속될 때마다 같은 세션 구독자에게 방송 →\n * 그룹챗의 척추. 클라이언트(board-ai-chat)가 구독해 원격 메시지를 transcript 에 append.\n * 자기 발신 에코는 클라이언트가 message.id 로 dedup (mutation 응답으로 이미 렌더).\n *\n * patch spine(board-patch-subscription.ts)과 동일 패턴 — PubSub + graphql-ws, domain 격리.\n * 차이: 필터 키가 board 가 아닌 sessionId (채팅은 ChatSession 단위).\n * 순수 로직(토픽/필터)은 chat-message-broadcast.ts (테스트 가능). 여기엔 pubsub 의존만.\n */\nimport { Resolver, Subscription, Root, Arg } from 'type-graphql'\nimport { filter, pipe } from 'graphql-yoga'\nimport { pubsub } from '@things-factory/shell'\nimport { ChatMessage } from './chat-message.js'\nimport { CHAT_MESSAGE_TOPIC, chatMessageFilter, type ChatMessagePayload } from './chat-message-broadcast.js'\nimport { assertDomainSubscribeAllowed } from '../subscribe-auth.js'\n\n// publishChatMessage 는 데코레이터 없는 chat-message-publish.ts 에 분리 (테스트 import 가능).\nexport { publishChatMessage } from './chat-message-publish.js'\nexport { CHAT_MESSAGE_TOPIC, chatMessageFilter } from './chat-message-broadcast.js'\nexport type { ChatMessagePayload } from './chat-message-broadcast.js'\n\n@Resolver(ChatMessage)\nexport class ChatMessageSubscription {\n @Subscription({\n description:\n 'Subscribes to chat messages (user / assistant / system) of a session for real-time group chat.',\n subscribe: async ({ args, context }) => {\n const { domain, user } = context.state\n const { sessionId } = args\n\n await assertDomainSubscribeAllowed(domain, user, (d, u) => process.superUserGranted(d, u))\n\n return pipe(pubsub.subscribe(CHAT_MESSAGE_TOPIC), filter(chatMessageFilter(domain.id, sessionId)))\n }\n })\n chatMessageActivity(@Root() payload: ChatMessagePayload, @Arg('sessionId') sessionId: string): ChatMessage {\n return payload.message\n }\n}\n"]}
@@ -0,0 +1,68 @@
1
+ import { User } from '@things-factory/auth-base';
2
+ import { ChatSession } from '../chat-session/chat-session.js';
3
+ export declare class ChatMessage {
4
+ readonly id?: string;
5
+ session?: ChatSession;
6
+ sessionId?: string;
7
+ role: string;
8
+ content: string;
9
+ relatedPatchId?: string;
10
+ relatedImportSessionId?: string;
11
+ /**
12
+ * AI 가 응답을 만드는 동안 호출한 도구 trace — JSON-stringified.
13
+ *
14
+ * 컬럼은 string (PatchEntry.ops 와 동일 패턴), GraphQL 노출은 toolUsagesJson getter
15
+ * 로 parse 된 객체. UI 의 fold-able 박스 ("AI 가 이런 도구를 사용했습니다") 가
16
+ * 페이지 reload / loadHistory 후에도 살아있도록 영속.
17
+ *
18
+ * 형태: ToolUsage[] (assistant.ts 의 시간순 trace, summarizeToolResult 로 압축).
19
+ * null 이면 도구 호출 없었던 메시지 (text-only 응답).
20
+ */
21
+ toolUsages?: string;
22
+ /** GraphQL 노출용 — JSON 배열로 parse. */
23
+ get toolUsagesJson(): any;
24
+ /**
25
+ * 접지 경고 — 이 답이 언급했으나 근거(프롬프트·보드 문맥·이력·도구 결과)에 없던 식별자 목록.
26
+ *
27
+ * **왜 영속하는가**: 도크의 협의는 여러 사람이 며칠에 걸쳐 읽는다. 경고가 그 자리에서만
28
+ * 보이고 새로 고치면 사라지면, 나중에 읽는 사람은 검증되지 않은 문장을 검증된 것으로 읽는다.
29
+ * 다시 계산할 수도 없다 — 근거였던 도구 결과 원문은 남지 않고 압축본만 남기 때문이다.
30
+ *
31
+ * 형태: string[] 를 JSON-stringify. 비었으면 null(= 접지 정상).
32
+ */
33
+ groundingWarnings?: string;
34
+ /** GraphQL 노출용 — 식별자 배열로 parse. */
35
+ get groundingWarningsJson(): any;
36
+ /**
37
+ * 메시지를 일으킨 사용자.
38
+ * - role='user' → 보낸 사용자
39
+ * - role='assistant' → 그 응답을 트리거한 사용자 (직전 user 메시지의 작성자)
40
+ * - role='system' → 시스템 액션을 일으킨 사용자 (예: 직접 편집한 사용자)
41
+ *
42
+ * nullable — legacy 메시지 (이 필드 추가 이전에 작성) 는 null. 또한 ad-hoc 모드 / 시스템
43
+ * 자동 메시지 중 사용자 컨텍스트가 없는 경우도 null 가능.
44
+ */
45
+ creator?: User;
46
+ creatorId?: string;
47
+ updater?: User;
48
+ updaterId?: string;
49
+ /**
50
+ * 부모 메시지 — 이 메시지가 "응답하는 대상" / "이어가는 메시지" 의 id.
51
+ *
52
+ * 의미 (role 별):
53
+ * - 'user' → 직전 in-session 메시지 (보통 assistant 또는 system). 첫 turn 은 null.
54
+ * - 'assistant' → 이 응답이 답하는 user 메시지. (regenerate 로 같은 user 메시지를 부모로
55
+ * 하는 형제 assistant 메시지가 생기면 자연스럽게 branching tree 가 됨)
56
+ * - 'system' → 직전 in-session 메시지 (timeline 연속성 유지).
57
+ *
58
+ * 이 한 컬럼으로 향후 어떤 흐름이든 표현 가능 — linear chain / branching (regenerate) /
59
+ * tree (replies-to-specific-message) 등. UI/쿼리는 점진 도입.
60
+ *
61
+ * onDelete: 'SET NULL' — 부모 메시지가 삭제돼도 child 는 남되 dangling 마킹.
62
+ */
63
+ parentMessage?: ChatMessage;
64
+ parentMessageId?: string;
65
+ createdAt?: Date;
66
+ updatedAt?: Date;
67
+ deletedAt?: Date;
68
+ }