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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/config/config.development.js +44 -0
  2. package/config/config.production.js +18 -0
  3. package/dist-server/index.d.ts +7 -0
  4. package/dist-server/index.js +11 -0
  5. package/dist-server/index.js.map +1 -0
  6. package/dist-server/service/adapters/dxf-adapter.d.ts +8 -0
  7. package/dist-server/service/adapters/dxf-adapter.js +220 -0
  8. package/dist-server/service/adapters/dxf-adapter.js.map +1 -0
  9. package/dist-server/service/adapters/image-adapter.d.ts +32 -0
  10. package/dist-server/service/adapters/image-adapter.js +420 -0
  11. package/dist-server/service/adapters/image-adapter.js.map +1 -0
  12. package/dist-server/service/ai/types.d.ts +185 -0
  13. package/dist-server/service/ai/types.js +378 -0
  14. package/dist-server/service/ai/types.js.map +1 -0
  15. package/dist-server/service/converters/generic-to.d.ts +31 -0
  16. package/dist-server/service/converters/generic-to.js +84 -0
  17. package/dist-server/service/converters/generic-to.js.map +1 -0
  18. package/dist-server/service/extraction/contracts.d.ts +112 -0
  19. package/dist-server/service/extraction/contracts.js +3 -0
  20. package/dist-server/service/extraction/contracts.js.map +1 -0
  21. package/dist-server/service/extraction/index.d.ts +17 -0
  22. package/dist-server/service/extraction/index.js +62 -0
  23. package/dist-server/service/extraction/index.js.map +1 -0
  24. package/dist-server/service/extraction/label-scale-calibrator.d.ts +5 -0
  25. package/dist-server/service/extraction/label-scale-calibrator.js +111 -0
  26. package/dist-server/service/extraction/label-scale-calibrator.js.map +1 -0
  27. package/dist-server/service/extraction/layout-refiner.d.ts +16 -0
  28. package/dist-server/service/extraction/layout-refiner.js +88 -0
  29. package/dist-server/service/extraction/layout-refiner.js.map +1 -0
  30. package/dist-server/service/extraction/pipeline.d.ts +11 -0
  31. package/dist-server/service/extraction/pipeline.js +124 -0
  32. package/dist-server/service/extraction/pipeline.js.map +1 -0
  33. package/dist-server/service/extraction/source-router.d.ts +17 -0
  34. package/dist-server/service/extraction/source-router.js +40 -0
  35. package/dist-server/service/extraction/source-router.js.map +1 -0
  36. package/dist-server/service/extraction/stubs.d.ts +18 -0
  37. package/dist-server/service/extraction/stubs.js +38 -0
  38. package/dist-server/service/extraction/stubs.js.map +1 -0
  39. package/dist-server/service/extraction/vector-geometry.d.ts +7 -0
  40. package/dist-server/service/extraction/vector-geometry.js +21 -0
  41. package/dist-server/service/extraction/vector-geometry.js.map +1 -0
  42. package/dist-server/service/extraction/vlm-recognizer.d.ts +18 -0
  43. package/dist-server/service/extraction/vlm-recognizer.js +78 -0
  44. package/dist-server/service/extraction/vlm-recognizer.js.map +1 -0
  45. package/dist-server/service/import-session/import-actions.d.ts +79 -0
  46. package/dist-server/service/import-session/import-actions.js +98 -0
  47. package/dist-server/service/import-session/import-actions.js.map +1 -0
  48. package/dist-server/service/import-session/import-session-resolver.d.ts +39 -0
  49. package/dist-server/service/import-session/import-session-resolver.js +263 -0
  50. package/dist-server/service/import-session/import-session-resolver.js.map +1 -0
  51. package/dist-server/service/import-session/import-session.d.ts +28 -0
  52. package/dist-server/service/import-session/import-session.js +152 -0
  53. package/dist-server/service/import-session/import-session.js.map +1 -0
  54. package/dist-server/service/import-session/import-worker.d.ts +1 -0
  55. package/dist-server/service/import-session/import-worker.js +121 -0
  56. package/dist-server/service/import-session/import-worker.js.map +1 -0
  57. package/dist-server/service/import-session/index.d.ts +8 -0
  58. package/dist-server/service/import-session/index.js +19 -0
  59. package/dist-server/service/import-session/index.js.map +1 -0
  60. package/dist-server/service/import-session/materialize-from-session.d.ts +66 -0
  61. package/dist-server/service/import-session/materialize-from-session.js +104 -0
  62. package/dist-server/service/import-session/materialize-from-session.js.map +1 -0
  63. package/dist-server/service/import-session/suggest-board-name.d.ts +80 -0
  64. package/dist-server/service/import-session/suggest-board-name.js +195 -0
  65. package/dist-server/service/import-session/suggest-board-name.js.map +1 -0
  66. package/dist-server/service/import-tools.d.ts +10 -0
  67. package/dist-server/service/import-tools.js +227 -0
  68. package/dist-server/service/import-tools.js.map +1 -0
  69. package/dist-server/service/index.d.ts +26 -0
  70. package/dist-server/service/index.js +60 -0
  71. package/dist-server/service/index.js.map +1 -0
  72. package/dist-server/service/pipeline/index.d.ts +66 -0
  73. package/dist-server/service/pipeline/index.js +127 -0
  74. package/dist-server/service/pipeline/index.js.map +1 -0
  75. package/dist-server/service/pipeline/stage2-mapping.d.ts +29 -0
  76. package/dist-server/service/pipeline/stage2-mapping.js +231 -0
  77. package/dist-server/service/pipeline/stage2-mapping.js.map +1 -0
  78. package/dist-server/service/pipeline/stage3-board.d.ts +52 -0
  79. package/dist-server/service/pipeline/stage3-board.js +108 -0
  80. package/dist-server/service/pipeline/stage3-board.js.map +1 -0
  81. package/dist-server/service/pipeline/stage4-binding.d.ts +9 -0
  82. package/dist-server/service/pipeline/stage4-binding.js +28 -0
  83. package/dist-server/service/pipeline/stage4-binding.js.map +1 -0
  84. package/dist-server/service/registry/index.d.ts +27 -0
  85. package/dist-server/service/registry/index.js +108 -0
  86. package/dist-server/service/registry/index.js.map +1 -0
  87. package/dist-server/service/types/index.d.ts +152 -0
  88. package/dist-server/service/types/index.js +3 -0
  89. package/dist-server/service/types/index.js.map +1 -0
  90. package/dist-server/service/types/registry.d.ts +175 -0
  91. package/dist-server/service/types/registry.js +3 -0
  92. package/dist-server/service/types/registry.js.map +1 -0
  93. package/dist-server/tsconfig.tsbuildinfo +1 -0
  94. package/package.json +35 -0
  95. package/probe/gcp-vision-probe.mjs +114 -0
  96. package/probe/opencv-geometry-probe.py +98 -0
  97. package/server/index.ts +7 -0
  98. package/server/service/adapters/dxf-adapter.ts +230 -0
  99. package/server/service/adapters/image-adapter.test.ts +545 -0
  100. package/server/service/adapters/image-adapter.ts +464 -0
  101. package/server/service/ai/types.ts +619 -0
  102. package/server/service/converters/generic-to.test.ts +105 -0
  103. package/server/service/converters/generic-to.ts +121 -0
  104. package/server/service/extraction/contracts.ts +142 -0
  105. package/server/service/extraction/extraction.test.ts +374 -0
  106. package/server/service/extraction/index.ts +51 -0
  107. package/server/service/extraction/label-scale-calibrator.ts +124 -0
  108. package/server/service/extraction/layout-refiner.ts +104 -0
  109. package/server/service/extraction/pipeline.ts +163 -0
  110. package/server/service/extraction/source-router.ts +45 -0
  111. package/server/service/extraction/stubs.ts +46 -0
  112. package/server/service/extraction/vector-geometry.ts +26 -0
  113. package/server/service/extraction/vlm-recognizer.ts +87 -0
  114. package/server/service/import-session/import-actions.test.ts +185 -0
  115. package/server/service/import-session/import-actions.ts +164 -0
  116. package/server/service/import-session/import-session-resolver.ts +245 -0
  117. package/server/service/import-session/import-session.ts +141 -0
  118. package/server/service/import-session/import-worker.ts +132 -0
  119. package/server/service/import-session/index.ts +24 -0
  120. package/server/service/import-session/materialize-from-session.test.ts +274 -0
  121. package/server/service/import-session/materialize-from-session.ts +158 -0
  122. package/server/service/import-session/suggest-board-name.test.ts +271 -0
  123. package/server/service/import-session/suggest-board-name.ts +279 -0
  124. package/server/service/import-tools.test.ts +137 -0
  125. package/server/service/import-tools.ts +255 -0
  126. package/server/service/index.ts +117 -0
  127. package/server/service/pipeline/index.ts +184 -0
  128. package/server/service/pipeline/stage2-mapping.test.ts +204 -0
  129. package/server/service/pipeline/stage2-mapping.ts +270 -0
  130. package/server/service/pipeline/stage3-board.test.ts +133 -0
  131. package/server/service/pipeline/stage3-board.ts +179 -0
  132. package/server/service/pipeline/stage4-binding.ts +31 -0
  133. package/server/service/registry/index.ts +127 -0
  134. package/server/service/types/index.ts +169 -0
  135. package/server/service/types/registry.ts +186 -0
  136. package/things-factory.config.js +1 -0
  137. package/tsconfig.json +9 -0
