@things-factory/board-ai 10.1.24 → 10.1.26

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 (107) hide show
  1. package/PLAN-styling-tools.md +1 -1
  2. package/dist-client/server/service/assistant.d.ts +5 -4
  3. package/dist-client/server/service/assistant.js +35 -35
  4. package/dist-client/server/service/assistant.js.map +1 -1
  5. package/dist-client/server/service/styling/clear-tools.d.ts +2 -2
  6. package/dist-client/server/service/styling/clear-tools.js.map +1 -1
  7. package/dist-client/server/service/styling/copy-tools.d.ts +3 -3
  8. package/dist-client/server/service/styling/copy-tools.js +1 -1
  9. package/dist-client/server/service/styling/copy-tools.js.map +1 -1
  10. package/dist-client/server/service/styling/effect-tools.d.ts +2 -2
  11. package/dist-client/server/service/styling/effect-tools.js.map +1 -1
  12. package/dist-client/server/service/styling/fill-tools.d.ts +3 -3
  13. package/dist-client/server/service/styling/fill-tools.js +1 -1
  14. package/dist-client/server/service/styling/fill-tools.js.map +1 -1
  15. package/dist-client/server/service/styling/material-tools.d.ts +2 -2
  16. package/dist-client/server/service/styling/material-tools.js.map +1 -1
  17. package/dist-client/server/service/styling/registry.d.ts +6 -6
  18. package/dist-client/server/service/styling/registry.js +2 -2
  19. package/dist-client/server/service/styling/registry.js.map +1 -1
  20. package/dist-client/server/service/styling/stroke-tools.d.ts +2 -2
  21. package/dist-client/server/service/styling/stroke-tools.js.map +1 -1
  22. package/dist-client/server/service/styling/text-tools.d.ts +2 -2
  23. package/dist-client/server/service/styling/text-tools.js.map +1 -1
  24. package/dist-client/server/service/types.d.ts +6 -148
  25. package/dist-client/server/service/types.js.map +1 -1
  26. package/dist-client/server/service/validation/board-model-schema.d.ts +262 -262
  27. package/dist-client/server/service/validation/board-model-schema.js +4 -4
  28. package/dist-client/server/service/validation/board-model-schema.js.map +1 -1
  29. package/dist-client/server/service/validation/tool-validation.js +5 -5
  30. package/dist-client/server/service/validation/tool-validation.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-server/index.d.ts +2 -2
  33. package/dist-server/index.js +2 -2
  34. package/dist-server/index.js.map +1 -1
  35. package/dist-server/service/assistant.d.ts +5 -4
  36. package/dist-server/service/assistant.js +37 -37
  37. package/dist-server/service/assistant.js.map +1 -1
  38. package/dist-server/service/board-ai-resolver.js +2 -2
  39. package/dist-server/service/board-ai-resolver.js.map +1 -1
  40. package/dist-server/service/index.d.ts +0 -1
  41. package/dist-server/service/index.js +0 -1
  42. package/dist-server/service/index.js.map +1 -1
  43. package/dist-server/service/patch-entry/board-patch-subscription.js +1 -1
  44. package/dist-server/service/patch-entry/board-patch-subscription.js.map +1 -1
  45. package/dist-server/service/patch-entry/patch-entry.d.ts +1 -1
  46. package/dist-server/service/patch-entry/patch-entry.js +1 -1
  47. package/dist-server/service/patch-entry/patch-entry.js.map +1 -1
  48. package/dist-server/service/styling/clear-tools.d.ts +2 -2
  49. package/dist-server/service/styling/clear-tools.js.map +1 -1
  50. package/dist-server/service/styling/copy-tools.d.ts +3 -3
  51. package/dist-server/service/styling/copy-tools.js +1 -1
  52. package/dist-server/service/styling/copy-tools.js.map +1 -1
  53. package/dist-server/service/styling/effect-tools.d.ts +2 -2
  54. package/dist-server/service/styling/effect-tools.js.map +1 -1
  55. package/dist-server/service/styling/fill-tools.d.ts +3 -3
  56. package/dist-server/service/styling/fill-tools.js +1 -1
  57. package/dist-server/service/styling/fill-tools.js.map +1 -1
  58. package/dist-server/service/styling/material-tools.d.ts +2 -2
  59. package/dist-server/service/styling/material-tools.js.map +1 -1
  60. package/dist-server/service/styling/registry.d.ts +6 -6
  61. package/dist-server/service/styling/registry.js +2 -2
  62. package/dist-server/service/styling/registry.js.map +1 -1
  63. package/dist-server/service/styling/stroke-tools.d.ts +2 -2
  64. package/dist-server/service/styling/stroke-tools.js.map +1 -1
  65. package/dist-server/service/styling/text-tools.d.ts +2 -2
  66. package/dist-server/service/styling/text-tools.js.map +1 -1
  67. package/dist-server/service/types.d.ts +6 -148
  68. package/dist-server/service/types.js.map +1 -1
  69. package/dist-server/service/validation/board-model-schema.d.ts +262 -262
  70. package/dist-server/service/validation/board-model-schema.js +4 -4
  71. package/dist-server/service/validation/board-model-schema.js.map +1 -1
  72. package/dist-server/service/validation/tool-validation.js +5 -5
  73. package/dist-server/service/validation/tool-validation.js.map +1 -1
  74. package/dist-server/tsconfig.tsbuildinfo +1 -1
  75. package/logs/.5e5d741d8b7784a2fbad65eedc0fd46946aaf6f2-audit.json +5 -0
  76. package/logs/connections-2026-09-17-16.log +0 -0
  77. package/package.json +8 -7
  78. package/server/index.ts +2 -2
  79. package/server/service/assistant-integration.test.ts +44 -44
  80. package/server/service/assistant-llm-smoke.test.ts +5 -5
  81. package/server/service/assistant.test.ts +86 -86
  82. package/server/service/assistant.ts +41 -43
  83. package/server/service/board-ai-resolver.ts +2 -2
  84. package/server/service/index.ts +0 -1
  85. package/server/service/patch-entry/board-patch-subscription.ts +1 -1
  86. package/server/service/patch-entry/patch-entry.ts +2 -2
  87. package/server/service/styling/clear-tools.ts +2 -2
  88. package/server/service/styling/copy-tools.ts +3 -3
  89. package/server/service/styling/effect-tools.ts +2 -2
  90. package/server/service/styling/fill-tools.test.ts +1 -1
  91. package/server/service/styling/fill-tools.ts +4 -4
  92. package/server/service/styling/material-tools.ts +2 -2
  93. package/server/service/styling/registry.ts +6 -6
  94. package/server/service/styling/stroke-tools.test.ts +1 -1
  95. package/server/service/styling/stroke-tools.ts +3 -3
  96. package/server/service/styling/text-tools.ts +2 -2
  97. package/server/service/tool-exposure.test.ts +7 -7
  98. package/server/service/types.ts +9 -110
  99. package/server/service/validation/board-model-schema.test.ts +3 -3
  100. package/server/service/validation/board-model-schema.ts +4 -4
  101. package/server/service/validation/tool-validation.ts +5 -5
  102. package/dist-server/service/apply-patch.d.ts +0 -47
  103. package/dist-server/service/apply-patch.js +0 -369
  104. package/dist-server/service/apply-patch.js.map +0 -1
  105. package/server/service/apply-patch-drift.test.ts +0 -831
  106. package/server/service/apply-patch.test.ts +0 -757
  107. package/server/service/apply-patch.ts +0 -411