@@ -0,0 +1,255 @@
1
+ /**
2
+ * board-import 의 LLM tool 카테고리 등록.
3
+ *
4
+ * board-ai 의 chat 흐름 안에서 사용자가 "이 도면을 보드로 만들어줘" / "import 결과 검수
5
+ * 후 이 이름으로 저장" 같은 자연어를 발화하면 LLM 이 본 카테고리의 tool 들을 호출한다.
6
+ *
7
+ * 노출되는 tool:
8
+ * - listImportAdapters (read) : 등록된 FormatAdapter 능력 조회
9
+ * - importBoardAsync (external) : Attachment → ImportSession 비동기 시작
10
+ * - getImportSession (read) : ImportSession 진행/결과 조회
11
+ * - materializeImportSession (external) : ImportSession.result → 새 Board entity
12
+ *
13
+ * dispatch wiring:
14
+ * board-ai 의 chat 메서드가 ToolCallContext (resolver state — domain/user/tx) 를 closure 로
15
+ * 주입한다. builder 는 ctx.state 에서 typeorm getRepository / domain / user 를 추출해
16
+ * service-level 함수 (import-actions, materialize-from-session) 를 호출한다.
17
+ *
18
+ * 이 layer 가 없으면 builder 가 typeorm 에 직접 의존해야 했고, board-ai 가 typeorm 까지
19
+ * 알게 되는 leakage 가 생겼을 것 — 의도적으로 builder 가 ctx.state.typeorm 같은 식으로
20
+ * 접근하지 않고 things-factory 표준 getRepository 를 호출.
21
+ */
22
+ import { getRepository } from '@things-factory/shell'
23
+ import {
24
+ registerToolCategory,
25
+ type ToolCategory,
26
+ type ToolSpec
27
+ } from '@things-factory/ai-client-base'
28
+
29
+ // 모든 server-side 의존성 (entity / service 함수 / 외부 패키지) 은 module-level import
30
+ // 없이 lazy require — module load 만으로 typeorm decorator / 외부 controllers chain
31
+ // (예: board-service 의 puppeteer pool) 이 평가되어 jest 환경에서 깨지는 것을 회피.
32
+ // 운영에서는 builder 첫 호출 시 cache 채워져 이후 호출은 직접 dispatch.
33
+ //
34
+ // 이 패턴은 LLM tool layer 의 일반 정책으로 가져갈 수 있다 — registerToolCategory 가
35
+ // 평가되는 시점은 server bootstrap 의 매우 이른 단계라, 그때 typeorm DataSource init 이
36
+ // 아직 끝나지 않았을 가능성이 있다.
37
+ let _depsCache: any | undefined
38
+ function getDeps(): any {
39
+ if (!_depsCache) {
40
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
41
+ const isMod = require('./import-session/import-session')
42
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
43
+ const bsMod = require('@things-factory/board-service')
44
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
45
+ const actionsMod = require('./import-session/import-actions')
46
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
47
+ const materializeMod = require('./import-session/materialize-from-session')
48
+ _depsCache = {
49
+ ImportSession: isMod.ImportSession,
50
+ Board: bsMod.Board,
51
+ Group: bsMod.Group,
52
+ startImportJobAsync: actionsMod.startImportJobAsync,
53
+ fetchImportSession: actionsMod.fetchImportSession,
54
+ describeAdapter: actionsMod.describeAdapter,
55
+ materializeFromSession: materializeMod.materializeFromSession
56
+ }
57
+ }
58
+ return _depsCache
59
+ }
60
+
61
+ /**
62
+ * tool builder 의 ctx.state 가 도메인 식별을 갖는지 검증. 미설정이면 명확한 에러로 실패 —
63
+ * board-ai 의 toolCallContext wiring 누락 / 비인증 호출 등을 빠르게 찾기 위해.
64
+ */
65
+ function requireDomainContext(state: any): { domain: any; user: any } {
66
+ if (!state || !state.domain) {
67
+ throw new Error(
68
+ '[board-import] tool builder 의 ctx.state.domain 미설정. ' +
69
+ 'board-ai 의 ChatOptions.toolCallContext 에 ResolverContext.state 가 전달됐는지 확인.'
70
+ )
71
+ }
72
+ return { domain: state.domain, user: state.user }
73
+ }
74
+
75
+ const importToolsCategory: ToolCategory = {
76
+ name: 'board-import',
77
+ description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',
78
+ specs: [
79
+ {
80
+ kind: 'read',
81
+ name: 'listImportAdapters',
82
+ description:
83
+ '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +
84
+ '사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',
85
+ schema: { type: 'object', properties: {} },
86
+ builder: async () => {
87
+ // 동적 import — pipeline 모듈의 DEFAULT_ADAPTERS 를 inspect.
88
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
89
+ const pipelineModule = require('./pipeline/index')
90
+ const adapters: any[] = pipelineModule.DEFAULT_ADAPTERS ?? []
91
+ return adapters.map(getDeps().describeAdapter)
92
+ }
93
+ },
94
+ {
95
+ kind: 'external',
96
+ name: 'importBoardAsync',
97
+ description:
98
+ 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +
99
+ '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +
100
+ 'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',
101
+ schema: {
102
+ type: 'object',
103
+ properties: {
104
+ attachmentId: {
105
+ type: 'string',
106
+ description: 'Attachment id (attachment-base 에 미리 업로드된 도면 파일).'
107
+ },
108
+ chatSessionId: {
109
+ type: 'string',
110
+ description: 'board-ai chat session id (선택). 진행 메시지가 chat 흐름에 합류.'
111
+ },
112
+ scopes: {
113
+ type: 'array',
114
+ items: { type: 'string' },
115
+ description: 'ImportRule 의 scope (예: ["fmsim"]). 도메인별 매핑 규칙 활성화.'
116
+ },
117
+ parseOptions: {
118
+ type: 'object',
119
+ description: 'Adapter 파싱 옵션 (excludeLayers / maxEntities / context 등).',
120
+ additionalProperties: true
121
+ }
122
+ },
123
+ required: ['attachmentId']
124
+ },
125
+ builder: async (args, ctx) => {
126
+ const { domain, user } = requireDomainContext(ctx?.state)
127
+ const session = await getDeps().startImportJobAsync(
128
+ {
129
+ attachmentId: args.attachmentId,
130
+ chatSessionId: args.chatSessionId,
131
+ scopes: args.scopes,
132
+ parseOptions: args.parseOptions
133
+ },
134
+ { domain, user },
135
+ getRepository(getDeps().ImportSession) as any
136
+ )
137
+ return {
138
+ sessionId: session.id,
139
+ status: session.status,
140
+ progress: session.progress,
141
+ message: session.message
142
+ }
143
+ }
144
+ },
145
+ {
146
+ kind: 'read',
147
+ name: 'getImportSession',
148
+ description:
149
+ 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +
150
+ '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +
151
+ 'message 에 에러 사유.',
152
+ schema: {
153
+ type: 'object',
154
+ properties: {
155
+ sessionId: { type: 'string', description: 'ImportSession id (importBoardAsync 가 반환).' }
156
+ },
157
+ required: ['sessionId']
158
+ },
159
+ builder: async (args, ctx) => {
160
+ const { domain, user } = requireDomainContext(ctx?.state)
161
+ const session = await getDeps().fetchImportSession(
162
+ args.sessionId,
163
+ { domain, user },
164
+ getRepository(getDeps().ImportSession) as any
165
+ )
166
+ if (!session) return { found: false }
167
+ return {
168
+ found: true,
169
+ sessionId: session.id,
170
+ status: session.status,
171
+ progress: session.progress,
172
+ message: session.message,
173
+ // result 는 큰 boardModel 을 포함할 수 있어 status 가 completed 일 때만.
174
+ result: session.status === 'completed' ? session.result : undefined,
175
+ totalEntities: session.totalEntities,
176
+ completedAt: session.completedAt
177
+ }
178
+ }
179
+ },
180
+ {
181
+ kind: 'external',
182
+ name: 'materializeImportSession',
183
+ description:
184
+ '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +
185
+ 'completed 인 세션만 처리. 결과 Board 는 state="draft" 로 생성되므로 사용자가 검수 후 ' +
186
+ '발행해야 한다.',
187
+ themeNote:
188
+ '⚠ 사용자의 명시적 confirm 후에만 호출. import 결과 미리보기를 보여주고 "이 이름으로 ' +
189
+ '저장하시겠어요?" 같은 확인 받은 다음 발동.',
190
+ schema: {
191
+ type: 'object',
192
+ properties: {
193
+ sessionId: { type: 'string' },
194
+ name: { type: 'string', description: '새 Board 이름 — 같은 도메인 내 unique.' },
195
+ description: { type: 'string' },
196
+ groupId: { type: 'string' },
197
+ type: { type: 'string', enum: ['main', 'sub', 'popup'] },
198
+ thumbnail: {
199
+ type: 'string',
200
+ description: 'Base64 thumbnail. 미지정 시 빈 placeholder 사용 (별도 thumbnail 생성 호출).'
201
+ }
202
+ },
203
+ required: ['sessionId', 'name']
204
+ },
205
+ builder: async (args, ctx) => {
206
+ const { domain, user } = requireDomainContext(ctx?.state)
207
+ const session = await getDeps().fetchImportSession(
208
+ args.sessionId,
209
+ { domain, user },
210
+ getRepository(getDeps().ImportSession) as any
211
+ )
212
+ const { Board, Group, materializeFromSession } = getDeps()
213
+ const board = await materializeFromSession(
214
+ session as any,
215
+ {
216
+ sessionId: args.sessionId,
217
+ name: args.name,
218
+ description: args.description,
219
+ groupId: args.groupId,
220
+ type: args.type,
221
+ thumbnail: args.thumbnail
222
+ },
223
+ { domain, user },
224
+ {
225
+ boardRepo: getRepository(Board) as any,
226
+ groupRepo: getRepository(Group) as any
227
+ }
228
+ )
229
+ // 큰 model 본문은 LLM 회신에서 제외 — id 와 메타만.
230
+ return {
231
+ boardId: (board as any).id,
232
+ name: (board as any).name,
233
+ state: (board as any).state,
234
+ sourceImportSessionId: (board as any).sourceImportSessionId
235
+ }
236
+ }
237
+ }
238
+ ] as ToolSpec[]
239
+ }
240
+
241
+ // 등록 — module 로드 시 side-effect.
242
+ registerToolCategory(importToolsCategory)
243
+
244
+ /**
245
+ * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가
246
+ * 부담스러울 때 import 후 명시 호출 가능.
247
+ */
248
+ export function registerBoardImportTools(): void {
249
+ registerToolCategory(importToolsCategory)
250
+ }
251
+
252
+ /**
253
+ * 테스트용 — registerToolCategory 의 입력으로 사용된 카테고리 객체. 외부에서 inspect 가능.
254
+ */
255
+ export const BOARD_IMPORT_TOOL_CATEGORY = importToolsCategory
@@ -0,0 +1,117 @@
1
+ // ImportSession (비동기 import) — things-factory 모듈 통합용
2
+ import {
3
+ entities as ImportSessionEntities,
4
+ resolverClasses as ImportSessionResolverClasses
5
+ } from './import-session/index'
6
+ export * from './import-session/index'
7
+
8
+ export const entities = [...ImportSessionEntities]
9
+ export const schema = { resolverClasses: [...ImportSessionResolverClasses] }
10
+
11
+ // Pipeline (high-level)
12
+ export { importBoard } from './pipeline/index'
13
+ export type { ImportOptions, ImportResult } from './pipeline/index'
14
+
15
+ // Stage 별 (low-level)
16
+ export {
17
+ applyMapping,
18
+ GENERIC_OBJECT_TYPE,
19
+ DEFAULT_CATEGORY_FALLBACK
20
+ } from './pipeline/stage2-mapping'
21
+ export { assembleBoard } from './pipeline/stage3-board'
22
+ export { applyDataBinding } from './pipeline/stage4-binding'
23
+ export type { BoardModel, BoardComponent, AssembleOptions } from './pipeline/stage3-board'
24
+ export type { MappingOptions } from './pipeline/stage2-mapping'
25
+ export type { BindingOptions } from './pipeline/stage4-binding'
26
+
27
+ // Converters
28
+ export {
29
+ convertGenericTo, // deprecated alias — keep for backward-compat
30
+ convertPlaceholderTo,
31
+ isPlaceholderType,
32
+ CORE_PLACEHOLDER_TYPES
33
+ } from './converters/generic-to'
34
+ export type { ConvertOptions } from './converters/generic-to'
35
+
36
+ // Adapters
37
+ export { DxfAdapter } from './adapters/dxf-adapter'
38
+ export { ImageAdapter } from './adapters/image-adapter'
39
+
40
+ // Registry
41
+ export {
42
+ registerImportRules,
43
+ registerSubscriptionRules,
44
+ register3DPresets,
45
+ getImportRules,
46
+ getSubscriptionRules,
47
+ get3DPresets,
48
+ find3DPreset,
49
+ findSubscriptionRule,
50
+ clearImportRules,
51
+ clearSubscriptionRules,
52
+ clear3DPresets,
53
+ clearAllRegistries
54
+ } from './registry/index'
55
+
56
+ // Types — Universal Entity
57
+ export type {
58
+ EntityKind,
59
+ EntityAttribs,
60
+ EntityRelation,
61
+ Vec2,
62
+ Vec3,
63
+ Size2,
64
+ Size3,
65
+ BoundingBox,
66
+ UniversalEntity,
67
+ ParseResult,
68
+ AdapterMetadata,
69
+ ParseOptions,
70
+ AdapterInput,
71
+ FormatAdapter,
72
+ AdapterCapabilities
73
+ } from './types/index'
74
+
75
+ // Types — Registry
76
+ export type {
77
+ ComponentCategory,
78
+ ImportRule,
79
+ ImportMatcher,
80
+ ImportBuildSpec,
81
+ IdStrategy,
82
+ ComponentDraft,
83
+ ThreeDPreset,
84
+ ThreeDGeometry,
85
+ ThreeDMaterial,
86
+ SubscriptionRule,
87
+ MappingResult,
88
+ MappingWarning
89
+ } from './types/registry'
90
+
91
+ // AI domain interface (Phase 3+) — provider 추상화는 @things-factory/ai-client-base
92
+ export {
93
+ setAIClient,
94
+ getAIClient,
95
+ DefaultBoardImportAI
96
+ } from './ai/types'
97
+
98
+ // LLM tool 노출 — board-ai 의 chat 흐름이 자동 dispatch 가능하도록 등록.
99
+ // import 시 module side-effect 로 registerToolCategory 발동. 명시 호출도 가능.
100
+ export {
101
+ registerBoardImportTools,
102
+ BOARD_IMPORT_TOOL_CATEGORY
103
+ } from './import-tools'
104
+ export type {
105
+ BoardImportAI,
106
+ AIClient, // deprecated alias for BoardImportAI
107
+ ConventionInferenceOptions,
108
+ RuleSuggestion,
109
+ ClassificationOptions,
110
+ LabeledEntity,
111
+ EntityClassification,
112
+ ImageParseOptions
113
+ } from './ai/types'
114
+
115
+ // 하이브리드 추출 모듈 (Phase Board-AI §3.8) — 순수 추가. 네임스페이스로 묶어 충돌 회피.
116
+ // 기존 어댑터/파이프라인(ImageAdapter, stage1-4) 무변경. 본격 테스트 단계에서 stub → 실 impl 교체.
117
+ export * as extraction from './extraction'
@@ -0,0 +1,184 @@
1
+ /**
2
+ * High-level Pipeline — 도면 한 장 → 운영 가능한 Board Model.
3
+ *
4
+ * 백엔드에서 호출:
5
+ * ```ts
6
+ * import { importBoard } from '@things-factory/board-import'
7
+ *
8
+ * const buf = await fs.promises.readFile(path)
9
+ * const result = await importBoard({ file: buf, filename: 'plant.dxf', scopes: ['fmsim'] })
10
+ * ```
11
+ */
12
+ import { DxfAdapter } from '../adapters/dxf-adapter'
13
+ import { ImageAdapter } from '../adapters/image-adapter'
14
+ import type { AdapterInput, BoundingBox, FormatAdapter, ParseOptions, ParseResult } from '../types/index'
15
+ import type { MappingResult } from '../types/registry'
16
+ import { applyMapping } from './stage2-mapping'
17
+ import { assembleBoard, type AssembleOptions, type BoardModel } from './stage3-board'
18
+ import { applyDataBinding } from './stage4-binding'
19
+
20
+ export interface ImportOptions {
21
+ file: AdapterInput
22
+ filename?: string
23
+ /** 등록된 매핑 규칙의 어느 scope 를 사용할지 */
24
+ scopes?: string[]
25
+ /** 어댑터 옵션 (excludeLayers 등) */
26
+ parseOptions?: ParseOptions
27
+ /** 좌표 normalize (좌상단 0,0) */
28
+ normalizeOrigin?: boolean
29
+ /** Y 축 뒤집기 (CAD Y-up → scene Y-down) */
30
+ flipY?: boolean
31
+ /** scene unit 변환 배율 */
32
+ scale?: number
33
+ /** 데이터 바인딩 stage 적용 여부 */
34
+ applyBinding?: boolean
35
+ /** 사용자 정의 어댑터 (있으면 디폴트 검색 대신 사용) */
36
+ adapter?: FormatAdapter
37
+ }
38
+
39
+ export interface ImportResult {
40
+ boardModel: BoardModel
41
+ mapping: MappingResult
42
+ parse: ParseResult
43
+ }
44
+
45
+ /**
46
+ * 변환 시안 (variant) — AI 가 같은 도면을 다른 관점으로 해석한 결과. 사용자가 review 단계에서
47
+ * 시각 비교 후 선택. parse 가 multi-variant 응답하면 (현재 ImageAdapter), 각 variant 마다
48
+ * 독립 mapping + assemble. DXF 같은 single-variant adapter 는 1 boardModel 만.
49
+ */
50
+ export interface VariantResult {
51
+ id: string
52
+ label: string
53
+ description?: string
54
+ /** 보드 정체성 — name 자동 추천 기반. */
55
+ subjectName?: string
56
+ /** 보드 정체성 — description 자동 추천 기반. */
57
+ subjectDescription?: string
58
+ /** AI 의 의도 narrative — review 카드 표시 (transient). */
59
+ importStrategy?: string
60
+ boardModel: BoardModel
61
+ /** mapping 통계 (시안별 — entities 가 다르므로 stats 도 다름). */
62
+ stats: MappingResult['stats']
63
+ }
64
+
65
+ export interface ImportVariantsResult {
66
+ variants: VariantResult[]
67
+ /** 각 시안의 mapping 결과 묶음. variants[i] 와 같은 인덱스. */
68
+ mappings: MappingResult[]
69
+ parse: ParseResult
70
+ }
71
+
72
+ // Adapter detection 순서가 중요: 정밀 형식 (DXF) 먼저, 폴백 / 비정형 (Image) 나중.
73
+ // detect() 가 filename 확장자 우선이라 충돌은 거의 없지만, 시그니처 모호한 경우 (예:
74
+ // 확장자 없는 binary stream) 도 정밀 형식이 먼저 잡히는 게 안전.
75
+ //
76
+ // import-tools.ts 의 listImportAdapters builder 가 본 배열을 inspect — 외부 export.
77
+ export const DEFAULT_ADAPTERS: FormatAdapter[] = [new DxfAdapter(), new ImageAdapter()]
78
+
79
+ export async function importBoard(opts: ImportOptions): Promise<ImportResult> {
80
+ // 단일 변형 — multi-variant adapter 라도 첫 시안만 반환. back-compat.
81
+ const result = await importBoardVariants(opts)
82
+ const first = result.variants[0]
83
+ if (!first) {
84
+ throw new Error('Import produced no variants.')
85
+ }
86
+ return {
87
+ boardModel: first.boardModel,
88
+ mapping: result.mappings[0],
89
+ parse: result.parse
90
+ }
91
+ }
92
+
93
+ /**
94
+ * 다중 시안 import — parse 1번 (비싼 VLM/DXF), 시안별 mapping + assemble.
95
+ *
96
+ * AI multi-variant adapter (ImageAdapter) 는 parse.variants 에 N 시안 반환 → 각각 mapping +
97
+ * assemble. Single-variant adapter (DxfAdapter) 는 parse.variants 미존재 → 단일 시안 1 boardModel.
98
+ */
99
+ export async function importBoardVariants(opts: ImportOptions): Promise<ImportVariantsResult> {
100
+ const adapter = opts.adapter ?? (await pickAdapter(opts.file, opts.filename))
101
+ if (!adapter) throw new Error('지원되지 않는 도면 형식이거나 어댑터를 식별할 수 없습니다.')
102
+
103
+ const parsed = await adapter.parse(opts.file, opts.parseOptions)
104
+ const importMeta = {
105
+ format: adapter.format,
106
+ sourceBbox: parsed.bbox,
107
+ units: parsed.units,
108
+ importedAt: new Date().toISOString()
109
+ }
110
+ const assembleOptions: AssembleOptions = {
111
+ scopes: opts.scopes,
112
+ normalizeOrigin: opts.normalizeOrigin ?? true,
113
+ flipY: opts.flipY ?? true,
114
+ scale: opts.scale ?? 1,
115
+ importMeta
116
+ }
117
+
118
+ // multi-variant adapter — parse.variants 가 존재
119
+ if (Array.isArray(parsed.variants) && parsed.variants.length > 0) {
120
+ const variants: VariantResult[] = []
121
+ const mappings: MappingResult[] = []
122
+ for (const v of parsed.variants) {
123
+ // 각 시안의 entities 로 mapping (시안별로 다름)
124
+ const variantParsed: ParseResult = {
125
+ ...parsed,
126
+ entities: v.entities,
127
+ bbox: v.bbox ?? parsed.bbox
128
+ }
129
+ const mapping = applyMapping(variantParsed, { scopes: opts.scopes })
130
+ mappings.push(mapping)
131
+ let boardModel = assembleBoard(mapping.components, variantParsed.bbox, assembleOptions)
132
+ if (opts.applyBinding !== false) {
133
+ boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })
134
+ }
135
+ variants.push({
136
+ id: v.id,
137
+ label: v.label,
138
+ description: v.description,
139
+ subjectName: v.subjectName,
140
+ subjectDescription: v.subjectDescription,
141
+ importStrategy: v.importStrategy,
142
+ boardModel,
143
+ stats: mapping.stats
144
+ })
145
+ }
146
+ return { variants, mappings, parse: parsed }
147
+ }
148
+
149
+ // single-variant adapter (DXF 등) — 단일 boardModel
150
+ const mapping = applyMapping(parsed, { scopes: opts.scopes })
151
+ let boardModel = assembleBoard(mapping.components, parsed.bbox, assembleOptions)
152
+ if (opts.applyBinding !== false) {
153
+ boardModel = applyDataBinding(boardModel, { scopes: opts.scopes })
154
+ }
155
+ return {
156
+ variants: [
157
+ {
158
+ id: 'default',
159
+ label: 'default',
160
+ boardModel,
161
+ stats: mapping.stats
162
+ }
163
+ ],
164
+ mappings: [mapping],
165
+ parse: parsed
166
+ }
167
+ }
168
+
169
+ async function pickAdapter(
170
+ file: AdapterInput,
171
+ filename?: string
172
+ ): Promise<FormatAdapter | undefined> {
173
+ for (const a of DEFAULT_ADAPTERS) {
174
+ if (await a.detect(file, filename)) return a
175
+ }
176
+ return undefined
177
+ }
178
+
179
+ export { applyMapping } from './stage2-mapping'
180
+ export { assembleBoard } from './stage3-board'
181
+ export { applyDataBinding } from './stage4-binding'
182
+ export type { BoardModel, BoardComponent, AssembleOptions } from './stage3-board'
183
+ export type { MappingOptions } from './stage2-mapping'
184
+ export type { BindingOptions } from './stage4-binding'