@@ -1 +1 @@
1
- {"version":3,"file":"stroke-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/stroke-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setStroke — 컴포넌트 테두리 / 라인 스타일.\n *\n * **things-scene 의 실제 데이터 형식 (drawer/stroke.ts 검증):**\n * - `state.strokeStyle: string` (색상 — Canvas API 의 strokeStyle 에 직접 대입되는 CSS color)\n * - `state.lineWidth: number` (top-level)\n * - `state.lineDash: string` (top-level — preset 이름. drawer 가 lineWidth 기반으로 배열 변환)\n * valid: 'solid' | 'round-dot' | 'square-dot' | 'dash' | 'dash-dot' | 'long-dash' | 'long-dash-dot' | 'long-dash-dot-dot'\n * - `state.lineCap: 'butt' | 'round' | 'square'` (top-level)\n * - `state.lineJoin: 'miter' | 'round' | 'bevel'` (top-level)\n *\n * 인터페이스의 `StrokeStyle` 객체 구조는 stale. 사용처는 모두 top-level 직접 접근.\n *\n * 입력 단순화 — AI 친화적 alias 도 받아 things-scene preset 으로 매핑:\n * AI 'dashed' → 'dash', 'dotted' → 'square-dot', 'dashdot' → 'dash-dot'.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetStrokeInput {\n refids: number[]\n /** synthetic — false 면 lineWidth=0 으로 매핑 (drawer 가 안 그림). 모델에 enabled 필드는 안 박힘. */\n enabled?: boolean\n /** state.strokeStyle 의 canonical 이름. CSS color string. */\n strokeStyle?: string\n /** state.lineWidth 의 canonical 이름. pixels. */\n lineWidth?: number\n /** preset 이름. AI 친화 alias 자동 매핑 — 'dashed' / 'dotted' / 'dashdot' 도 가능. */\n dash?:\n | 'solid'\n | 'dashed'\n | 'dotted'\n | 'dashdot'\n | 'round-dot'\n | 'square-dot'\n | 'dash'\n | 'dash-dot'\n | 'long-dash'\n | 'long-dash-dot'\n | 'long-dash-dot-dot'\n | string\n lineCap?: 'butt' | 'round' | 'square'\n lineJoin?: 'miter' | 'round' | 'bevel'\n}\n\nexport interface SetStrokeResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\n/** AI 친화 alias → things-scene 의 실제 preset 이름. */\nconst DASH_ALIASES: Record<string, string> = {\n dashed: 'dash',\n dotted: 'square-dot',\n dashdot: 'dash-dot'\n}\n\nconst VALID_DASH = new Set([\n 'solid',\n 'round-dot',\n 'square-dot',\n 'dash',\n 'dash-dot',\n 'long-dash',\n 'long-dash-dot',\n 'long-dash-dot-dot'\n])\n\n// Canvas CanvasRenderingContext2D 가 허용하는 lineCap / lineJoin 만.\n// 잘못된 값은 Canvas 가 silent ignore → AI 가 'flat' / 'cap' 등을 보내도\n// 효과 없음. 명시 검증으로 ops 미실행 + 명확한 에러.\nconst VALID_LINE_CAP = new Set(['butt', 'round', 'square'])\nconst VALID_LINE_JOIN = new Set(['miter', 'round', 'bevel'])\n\nexport function buildSetStrokeOps(input: SetStrokeInput): SetStrokeResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array of numbers'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const noOp =\n input.enabled === undefined &&\n input.strokeStyle === undefined &&\n input.lineWidth === undefined &&\n input.dash === undefined &&\n input.lineCap === undefined &&\n input.lineJoin === undefined\n if (noOp) {\n return { ops: [], errors: ['No stroke changes specified'] }\n }\n\n // 검증\n if (input.strokeStyle !== undefined) {\n const v = validateColor(input.strokeStyle)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid strokeStyle'] }\n }\n if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {\n return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] }\n }\n if (input.lineCap !== undefined && !VALID_LINE_CAP.has(input.lineCap)) {\n return {\n ops: [],\n errors: [`Unknown lineCap: '${input.lineCap}'. Valid: ${[...VALID_LINE_CAP].join(' / ')}.`]\n }\n }\n if (input.lineJoin !== undefined && !VALID_LINE_JOIN.has(input.lineJoin)) {\n return {\n ops: [],\n errors: [`Unknown lineJoin: '${input.lineJoin}'. Valid: ${[...VALID_LINE_JOIN].join(' / ')}.`]\n }\n }\n\n // dash 정규화 — alias → 실제 preset → 검증\n let dashValue: string | undefined\n if (input.dash !== undefined) {\n const raw = String(input.dash)\n const resolved = DASH_ALIASES[raw] ?? raw\n if (!VALID_DASH.has(resolved)) {\n return {\n ops: [],\n errors: [\n `Unknown dash: '${input.dash}'. Valid: ${[...VALID_DASH].join(' / ')} (or alias: dashed/dotted/dashdot).`\n ]\n }\n }\n dashValue = resolved\n }\n\n // patch 구성 — canonical 이름 그대로. 매핑 X. (color 만 normalizeColor 통과 — 입력 정규화는 mapping 이 아닌 처리)\n const patch: any = {}\n if (input.strokeStyle !== undefined) patch.strokeStyle = normalizeColor(input.strokeStyle)\n if (input.lineWidth !== undefined) patch.lineWidth = input.lineWidth\n if (dashValue !== undefined) patch.lineDash = dashValue\n if (input.lineCap !== undefined) patch.lineCap = input.lineCap\n if (input.lineJoin !== undefined) patch.lineJoin = input.lineJoin\n\n // synthetic enabled — false 면 lineWidth 0 으로 변환 (drawer 가 안 그림 — drawer/stroke.ts:9)\n if (input.enabled === false) {\n patch.lineWidth = 0\n }\n\n const ops: BoardEditOp[] = refids.map(refid => ({ op: 'modify', refid, patch }))\n return { ops, errors: [] }\n}\n"]}
1
+ {"version":3,"file":"stroke-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/stroke-tools.ts"],"names":[],"mappings":"AAiBA,OAAO,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAiC7D,iDAAiD;AACjD,MAAM,YAAY,GAA2B;IAC3C,MAAM,EAAE,MAAM;IACd,MAAM,EAAE,YAAY;IACpB,OAAO,EAAE,UAAU;CACpB,CAAA;AAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC;IACzB,OAAO;IACP,WAAW;IACX,YAAY;IACZ,MAAM;IACN,UAAU;IACV,WAAW;IACX,eAAe;IACf,mBAAmB;CACpB,CAAC,CAAA;AAEF,+DAA+D;AAC/D,6DAA6D;AAC7D,mCAAmC;AACnC,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAA;AAC3D,MAAM,eAAe,GAAG,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAA;AAE5D,MAAM,UAAU,iBAAiB,CAAC,KAAqB;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9D,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,6CAA6C,CAAC,EAAE,CAAA;IAC7E,CAAC;IACD,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAA;IACpF,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,yBAAyB,CAAC,EAAE,CAAA;IAEhF,MAAM,IAAI,GACR,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,WAAW,KAAK,SAAS;QAC/B,KAAK,CAAC,SAAS,KAAK,SAAS;QAC7B,KAAK,CAAC,IAAI,KAAK,SAAS;QACxB,KAAK,CAAC,OAAO,KAAK,SAAS;QAC3B,KAAK,CAAC,QAAQ,KAAK,SAAS,CAAA;IAC9B,IAAI,IAAI,EAAE,CAAC;QACT,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,6BAA6B,CAAC,EAAE,CAAA;IAC7D,CAAC;IAED,KAAK;IACL,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;QACpC,MAAM,CAAC,GAAG,aAAa,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;QAC1C,IAAI,CAAC,CAAC,CAAC,KAAK;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,qBAAqB,CAAC,EAAE,CAAA;IAC9E,CAAC;IACD,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,EAAE,CAAC;QAChG,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,sBAAsB,KAAK,CAAC,SAAS,EAAE,CAAC,EAAE,CAAA;IACvE,CAAC;IACD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC;QACtE,OAAO;YACL,GAAG,EAAE,EAAE;YACP,MAAM,EAAE,CAAC,qBAAqB,KAAK,CAAC,OAAO,aAAa,CAAC,GAAG,cAAc,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;SAC5F,CAAA;IACH,CAAC;IACD,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,eAAe,CAAC,GAAG,CAAC,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzE,OAAO;YACL,GAAG,EAAE,EAAE;YACP,MAAM,EAAE,CAAC,sBAAsB,KAAK,CAAC,QAAQ,aAAa,CAAC,GAAG,eAAe,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;SAC/F,CAAA;IACH,CAAC;IAED,oCAAoC;IACpC,IAAI,SAA6B,CAAA;IACjC,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;QAC9B,MAAM,QAAQ,GAAG,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAA;QACzC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC9B,OAAO;gBACL,GAAG,EAAE,EAAE;gBACP,MAAM,EAAE;oBACN,kBAAkB,KAAK,CAAC,IAAI,aAAa,CAAC,GAAG,UAAU,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,qCAAqC;iBAC1G;aACF,CAAA;QACH,CAAC;QACD,SAAS,GAAG,QAAQ,CAAA;IACtB,CAAC;IAED,2FAA2F;IAC3F,MAAM,KAAK,GAAQ,EAAE,CAAA;IACrB,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS;QAAE,KAAK,CAAC,WAAW,GAAG,cAAc,CAAC,KAAK,CAAC,WAAW,CAAC,CAAA;IAC1F,IAAI,KAAK,CAAC,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,SAAS,CAAA;IACpE,IAAI,SAAS,KAAK,SAAS;QAAE,KAAK,CAAC,QAAQ,GAAG,SAAS,CAAA;IACvD,IAAI,KAAK,CAAC,OAAO,KAAK,SAAS;QAAE,KAAK,CAAC,OAAO,GAAG,KAAK,CAAC,OAAO,CAAA;IAC9D,IAAI,KAAK,CAAC,QAAQ,KAAK,SAAS;QAAE,KAAK,CAAC,QAAQ,GAAG,KAAK,CAAC,QAAQ,CAAA;IAEjE,qFAAqF;IACrF,IAAI,KAAK,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;QAC5B,KAAK,CAAC,SAAS,GAAG,CAAC,CAAA;IACrB,CAAC;IAED,MAAM,GAAG,GAAkB,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAA;IAChF,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,EAAE,CAAA;AAC5B,CAAC","sourcesContent":["/**\n * setStroke — 컴포넌트 테두리 / 라인 스타일.\n *\n * **things-scene 의 실제 데이터 형식 (drawer/stroke.ts 검증):**\n * - `state.strokeStyle: string` (색상 — Canvas API 의 strokeStyle 에 직접 대입되는 CSS color)\n * - `state.lineWidth: number` (top-level)\n * - `state.lineDash: string` (top-level — preset 이름. drawer 가 lineWidth 기반으로 배열 변환)\n * valid: 'solid' | 'round-dot' | 'square-dot' | 'dash' | 'dash-dot' | 'long-dash' | 'long-dash-dot' | 'long-dash-dot-dot'\n * - `state.lineCap: 'butt' | 'round' | 'square'` (top-level)\n * - `state.lineJoin: 'miter' | 'round' | 'bevel'` (top-level)\n *\n * 인터페이스의 `StrokeStyle` 객체 구조는 stale. 사용처는 모두 top-level 직접 접근.\n *\n * 입력 단순화 — AI 친화적 alias 도 받아 things-scene preset 으로 매핑:\n * AI 'dashed' → 'dash', 'dotted' → 'square-dot', 'dashdot' → 'dash-dot'.\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetStrokeInput {\n refids: number[]\n /** synthetic — false 면 lineWidth=0 으로 매핑 (drawer 가 안 그림). 모델에 enabled 필드는 안 박힘. */\n enabled?: boolean\n /** state.strokeStyle 의 canonical 이름. CSS color string. */\n strokeStyle?: string\n /** state.lineWidth 의 canonical 이름. pixels. */\n lineWidth?: number\n /** preset 이름. AI 친화 alias 자동 매핑 — 'dashed' / 'dotted' / 'dashdot' 도 가능. */\n dash?:\n | 'solid'\n | 'dashed'\n | 'dotted'\n | 'dashdot'\n | 'round-dot'\n | 'square-dot'\n | 'dash'\n | 'dash-dot'\n | 'long-dash'\n | 'long-dash-dot'\n | 'long-dash-dot-dot'\n | string\n lineCap?: 'butt' | 'round' | 'square'\n lineJoin?: 'miter' | 'round' | 'bevel'\n}\n\nexport interface SetStrokeResult {\n ops: SceneEditOp[]\n errors: string[]\n}\n\n/** AI 친화 alias → things-scene 의 실제 preset 이름. */\nconst DASH_ALIASES: Record<string, string> = {\n dashed: 'dash',\n dotted: 'square-dot',\n dashdot: 'dash-dot'\n}\n\nconst VALID_DASH = new Set([\n 'solid',\n 'round-dot',\n 'square-dot',\n 'dash',\n 'dash-dot',\n 'long-dash',\n 'long-dash-dot',\n 'long-dash-dot-dot'\n])\n\n// Canvas CanvasRenderingContext2D 가 허용하는 lineCap / lineJoin 만.\n// 잘못된 값은 Canvas 가 silent ignore → AI 가 'flat' / 'cap' 등을 보내도\n// 효과 없음. 명시 검증으로 ops 미실행 + 명확한 에러.\nconst VALID_LINE_CAP = new Set(['butt', 'round', 'square'])\nconst VALID_LINE_JOIN = new Set(['miter', 'round', 'bevel'])\n\nexport function buildSetStrokeOps(input: SetStrokeInput): SetStrokeResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array of numbers'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const noOp =\n input.enabled === undefined &&\n input.strokeStyle === undefined &&\n input.lineWidth === undefined &&\n input.dash === undefined &&\n input.lineCap === undefined &&\n input.lineJoin === undefined\n if (noOp) {\n return { ops: [], errors: ['No stroke changes specified'] }\n }\n\n // 검증\n if (input.strokeStyle !== undefined) {\n const v = validateColor(input.strokeStyle)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid strokeStyle'] }\n }\n if (input.lineWidth !== undefined && (input.lineWidth < 0 || !Number.isFinite(input.lineWidth))) {\n return { ops: [], errors: [`Invalid lineWidth: ${input.lineWidth}`] }\n }\n if (input.lineCap !== undefined && !VALID_LINE_CAP.has(input.lineCap)) {\n return {\n ops: [],\n errors: [`Unknown lineCap: '${input.lineCap}'. Valid: ${[...VALID_LINE_CAP].join(' / ')}.`]\n }\n }\n if (input.lineJoin !== undefined && !VALID_LINE_JOIN.has(input.lineJoin)) {\n return {\n ops: [],\n errors: [`Unknown lineJoin: '${input.lineJoin}'. Valid: ${[...VALID_LINE_JOIN].join(' / ')}.`]\n }\n }\n\n // dash 정규화 — alias → 실제 preset → 검증\n let dashValue: string | undefined\n if (input.dash !== undefined) {\n const raw = String(input.dash)\n const resolved = DASH_ALIASES[raw] ?? raw\n if (!VALID_DASH.has(resolved)) {\n return {\n ops: [],\n errors: [\n `Unknown dash: '${input.dash}'. Valid: ${[...VALID_DASH].join(' / ')} (or alias: dashed/dotted/dashdot).`\n ]\n }\n }\n dashValue = resolved\n }\n\n // patch 구성 — canonical 이름 그대로. 매핑 X. (color 만 normalizeColor 통과 — 입력 정규화는 mapping 이 아닌 처리)\n const patch: any = {}\n if (input.strokeStyle !== undefined) patch.strokeStyle = normalizeColor(input.strokeStyle)\n if (input.lineWidth !== undefined) patch.lineWidth = input.lineWidth\n if (dashValue !== undefined) patch.lineDash = dashValue\n if (input.lineCap !== undefined) patch.lineCap = input.lineCap\n if (input.lineJoin !== undefined) patch.lineJoin = input.lineJoin\n\n // synthetic enabled — false 면 lineWidth 0 으로 변환 (drawer 가 안 그림 — drawer/stroke.ts:9)\n if (input.enabled === false) {\n patch.lineWidth = 0\n }\n\n const ops: SceneEditOp[] = refids.map(refid => ({ op: 'modify', refid, patch }))\n return { ops, errors: [] }\n}\n"]}
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.
9
9
  */
10
- import type { BoardEditOp } from '../types';
10
+ import type { SceneEditOp } from '@operato/scene-ops';
11
11
  export interface SetTextInput {
12
12
  refids: number[];
13
13
  text: string;
@@ -25,7 +25,7 @@ export interface SetFontInput {
25
25
  lineHeight?: number;
26
26
  }
27
27
  export interface OpsResult {
28
- ops: BoardEditOp[];
28
+ ops: SceneEditOp[];
29
29
  errors: string[];
30
30
  }
31
31
  export declare function buildSetTextOps(input: SetTextInput): OpsResult;
@@ -1 +1 @@
1
- {"version":3,"file":"text-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/text-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.\n *\n * setText: state.text 변경 (내용)\n * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /\n * textAlign / textBaseline / lineHeight 변경 (폰트 스타일)\n *\n * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.\n */\nimport type { BoardEditOp } from '../types'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetTextInput {\n refids: number[]\n text: string\n}\n\nexport interface SetFontInput {\n refids: number[]\n fontFamily?: string\n fontSize?: number\n fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number\n fontStyle?: 'normal' | 'italic' | 'oblique'\n fontColor?: string\n /** Canvas API 표준 — 'justify' 는 Canvas 미지원이라 제외. */\n textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'\n textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'\n lineHeight?: number\n}\n\n// Canvas CanvasRenderingContext2D 가 허용하는 값만.\n// dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',\n// 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로\n// 실행되는 것 방지.\nconst VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique'])\nconst VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center'])\nconst VALID_TEXT_BASELINE = new Set([\n 'top',\n 'middle',\n 'bottom',\n 'hanging',\n 'alphabetic',\n 'ideographic'\n])\n\nexport interface OpsResult {\n ops: BoardEditOp[]\n errors: string[]\n}\n\nexport function buildSetTextOps(input: SetTextInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.text !== 'string') {\n return { ops: [], errors: ['text must be a string'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const patch = { text: input.text }\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n\nexport function buildSetFontOps(input: SetFontInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // 어느 인자도 없으면 무의미\n const noOp = !(\n input.fontFamily ||\n input.fontSize !== undefined ||\n input.fontWeight !== undefined ||\n input.fontStyle ||\n input.fontColor ||\n input.textAlign ||\n input.textBaseline ||\n input.lineHeight !== undefined\n )\n if (noOp) return { ops: [], errors: ['No font changes specified'] }\n\n if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {\n return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }\n }\n if (input.lineHeight !== undefined && input.lineHeight <= 0) {\n return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }\n }\n if (input.fontColor !== undefined) {\n const v = validateColor(input.fontColor)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }\n }\n if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {\n return {\n ops: [],\n errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]\n }\n }\n if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {\n return {\n ops: [],\n errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]\n }\n }\n if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {\n return {\n ops: [],\n errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]\n }\n }\n\n const patch: any = {}\n if (input.fontFamily) patch.fontFamily = input.fontFamily\n if (input.fontSize !== undefined) patch.fontSize = input.fontSize\n if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)\n if (input.fontStyle) patch.fontStyle = input.fontStyle\n if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)\n if (input.textAlign) patch.textAlign = input.textAlign\n if (input.textBaseline) patch.textBaseline = input.textBaseline\n if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
1
+ {"version":3,"file":"text-tools.js","sourceRoot":"","sources":["../../../../server/service/styling/text-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * setText / setFont — 컴포넌트의 텍스트 내용 / 폰트 스타일.\n *\n * setText: state.text 변경 (내용)\n * setFont: state.fontFamily / fontSize / fontWeight / fontStyle / fontColor /\n * textAlign / textBaseline / lineHeight 변경 (폰트 스타일)\n *\n * 두 tool 분리 — 의도가 다름. 내용 변경 vs 시각 스타일.\n */\nimport type { SceneEditOp } from '@operato/scene-ops'\nimport { normalizeColor, validateColor } from './color-utils'\n\nexport interface SetTextInput {\n refids: number[]\n text: string\n}\n\nexport interface SetFontInput {\n refids: number[]\n fontFamily?: string\n fontSize?: number\n fontWeight?: 'normal' | 'bold' | 'lighter' | 'bolder' | string | number\n fontStyle?: 'normal' | 'italic' | 'oblique'\n fontColor?: string\n /** Canvas API 표준 — 'justify' 는 Canvas 미지원이라 제외. */\n textAlign?: 'left' | 'center' | 'right' | 'start' | 'end'\n textBaseline?: 'top' | 'middle' | 'bottom' | 'hanging' | 'alphabetic' | 'ideographic'\n lineHeight?: number\n}\n\n// Canvas CanvasRenderingContext2D 가 허용하는 값만.\n// dash / lineCap / lineJoin 과 동일 패턴 — AI 가 비표준 값 ('serif-italic',\n// 'middle-align' 등) 을 보내도 Canvas 가 silent ignore → 효과 없는 op 가 그대로\n// 실행되는 것 방지.\nconst VALID_FONT_STYLE = new Set(['normal', 'italic', 'oblique'])\nconst VALID_TEXT_ALIGN = new Set(['start', 'end', 'left', 'right', 'center'])\nconst VALID_TEXT_BASELINE = new Set([\n 'top',\n 'middle',\n 'bottom',\n 'hanging',\n 'alphabetic',\n 'ideographic'\n])\n\nexport interface OpsResult {\n ops: SceneEditOp[]\n errors: string[]\n}\n\nexport function buildSetTextOps(input: SetTextInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n if (typeof input.text !== 'string') {\n return { ops: [], errors: ['text must be a string'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n const patch = { text: input.text }\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n\nexport function buildSetFontOps(input: SetFontInput): OpsResult {\n if (!Array.isArray(input.refids) || input.refids.length === 0) {\n return { ops: [], errors: ['refids must be a non-empty array'] }\n }\n const refids = input.refids.filter(n => typeof n === 'number' && Number.isFinite(n))\n if (refids.length === 0) return { ops: [], errors: ['No valid numeric refids'] }\n\n // 어느 인자도 없으면 무의미\n const noOp = !(\n input.fontFamily ||\n input.fontSize !== undefined ||\n input.fontWeight !== undefined ||\n input.fontStyle ||\n input.fontColor ||\n input.textAlign ||\n input.textBaseline ||\n input.lineHeight !== undefined\n )\n if (noOp) return { ops: [], errors: ['No font changes specified'] }\n\n if (input.fontSize !== undefined && (input.fontSize <= 0 || !Number.isFinite(input.fontSize))) {\n return { ops: [], errors: [`Invalid fontSize: ${input.fontSize}`] }\n }\n if (input.lineHeight !== undefined && input.lineHeight <= 0) {\n return { ops: [], errors: [`Invalid lineHeight: ${input.lineHeight}`] }\n }\n if (input.fontColor !== undefined) {\n const v = validateColor(input.fontColor)\n if (!v.valid) return { ops: [], errors: [v.error ?? 'Invalid fontColor'] }\n }\n if (input.fontStyle !== undefined && !VALID_FONT_STYLE.has(input.fontStyle)) {\n return {\n ops: [],\n errors: [`Unknown fontStyle: '${input.fontStyle}'. Valid: ${[...VALID_FONT_STYLE].join(' / ')}.`]\n }\n }\n if (input.textAlign !== undefined && !VALID_TEXT_ALIGN.has(input.textAlign)) {\n return {\n ops: [],\n errors: [`Unknown textAlign: '${input.textAlign}'. Valid: ${[...VALID_TEXT_ALIGN].join(' / ')}.`]\n }\n }\n if (input.textBaseline !== undefined && !VALID_TEXT_BASELINE.has(input.textBaseline)) {\n return {\n ops: [],\n errors: [`Unknown textBaseline: '${input.textBaseline}'. Valid: ${[...VALID_TEXT_BASELINE].join(' / ')}.`]\n }\n }\n\n const patch: any = {}\n if (input.fontFamily) patch.fontFamily = input.fontFamily\n if (input.fontSize !== undefined) patch.fontSize = input.fontSize\n if (input.fontWeight !== undefined) patch.fontWeight = String(input.fontWeight)\n if (input.fontStyle) patch.fontStyle = input.fontStyle\n if (input.fontColor !== undefined) patch.fontColor = normalizeColor(input.fontColor)\n if (input.textAlign) patch.textAlign = input.textAlign\n if (input.textBaseline) patch.textBaseline = input.textBaseline\n if (input.lineHeight !== undefined) patch.lineHeight = input.lineHeight\n\n return {\n ops: refids.map(refid => ({ op: 'modify', refid, patch })),\n errors: []\n }\n}\n"]}
@@ -7,7 +7,8 @@
7
7
  * - patch 의 op 는 직교 4 종 (add / remove / modify / replace).
8
8
  * style/move 등은 modify.patch 안에 흡수.
9
9
  */
10
- import type { BoardComponent, BoardModel } from '@things-factory/board-import';
10
+ import type { BoardModel } from '@things-factory/board-import';
11
+ import type { SceneActionOp, SceneEditPatch } from '@operato/scene-ops';
11
12
  import type { LLMMessage } from '@things-factory/ai-assistant';
12
13
  export type { LLMMessage };
13
14
  export interface ChatOptions {
@@ -118,19 +119,19 @@ import type { ToolUsage } from '@things-factory/ai-client-base';
118
119
  import type { AIAnalysisClaim, GroundedAnalysisContract } from '@things-factory/ai-client-base';
119
120
  export type { ToolUsage };
120
121
  /** Board-specific extension of the shared evidence/approval analysis boundary. */
121
- export interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim, BoardEditPatch | BoardActionOp[]> {
122
+ export interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim, SceneEditPatch | SceneActionOp[]> {
122
123
  kind: 'board.analysis.v1';
123
124
  }
124
125
  export interface ChatResponse {
125
126
  /** 사용자에게 보여줄 텍스트 응답 */
126
127
  reply: string;
127
128
  /** 보드 변경이 있으면 patch */
128
- patch?: BoardEditPatch;
129
+ patch?: SceneEditPatch;
129
130
  /**
130
131
  * Scene 조작 action (ephemeral — 모델 변경 없음 / undo 영향 없음).
131
132
  * 시간순 시퀀스로 호스트가 things-scene API 직접 실행.
132
133
  */
133
- actions?: BoardActionOp[];
134
+ actions?: SceneActionOp[];
134
135
  /** 모호한 입력 시 명확화 질문 (patch 없음) */
135
136
  followUp?: string;
136
137
  /** AI 가 응답 만드는 과정에서 호출한 도구들 (시간순). UI fold-able 박스용. */
@@ -152,156 +153,13 @@ export interface ChatResponse {
152
153
  /** Present when the turn inspected state or proposed a board/action change. */
153
154
  analysis?: BoardGroundedAnalysis;
154
155
  }
155
- export interface BoardEditPatch {
156
- ops: BoardEditOp[];
157
- /** 사용자 검수용 1-2 문장 요약 */
158
- summary: string;
159
- /** 0..1 신뢰도 */
160
- confidence: number;
161
- }
162
- /**
163
- * 보드 변경 op.
164
- *
165
- * 식별자 정책 — 기존 컴포넌트 타깃팅은 `refid` (number) 만 사용.
166
- * things-scene 의 모든 컴포넌트는 `refid` 를 자동 발급받는다 (universal).
167
- * `model.id` 는 optional metadata 일 뿐 — 항상 존재하지 않으므로 targeting 에는
168
- * 부적합. 별개 개념이므로 BoardEditOp / tool 인자에서도 별개 식별자로 분리하지
169
- * 않고 refid 단일 채널로 일원화.
170
- *
171
- * 계층 — 보드는 **최상위 부모** (things-scene 의 model-layer) 이고 그 자체로 자기
172
- * 속성을 갖는다 (fillStyle, width, height, fitMode, translate, scale, sky, skyColor,
173
- * exposure, hemi/dirLight 계열, camera 계열). 자식 컴포넌트와 별개. 보드 속성 변경은
174
- * `modifyBoard`, 자식 변경은 `modify` (refid 기반).
175
- *
176
- * 주의 — `name` 은 보드 *엔티티* (DB row) 의 컬럼이지 scene MODEL 의 필드가 아니다.
177
- * things-scene 의 model-layer 가 인식 안 함. modifyBoard 로 name 을 보내면 model
178
- * JSON 에 죽은 필드로 박힐 뿐 렌더링/동작에 영향 없음. 보드 라벨 변경은 GraphQL
179
- * boardPatch (BoardPatch input 의 name 필드) 영역.
180
- *
181
- * style 변경 / 이동 / 크기 변경 등 자식 컴포넌트의 변경은 모두 `modify.patch` 안에
182
- * 흡수. 보드 root 속성 변경은 `modifyBoard.patch` 로.
183
- */
184
- /**
185
- * Phase 2 — Scene 조작 op 들. 모델 차원 (좌표 / 부모-자식 관계 / z-order) 변경이지만
186
- * things-scene 의 자체 API (align/distribute/group/ungroup/zorder) 가 일관된 결과를
187
- * 보장하므로 호스트가 직접 호출. 모델 차원 시뮬레이션 (apply-patch) 은 단순화 —
188
- * scene 호출 결과가 정본.
189
- */
190
- export type AlignDirection = 'left' | 'right' | 'center' | 'top' | 'middle' | 'bottom';
191
- export type DistributeAxis = 'horizontal' | 'vertical';
192
- export type ZorderDirection = 'front' | 'back' | 'forward' | 'backward';
193
- /**
194
- * Sugar layout — `arrange` op 의 layout 종류.
195
- *
196
- * 의도: align/distribute 위에 얹는 high-level 의도 표현. AI 가 "3x2 그리드로",
197
- * "한 줄로", "세로로 일렬" 같은 자연어를 픽셀 노가다 없이 단일 op 로 표현.
198
- *
199
- * left/top 만 변경 — width/height 는 유지. AI 가 사이즈도 바꾸려면 별도 modify.
200
- *
201
- * 위치 계산은 호스트 (things-scene 측) 가 담당 — 각 컴포넌트의 현재 width/height 를
202
- * 정확히 알아야 하므로 model 차원 시뮬레이션은 SCENE_ONLY (apply-patch noop).
203
- */
204
- export type ArrangeLayout = {
205
- type: 'grid';
206
- cols: number;
207
- gap?: number;
208
- anchor?: {
209
- left: number;
210
- top: number;
211
- };
212
- } | {
213
- type: 'row';
214
- gap?: number;
215
- anchor?: {
216
- left: number;
217
- top: number;
218
- };
219
- align?: 'start' | 'center' | 'end';
220
- } | {
221
- type: 'column';
222
- gap?: number;
223
- anchor?: {
224
- left: number;
225
- top: number;
226
- };
227
- align?: 'start' | 'center' | 'end';
228
- };
229
- /**
230
- * C-1 — Scene 조작 action 들 (ephemeral). 모델 변경 X, 따라서 BoardEditOp 와 별개
231
- * 채널 (board-action-execute 이벤트). undo 히스토리 미영향, dirty flag 미영향.
232
- *
233
- * 종류:
234
- * - selectComponents: scene.selected 직접 set (사용자 선택 변경)
235
- * - centerToComponent: 특정 컴포넌트로 view 이동
236
- * - fitToView: 보드 전체가 보이도록 fit
237
- * - setSceneMode: edit / view 모드 전환
238
- */
239
- export type BoardActionOp = {
240
- action: 'selectComponents';
241
- refids: number[];
242
- } | {
243
- action: 'centerToComponent';
244
- refid: number;
245
- animated?: boolean;
246
- } | {
247
- action: 'fitToView';
248
- mode?: 'fit' | 'ratio' | 'width' | 'height';
249
- } | {
250
- action: 'setSceneMode';
251
- mode: 'edit' | 'view';
252
- }
253
- /** 다중 컴포넌트 outline highlight — search/finder UX 의 "이게 모두 매칭이다" 시각화.
254
- * things-scene 의 highlightSearchResults API 위임 (2D/3D 모두 지원). */
255
- | {
256
- action: 'highlightComponents';
257
- refids: number[];
258
- };
259
- export type BoardEditOp = {
260
- op: 'add';
261
- component: BoardComponent;
262
- } | {
263
- op: 'remove';
264
- refid: number;
265
- } | {
266
- op: 'modify';
267
- refid: number;
268
- patch: Partial<BoardComponent>;
269
- } | {
270
- op: 'modifyBoard';
271
- patch: Partial<BoardModel>;
272
- } | {
273
- op: 'replace';
274
- board: BoardModel;
275
- } | {
276
- op: 'align';
277
- refids: number[];
278
- direction: AlignDirection;
279
- } | {
280
- op: 'distribute';
281
- refids: number[];
282
- axis: DistributeAxis;
283
- } | {
284
- op: 'group';
285
- refids: number[];
286
- } | {
287
- op: 'ungroup';
288
- refid: number;
289
- } | {
290
- op: 'zorder';
291
- refid: number;
292
- direction: ZorderDirection;
293
- } | {
294
- op: 'arrange';
295
- refids: number[];
296
- layout: ArrangeLayout;
297
- };
298
156
  /**
299
157
  * BoardAIAssistant — 자연어 채팅으로 보드를 다루는 단일 인터페이스.
300
158
  *
301
159
  * 사용:
302
160
  * const ai = new DefaultBoardAIAssistant(baseClient, { scopes: ['fmsim'] })
303
161
  * const r = await ai.chat([{ role: 'user', content: 'AGV 3대 추가' }], currentBoard)
304
- * if (r.patch) currentBoard = applyBoardEditPatch(currentBoard, r.patch)
162
+ * if (r.patch) currentBoard = applyScenePatch(currentBoard, r.patch)
305
163
  */
306
164
  export interface BoardAIAssistant {
307
165
  readonly id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Board AI — 자연어 채팅 인터페이스 타입 정의.\n *\n * 핵심 개념:\n * - 단일 진입점 `chat()`. 의도 분류(생성/편집/스타일/질문)는 AI 의 일.\n * - 출력은 항상 `ChatResponse`. 보드 변경이 있으면 `patch` 동봉.\n * - patch 의 op 는 직교 4 종 (add / remove / modify / replace).\n * style/move 등은 modify.patch 안에 흡수.\n */\nimport type { BoardComponent, BoardModel } from '@things-factory/board-import'\n\n/* 대화 한 줄은 도메인을 모른다 — 정의는 `@things-factory/ai-assistant` 에 있다(2026-09-10). */\nimport type { LLMMessage } from '@things-factory/ai-assistant'\nexport type { LLMMessage }\n\nexport interface ChatOptions {\n /**\n * 이 대화에 노출할 **외부 도구 카테고리** 화이트리스트(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 성격이 다른 대화면(보드 저작 도크 · 운영 도크)이\n * 서로의 능력을 갖지 않게 하는 경계 — 정의뿐 아니라 실행 판정에도 적용된다.\n */\n toolCategories?: string[]\n /**\n * 코어 보드 편집 도구(addComponent · 스타일링 · 씬 조작) 노출 여부. 기본 true.\n * 운영 대화처럼 보드를 고칠 이유가 없는 면에서는 false 로 꺼서, 되돌릴 수 없는 행동과\n * 되돌릴 수 있는 편집이 한 대화에 섞이지 않게 한다.\n */\n boardTools?: boolean\n /**\n * **첫 턴에 도구 호출을 강제**한다. 라이브 상태를 다루는 대화면(운영 협의)에서 켠다.\n *\n * 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다\n * (\"지게차들이 바쁘게 움직이고 있다\" — 실제로 나온 답이다). 그건 도구를 아예 부르지 않고 답한\n * 경우여서 사후 검사로 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할\n * 길이 없어진다. 켜고 끄는 판단은 대화면이 한다 — 서버가 질문 내용을 눈치로 분류하지 않는다.\n */\n requireGroundingTools?: boolean\n /**\n * 호스트(대화면)가 실어 보내는 **문맥** — 도구가 ctx.state.host 로 읽는다.\n *\n * 왜 필요한가: 대화면마다 \"지금 무엇을 보고 있는지\" 가 다르다(어느 공간·어느 인스턴스·어느 보드).\n * 이것 없이는 도구가 대상을 알 수 없어 모델이 사용자에게 식별자를 되묻는다 — 제품으로 성립하지 않는다.\n * 보드 대화는 보드 모델에 각인해 우회할 수 있었지만 보드가 없는 대화면(공간 협의)은 통로가 없었다.\n *\n * 신뢰 경계: 이 값은 **클라이언트가 보낸 것**이다. 도메인·사용자 같은 권한 판정에 쓰지 말 것\n * (그건 서버가 세션에서 얻는 ctx.state.domain/user 가 정본). 여기 값은 \"무엇을 보고 있는지\" 범위\n * 힌트이며, 도구는 그 범위가 호출자 테넌트 소속인지 반드시 검증한다.\n */\n hostContext?: any\n /** board-import 의 registry scope — 등록된 ImportRule 에서 type/category 후보 추출 */\n scopes?: string[]\n /** 사용 가능한 도메인 type (override). 미지정 시 scopes 에서 자동 추출 */\n knownTypes?: string[]\n /** 사용 가능한 카테고리 (override). 미지정 시 scopes 에서 자동 추출 */\n categories?: string[]\n /**\n * 컴포넌트 type 별 유효 속성 스킴.\n * LLM 이 정확한 컴포넌트 생성을 하도록 type 의 description, group, default model 키 전달.\n *\n * 예:\n * [{ type: 'rect', description: 'Rectangle', group: 'shape',\n * properties: { fillStyle: '#fff', strokeStyle: '#000' } }, ...]\n */\n componentSchemas?: ComponentSchema[]\n /**\n * 사용자가 모델러에서 현재 선택한 컴포넌트의 `refid` 목록.\n *\n * refid 는 things-scene 이 모든 컴포넌트에 자동 발급하는 universal numeric handle.\n * id (데이터 바인딩 이름, unique 아님) 와는 다른 개념 — 선택은 항상 refid 로 표현.\n */\n selectedRefids?: number[]\n /**\n * popup 에서 사용자가 명시 선택한 #mention 매핑 — {token, refid}.\n * server 가 mention 해석 시 이 매핑을 우선 사용. 본문엔 #token 만 남고\n * refid 는 사용자에게 노출되지 않음 (LLM mention map 에만 첨부).\n */\n mentionPicks?: Array<{ token: string; refid: number }>\n /** LLM 모델 override */\n model?: string\n /** LLM 옵션 */\n temperature?: number\n maxTokens?: number\n /**\n * Tool 카테고리 (read/external) builder 가 사용할 컨텍스트.\n *\n * board-ai 자체는 ResolverContext / typeorm 을 직접 알지 않지만, 외부 도메인 패키지가\n * 등록한 tool (예: board-import 의 importBoardAsync) 은 server-side 의존성 (도메인,\n * 사용자, 트랜잭션) 이 필요할 수 있다. board-ai-resolver 가 ResolverContext.state 등을\n * 이 필드로 전달하면, dispatch 시 closure 로 builder 의 ctx.state 에 주입.\n *\n * 미지정이면 외부 builder 는 ctx.state === undefined — 빈 응답이나 에러를 반환할 책임.\n */\n toolCallContext?: any\n}\n\nexport interface ComponentSchema {\n type: string\n description?: string\n group?: string\n /**\n * 좌표/크기/경로 관련 키들 — type 마다 다름.\n * 예) ['left', 'top', 'width', 'height'] vs ['cx', 'cy', 'radius'] vs ['points']\n * LLM 이 type 별 정확한 좌표 키를 사용하도록 명시.\n */\n geometryKeys?: string[]\n /**\n * 좌표 외 type 특화 속성 (style, value, min/max 등).\n * default 값 또는 keys 만 — LLM 의 token 절감을 위해 호출자가 적절히 압축.\n */\n properties?: Record<string, any>\n}\n\n/**\n * 도구 사용 기록 — **`@things-factory/ai-client-base` 가 소유한다.**\n *\n * 도메인 중립이라 루프와 함께 그리로 옮겼다(2026-09-05). 여기서 다시 내보내는 것은 부르는 쪽의\n * import 를 그대로 두기 위해서다 — 뜻의 정본은 저쪽 한 곳이다.\n */\nimport type { ToolUsage } from '@things-factory/ai-client-base'\nimport type { AIAnalysisClaim, GroundedAnalysisContract } from '@things-factory/ai-client-base'\nexport type { ToolUsage }\n\n/** Board-specific extension of the shared evidence/approval analysis boundary. */\nexport interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim, BoardEditPatch | BoardActionOp[]> {\n kind: 'board.analysis.v1'\n}\n\n\nexport interface ChatResponse {\n /** 사용자에게 보여줄 텍스트 응답 */\n reply: string\n /** 보드 변경이 있으면 patch */\n patch?: BoardEditPatch\n /**\n * Scene 조작 action (ephemeral — 모델 변경 없음 / undo 영향 없음).\n * 시간순 시퀀스로 호스트가 things-scene API 직접 실행.\n */\n actions?: BoardActionOp[]\n /** 모호한 입력 시 명확화 질문 (patch 없음) */\n followUp?: string\n /** AI 가 응답 만드는 과정에서 호출한 도구들 (시간순). UI fold-able 박스용. */\n toolUsages?: ToolUsage[]\n /**\n * **조치 제안** — 도구가 실행하지 않고 제안만 한 것들. 실행은 사용자가 버튼으로 한다.\n *\n * 규약: 도구 결과에 `proposed: true` 가 있으면 제안이다(도메인 무관 — board-ai 는 내용을 해석하지\n * 않고 그대로 전달한다). 되돌릴 수 없는 조치를 AI 가 직접 실행하지 않게 하는 경계가 이 채널이다.\n */\n proposals?: any[]\n /**\n * 접지 경고 — 답이 언급했으나 모델이 받은 근거(프롬프트·보드 문맥·이력·도구 결과)에 없는 식별자.\n *\n * 있으면 그 대상은 **만들어 낸 것일 수 있다**. 답을 막거나 고치지 않고 사용자에게 표시한다 —\n * 판정 규칙·사정거리는 ./grounding 참조. 비면(undefined) 접지 정상.\n */\n groundingWarnings?: string[]\n /** Present when the turn inspected state or proposed a board/action change. */\n analysis?: BoardGroundedAnalysis\n}\n\nexport interface BoardEditPatch {\n ops: BoardEditOp[]\n /** 사용자 검수용 1-2 문장 요약 */\n summary: string\n /** 0..1 신뢰도 */\n confidence: number\n}\n\n/**\n * 보드 변경 op.\n *\n * 식별자 정책 — 기존 컴포넌트 타깃팅은 `refid` (number) 만 사용.\n * things-scene 의 모든 컴포넌트는 `refid` 를 자동 발급받는다 (universal).\n * `model.id` 는 optional metadata 일 뿐 — 항상 존재하지 않으므로 targeting 에는\n * 부적합. 별개 개념이므로 BoardEditOp / tool 인자에서도 별개 식별자로 분리하지\n * 않고 refid 단일 채널로 일원화.\n *\n * 계층 — 보드는 **최상위 부모** (things-scene 의 model-layer) 이고 그 자체로 자기\n * 속성을 갖는다 (fillStyle, width, height, fitMode, translate, scale, sky, skyColor,\n * exposure, hemi/dirLight 계열, camera 계열). 자식 컴포넌트와 별개. 보드 속성 변경은\n * `modifyBoard`, 자식 변경은 `modify` (refid 기반).\n *\n * 주의 — `name` 은 보드 *엔티티* (DB row) 의 컬럼이지 scene MODEL 의 필드가 아니다.\n * things-scene 의 model-layer 가 인식 안 함. modifyBoard 로 name 을 보내면 model\n * JSON 에 죽은 필드로 박힐 뿐 렌더링/동작에 영향 없음. 보드 라벨 변경은 GraphQL\n * boardPatch (BoardPatch input 의 name 필드) 영역.\n *\n * style 변경 / 이동 / 크기 변경 등 자식 컴포넌트의 변경은 모두 `modify.patch` 안에\n * 흡수. 보드 root 속성 변경은 `modifyBoard.patch` 로.\n */\n/**\n * Phase 2 — Scene 조작 op 들. 모델 차원 (좌표 / 부모-자식 관계 / z-order) 변경이지만\n * things-scene 의 자체 API (align/distribute/group/ungroup/zorder) 가 일관된 결과를\n * 보장하므로 호스트가 직접 호출. 모델 차원 시뮬레이션 (apply-patch) 은 단순화 —\n * scene 호출 결과가 정본.\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 *\n * 의도: align/distribute 위에 얹는 high-level 의도 표현. AI 가 \"3x2 그리드로\",\n * \"한 줄로\", \"세로로 일렬\" 같은 자연어를 픽셀 노가다 없이 단일 op 로 표현.\n *\n * left/top 만 변경 — width/height 는 유지. AI 가 사이즈도 바꾸려면 별도 modify.\n *\n * 위치 계산은 호스트 (things-scene 측) 가 담당 — 각 컴포넌트의 현재 width/height 를\n * 정확히 알아야 하므로 model 차원 시뮬레이션은 SCENE_ONLY (apply-patch noop).\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\n/**\n * C-1 — Scene 조작 action 들 (ephemeral). 모델 변경 X, 따라서 BoardEditOp 와 별개\n * 채널 (board-action-execute 이벤트). undo 히스토리 미영향, dirty flag 미영향.\n *\n * 종류:\n * - selectComponents: scene.selected 직접 set (사용자 선택 변경)\n * - centerToComponent: 특정 컴포넌트로 view 이동\n * - fitToView: 보드 전체가 보이도록 fit\n * - setSceneMode: edit / view 모드 전환\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 /** 다중 컴포넌트 outline highlight — search/finder UX 의 \"이게 모두 매칭이다\" 시각화.\n * things-scene 의 highlightSearchResults API 위임 (2D/3D 모두 지원). */\n | { action: 'highlightComponents'; refids: number[] }\n\nexport type BoardEditOp =\n | { op: 'add'; component: BoardComponent }\n | { op: 'remove'; refid: number }\n | { op: 'modify'; refid: number; patch: Partial<BoardComponent> }\n | { op: 'modifyBoard'; patch: Partial<BoardModel> }\n | { op: 'replace'; board: BoardModel }\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 * BoardAIAssistant — 자연어 채팅으로 보드를 다루는 단일 인터페이스.\n *\n * 사용:\n * const ai = new DefaultBoardAIAssistant(baseClient, { scopes: ['fmsim'] })\n * const r = await ai.chat([{ role: 'user', content: 'AGV 3대 추가' }], currentBoard)\n * if (r.patch) currentBoard = applyBoardEditPatch(currentBoard, r.patch)\n */\nexport interface BoardAIAssistant {\n readonly id: string\n chat(\n messages: LLMMessage[],\n currentBoard: BoardModel | undefined,\n options?: ChatOptions\n ): Promise<ChatResponse>\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/types.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Board AI — 자연어 채팅 인터페이스 타입 정의.\n *\n * 핵심 개념:\n * - 단일 진입점 `chat()`. 의도 분류(생성/편집/스타일/질문)는 AI 의 일.\n * - 출력은 항상 `ChatResponse`. 보드 변경이 있으면 `patch` 동봉.\n * - patch 의 op 는 직교 4 종 (add / remove / modify / replace).\n * style/move 등은 modify.patch 안에 흡수.\n */\nimport type { BoardComponent, BoardModel } from '@things-factory/board-import'\n\n/* 대화 한 줄은 도메인을 모른다 — 정의는 `@things-factory/ai-assistant` 에 있다(2026-09-10). */\n/*\n * The edit vocabulary is not this package's. A person dragging a box produces the same\n * operations, so they live in @operato/scene-ops with the two appliers that carry them out.\n */\nimport type { SceneActionOp, SceneEditPatch } from '@operato/scene-ops'\nimport type { LLMMessage } from '@things-factory/ai-assistant'\nexport type { LLMMessage }\n\nexport interface ChatOptions {\n /**\n * 이 대화에 노출할 **외부 도구 카테고리** 화이트리스트(registerToolCategory 이름).\n * 미지정 = 등록된 전부(현행 동작). 성격이 다른 대화면(보드 저작 도크 · 운영 도크)이\n * 서로의 능력을 갖지 않게 하는 경계 — 정의뿐 아니라 실행 판정에도 적용된다.\n */\n toolCategories?: string[]\n /**\n * 코어 보드 편집 도구(addComponent · 스타일링 · 씬 조작) 노출 여부. 기본 true.\n * 운영 대화처럼 보드를 고칠 이유가 없는 면에서는 false 로 꺼서, 되돌릴 수 없는 행동과\n * 되돌릴 수 있는 편집이 한 대화에 섞이지 않게 한다.\n */\n boardTools?: boolean\n /**\n * **첫 턴에 도구 호출을 강제**한다. 라이브 상태를 다루는 대화면(운영 협의)에서 켠다.\n *\n * 접지 가드는 답이 지어낸 **식별자**를 잡지만, 식별자 없이 지어낸 **상황 서술**은 잡지 못한다\n * (\"지게차들이 바쁘게 움직이고 있다\" — 실제로 나온 답이다). 그건 도구를 아예 부르지 않고 답한\n * 경우여서 사후 검사로 막을 수 없다. 첫 턴에 무엇이든 조회하게 만들면 근거 없이 상태를 단언할\n * 길이 없어진다. 켜고 끄는 판단은 대화면이 한다 — 서버가 질문 내용을 눈치로 분류하지 않는다.\n */\n requireGroundingTools?: boolean\n /**\n * 호스트(대화면)가 실어 보내는 **문맥** — 도구가 ctx.state.host 로 읽는다.\n *\n * 왜 필요한가: 대화면마다 \"지금 무엇을 보고 있는지\" 가 다르다(어느 공간·어느 인스턴스·어느 보드).\n * 이것 없이는 도구가 대상을 알 수 없어 모델이 사용자에게 식별자를 되묻는다 — 제품으로 성립하지 않는다.\n * 보드 대화는 보드 모델에 각인해 우회할 수 있었지만 보드가 없는 대화면(공간 협의)은 통로가 없었다.\n *\n * 신뢰 경계: 이 값은 **클라이언트가 보낸 것**이다. 도메인·사용자 같은 권한 판정에 쓰지 말 것\n * (그건 서버가 세션에서 얻는 ctx.state.domain/user 가 정본). 여기 값은 \"무엇을 보고 있는지\" 범위\n * 힌트이며, 도구는 그 범위가 호출자 테넌트 소속인지 반드시 검증한다.\n */\n hostContext?: any\n /** board-import 의 registry scope — 등록된 ImportRule 에서 type/category 후보 추출 */\n scopes?: string[]\n /** 사용 가능한 도메인 type (override). 미지정 시 scopes 에서 자동 추출 */\n knownTypes?: string[]\n /** 사용 가능한 카테고리 (override). 미지정 시 scopes 에서 자동 추출 */\n categories?: string[]\n /**\n * 컴포넌트 type 별 유효 속성 스킴.\n * LLM 이 정확한 컴포넌트 생성을 하도록 type 의 description, group, default model 키 전달.\n *\n * 예:\n * [{ type: 'rect', description: 'Rectangle', group: 'shape',\n * properties: { fillStyle: '#fff', strokeStyle: '#000' } }, ...]\n */\n componentSchemas?: ComponentSchema[]\n /**\n * 사용자가 모델러에서 현재 선택한 컴포넌트의 `refid` 목록.\n *\n * refid 는 things-scene 이 모든 컴포넌트에 자동 발급하는 universal numeric handle.\n * id (데이터 바인딩 이름, unique 아님) 와는 다른 개념 — 선택은 항상 refid 로 표현.\n */\n selectedRefids?: number[]\n /**\n * popup 에서 사용자가 명시 선택한 #mention 매핑 — {token, refid}.\n * server 가 mention 해석 시 이 매핑을 우선 사용. 본문엔 #token 만 남고\n * refid 는 사용자에게 노출되지 않음 (LLM mention map 에만 첨부).\n */\n mentionPicks?: Array<{ token: string; refid: number }>\n /** LLM 모델 override */\n model?: string\n /** LLM 옵션 */\n temperature?: number\n maxTokens?: number\n /**\n * Tool 카테고리 (read/external) builder 가 사용할 컨텍스트.\n *\n * board-ai 자체는 ResolverContext / typeorm 을 직접 알지 않지만, 외부 도메인 패키지가\n * 등록한 tool (예: board-import 의 importBoardAsync) 은 server-side 의존성 (도메인,\n * 사용자, 트랜잭션) 이 필요할 수 있다. board-ai-resolver 가 ResolverContext.state 등을\n * 이 필드로 전달하면, dispatch 시 closure 로 builder 의 ctx.state 에 주입.\n *\n * 미지정이면 외부 builder 는 ctx.state === undefined — 빈 응답이나 에러를 반환할 책임.\n */\n toolCallContext?: any\n}\n\nexport interface ComponentSchema {\n type: string\n description?: string\n group?: string\n /**\n * 좌표/크기/경로 관련 키들 — type 마다 다름.\n * 예) ['left', 'top', 'width', 'height'] vs ['cx', 'cy', 'radius'] vs ['points']\n * LLM 이 type 별 정확한 좌표 키를 사용하도록 명시.\n */\n geometryKeys?: string[]\n /**\n * 좌표 외 type 특화 속성 (style, value, min/max 등).\n * default 값 또는 keys 만 — LLM 의 token 절감을 위해 호출자가 적절히 압축.\n */\n properties?: Record<string, any>\n}\n\n/**\n * 도구 사용 기록 — **`@things-factory/ai-client-base` 가 소유한다.**\n *\n * 도메인 중립이라 루프와 함께 그리로 옮겼다(2026-09-05). 여기서 다시 내보내는 것은 부르는 쪽의\n * import 를 그대로 두기 위해서다 — 뜻의 정본은 저쪽 한 곳이다.\n */\nimport type { ToolUsage } from '@things-factory/ai-client-base'\nimport type { AIAnalysisClaim, GroundedAnalysisContract } from '@things-factory/ai-client-base'\nexport type { ToolUsage }\n\n/** Board-specific extension of the shared evidence/approval analysis boundary. */\nexport interface BoardGroundedAnalysis extends GroundedAnalysisContract<any, AIAnalysisClaim, SceneEditPatch | SceneActionOp[]> {\n kind: 'board.analysis.v1'\n}\n\n\nexport interface ChatResponse {\n /** 사용자에게 보여줄 텍스트 응답 */\n reply: string\n /** 보드 변경이 있으면 patch */\n patch?: SceneEditPatch\n /**\n * Scene 조작 action (ephemeral — 모델 변경 없음 / undo 영향 없음).\n * 시간순 시퀀스로 호스트가 things-scene API 직접 실행.\n */\n actions?: SceneActionOp[]\n /** 모호한 입력 시 명확화 질문 (patch 없음) */\n followUp?: string\n /** AI 가 응답 만드는 과정에서 호출한 도구들 (시간순). UI fold-able 박스용. */\n toolUsages?: ToolUsage[]\n /**\n * **조치 제안** — 도구가 실행하지 않고 제안만 한 것들. 실행은 사용자가 버튼으로 한다.\n *\n * 규약: 도구 결과에 `proposed: true` 가 있으면 제안이다(도메인 무관 — board-ai 는 내용을 해석하지\n * 않고 그대로 전달한다). 되돌릴 수 없는 조치를 AI 가 직접 실행하지 않게 하는 경계가 이 채널이다.\n */\n proposals?: any[]\n /**\n * 접지 경고 — 답이 언급했으나 모델이 받은 근거(프롬프트·보드 문맥·이력·도구 결과)에 없는 식별자.\n *\n * 있으면 그 대상은 **만들어 낸 것일 수 있다**. 답을 막거나 고치지 않고 사용자에게 표시한다 —\n * 판정 규칙·사정거리는 ./grounding 참조. 비면(undefined) 접지 정상.\n */\n groundingWarnings?: string[]\n /** Present when the turn inspected state or proposed a board/action change. */\n analysis?: BoardGroundedAnalysis\n}\n\n/**\n * BoardAIAssistant — 자연어 채팅으로 보드를 다루는 단일 인터페이스.\n *\n * 사용:\n * const ai = new DefaultBoardAIAssistant(baseClient, { scopes: ['fmsim'] })\n * const r = await ai.chat([{ role: 'user', content: 'AGV 3대 추가' }], currentBoard)\n * if (r.patch) currentBoard = applyScenePatch(currentBoard, r.patch)\n */\nexport interface BoardAIAssistant {\n readonly id: string\n chat(\n messages: LLMMessage[],\n currentBoard: BoardModel | undefined,\n options?: ChatOptions\n ): Promise<ChatResponse>\n}\n"]}