@things-factory/board-import 10.1.29 → 10.1.31
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.
|
@@ -79,9 +79,13 @@ function requireDomainContext(state) {
|
|
|
79
79
|
* `ai-assistant:mutation` — could create a Board through it.
|
|
80
80
|
*
|
|
81
81
|
* The check lives in the builder rather than in whichever chat door dispatches the tool: there is
|
|
82
|
-
* more than one such door, and the effect has to be guarded wherever it is reached from.
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* more than one such door, and the effect has to be guarded wherever it is reached from.
|
|
83
|
+
*
|
|
84
|
+
* Whether the tool is **offered** at all is a separate question, and since 2026-09-18 the spec does
|
|
85
|
+
* not answer it with a copy of this object (ADR-0071 decision 2). The spec names the GraphQL doors
|
|
86
|
+
* it reaches and the gate resolves each one against that field's own `@privilege` — the same
|
|
87
|
+
* declarations this constant mirrors (`import-session-resolver.ts`). This constant stays because it
|
|
88
|
+
* guards the **effect**, which is reached from more places than the offering.
|
|
85
89
|
*/
|
|
86
90
|
exports.IMPORT_TOOL_PRIVILEGE = {
|
|
87
91
|
importBoardAsync: { category: 'board-import', privilege: 'mutation' },
|
|
@@ -123,6 +127,8 @@ const importToolsCategory = {
|
|
|
123
127
|
{
|
|
124
128
|
kind: 'read',
|
|
125
129
|
name: 'listImportAdapters',
|
|
130
|
+
/* 등록된 어댑터를 그 자리에서 셀 뿐이다 — 저장소에 닿지 않으므로 지나는 문이 없다. */
|
|
131
|
+
doors: [],
|
|
126
132
|
description: '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +
|
|
127
133
|
'사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',
|
|
128
134
|
schema: { type: 'object', properties: {} },
|
|
@@ -137,7 +143,7 @@ const importToolsCategory = {
|
|
|
137
143
|
{
|
|
138
144
|
kind: 'external',
|
|
139
145
|
name: 'importBoardAsync',
|
|
140
|
-
|
|
146
|
+
doors: ['Mutation.importBoardAsync'],
|
|
141
147
|
description: 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +
|
|
142
148
|
'반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +
|
|
143
149
|
'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',
|
|
@@ -184,7 +190,7 @@ const importToolsCategory = {
|
|
|
184
190
|
{
|
|
185
191
|
kind: 'read',
|
|
186
192
|
name: 'getImportSession',
|
|
187
|
-
|
|
193
|
+
doors: ['Query.importSession'],
|
|
188
194
|
description: 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +
|
|
189
195
|
'동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +
|
|
190
196
|
'message 에 에러 사유.',
|
|
@@ -216,7 +222,7 @@ const importToolsCategory = {
|
|
|
216
222
|
{
|
|
217
223
|
kind: 'external',
|
|
218
224
|
name: 'materializeImportSession',
|
|
219
|
-
|
|
225
|
+
doors: ['Mutation.materializeImportSession'],
|
|
220
226
|
description: '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +
|
|
221
227
|
'completed 인 세션만 처리. 결과 Board 는 state="draft" 로 생성되므로 사용자가 검수 후 ' +
|
|
222
228
|
'발행해야 한다.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import-tools.js","sourceRoot":"","sources":["../../server/service/import-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-import 의 LLM tool 카테고리 등록.\n *\n * board-ai 의 chat 흐름 안에서 사용자가 \"이 도면을 보드로 만들어줘\" / \"import 결과 검수\n * 후 이 이름으로 저장\" 같은 자연어를 발화하면 LLM 이 본 카테고리의 tool 들을 호출한다.\n *\n * 노출되는 tool:\n * - listImportAdapters (read) : 등록된 FormatAdapter 능력 조회\n * - importBoardAsync (external) : Attachment → ImportSession 비동기 시작\n * - getImportSession (read) : ImportSession 진행/결과 조회\n * - materializeImportSession (external) : ImportSession.result → 새 Board entity\n *\n * dispatch wiring:\n * board-ai 의 chat 메서드가 ToolCallContext (resolver state — domain/user/tx) 를 closure 로\n * 주입한다. builder 는 ctx.state 에서 typeorm getRepository / domain / user 를 추출해\n * service-level 함수 (import-actions, materialize-from-session) 를 호출한다.\n *\n * 이 layer 가 없으면 builder 가 typeorm 에 직접 의존해야 했고, board-ai 가 typeorm 까지\n * 알게 되는 leakage 가 생겼을 것 — 의도적으로 builder 가 ctx.state.typeorm 같은 식으로\n * 접근하지 않고 things-factory 표준 getRepository 를 호출.\n */\nimport { getRepository } from '@things-factory/shell'\nimport {\n registerToolCategory,\n type ToolCategory,\n type ToolSpec\n} from '@things-factory/ai-client-base'\n\n// 모든 server-side 의존성 (entity / service 함수 / 외부 패키지) 은 module-level import\n// 없이 lazy require — module load 만으로 typeorm decorator / 외부 controllers chain\n// (예: board-service 의 puppeteer pool) 이 평가되어 jest 환경에서 깨지는 것을 회피.\n// 운영에서는 builder 첫 호출 시 cache 채워져 이후 호출은 직접 dispatch.\n//\n// 이 패턴은 LLM tool layer 의 일반 정책으로 가져갈 수 있다 — registerToolCategory 가\n// 평가되는 시점은 server bootstrap 의 매우 이른 단계라, 그때 typeorm DataSource init 이\n// 아직 끝나지 않았을 가능성이 있다.\nlet _depsCache: any | undefined\nfunction getDeps(): any {\n if (!_depsCache) {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const isMod = require('./import-session/import-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const bsMod = require('@things-factory/board-service')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const actionsMod = require('./import-session/import-actions')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const materializeMod = require('./import-session/materialize-from-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const authMod = require('@things-factory/auth-base')\n _depsCache = {\n checkPermission: authMod.checkPermission,\n ImportSession: isMod.ImportSession,\n Board: bsMod.Board,\n Group: bsMod.Group,\n startImportJobAsync: actionsMod.startImportJobAsync,\n fetchImportSession: actionsMod.fetchImportSession,\n describeAdapter: actionsMod.describeAdapter,\n materializeFromSession: materializeMod.materializeFromSession\n }\n }\n return _depsCache\n}\n\n/**\n * tool builder 의 ctx.state 가 도메인 식별을 갖는지 검증. 미설정이면 명확한 에러로 실패 —\n * board-ai 의 toolCallContext wiring 누락 / 비인증 호출 등을 빠르게 찾기 위해.\n */\nfunction requireDomainContext(state: any): { domain: any; user: any } {\n if (!state || !state.domain) {\n throw new Error(\n '[board-import] tool builder 의 ctx.state.domain 미설정. ' +\n 'board-ai 의 ChatOptions.toolCallContext 에 ResolverContext.state 가 전달됐는지 확인.'\n )\n }\n return { domain: state.domain, user: state.user }\n}\n\n/*\n * A tool is a second entrance to a door that already exists, so it asks for what that door asks for.\n *\n * `materializeImportSession` and `importBoardAsync` sit behind `board-import:mutation` as GraphQL\n * mutations, and `importSession` behind `board-import:query` (import-session-resolver.ts). The tools\n * called the same service functions with no check at all, so whoever could open a conversation —\n * `ai-assistant:mutation` — could create a Board through it.\n *\n * The check lives in the builder rather than in whichever chat door dispatches the tool: there is\n * more than one such door, and the effect has to be guarded wherever it is reached from. The same\n * object is also declared on the spec as `privilege`, which is what a door reads to decide whether\n * to offer the tool at all; declaring and enforcing from one constant keeps the two from drifting.\n */\nexport const IMPORT_TOOL_PRIVILEGE = {\n importBoardAsync: { category: 'board-import', privilege: 'mutation' },\n getImportSession: { category: 'board-import', privilege: 'query' },\n materializeImportSession: { category: 'board-import', privilege: 'mutation' }\n} as const\n\ntype GuardedImportTool = keyof typeof IMPORT_TOOL_PRIVILEGE\n\nasync function requireImportPrivilege(tool: GuardedImportTool, state: any): Promise<{ domain: any; user: any }> {\n const { domain, user } = requireDomainContext(state)\n const required = IMPORT_TOOL_PRIVILEGE[tool]\n const allowed = await getDeps().checkPermission(required, user, domain, state.unsafeIP, state.prohibitedPrivileges)\n if (!allowed) {\n throw new Error(\n `[board-import] '${tool}' needs ${required.category}:${required.privilege}. ` +\n 'Being allowed to talk to the assistant does not open it.'\n )\n }\n return { domain, user }\n}\n\nconst importToolsCategory: ToolCategory = {\n name: 'board-import',\n description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',\n /*\n * ⚠ 이 지침은 **아키텍트가 이 파일의 도구 설명만 보고 쓴 최소본**이다(2026-09-09).\n *\n * `registerToolCategory` 가 지침 없는 카테고리를 거절하게 되면서, 이 카테고리가 걸리는 유일한\n * 자리였다. 도구를 등록한 쪽이 규율을 소유하므로(§`ToolCategory.guidance`), **이 도구를 아는\n * 사람이 도메인 규율로 바꿔 주십시오.** 아래는 위 `description` 들에서 직접 읽어낸 것만 적었고\n * 도메인 지식을 지어내지 않았다.\n *\n * 두 줄이 특히 중요하다 — 이 카테고리에는 **즉시 돌아오지만 끝나지 않은** 도구와 **확인을 받아야\n * 하는** 도구가 있다. 그 둘이 지침이 필요한 전형이다.\n */\n guidance: [\n '- 어떤 형식을 변환할 수 있는지 물으면 추측하지 말고 `listImportAdapters` 를 호출한다. 등록된 어댑터만이 답이다.',\n '- `importBoardAsync` 는 **즉시 돌아오지만 변환은 끝나지 않았다.** 세션을 돌려줄 뿐이다 — `getImportSession` 으로 `completed` 를 확인하기 전에 \"변환했습니다\"라고 답하면 사용자는 없는 결과를 찾는다.',\n '- `getImportSession` 이 `failed` 면 그 `message` 를 그대로 옮긴다. 원인을 지어내지 않는다.',\n '- `materializeImportSession` 은 **사용자가 명시적으로 확인한 뒤에만** 호출한다. 미리보기를 보여 주고 저장 여부를 물은 다음이다. 만들어지는 Board 는 `draft` 이므로 발행은 사용자가 한다.',\n '- attachment id 를 지어내지 않는다. 미리 업로드된 것만 있고, 없으면 업로드가 먼저라고 말한다.'\n ].join('\\n'),\n specs: [\n {\n kind: 'read',\n name: 'listImportAdapters',\n description:\n '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +\n '사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',\n schema: { type: 'object', properties: {} },\n builder: async () => {\n // 동적 import — pipeline 모듈의 DEFAULT_ADAPTERS 를 inspect.\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const pipelineModule = require('./pipeline/index')\n const adapters: any[] = pipelineModule.DEFAULT_ADAPTERS ?? []\n return adapters.map(getDeps().describeAdapter)\n }\n },\n {\n kind: 'external',\n name: 'importBoardAsync',\n privilege: IMPORT_TOOL_PRIVILEGE.importBoardAsync,\n description:\n 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +\n '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +\n 'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',\n schema: {\n type: 'object',\n properties: {\n attachmentId: {\n type: 'string',\n description: 'Attachment id (attachment-base 에 미리 업로드된 도면 파일).'\n },\n chatSessionId: {\n type: 'string',\n description: 'board-ai chat session id (선택). 진행 메시지가 chat 흐름에 합류.'\n },\n scopes: {\n type: 'array',\n items: { type: 'string' },\n description: 'ImportRule 의 scope (예: [\"fmsim\"]). 도메인별 매핑 규칙 활성화.'\n },\n parseOptions: {\n type: 'object',\n description: 'Adapter 파싱 옵션 (excludeLayers / maxEntities / context 등).',\n additionalProperties: true\n }\n },\n required: ['attachmentId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('importBoardAsync', ctx?.state)\n const session = await getDeps().startImportJobAsync(\n {\n attachmentId: args.attachmentId,\n chatSessionId: args.chatSessionId,\n scopes: args.scopes,\n parseOptions: args.parseOptions\n },\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n return {\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message\n }\n }\n },\n {\n kind: 'read',\n name: 'getImportSession',\n privilege: IMPORT_TOOL_PRIVILEGE.getImportSession,\n description:\n 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +\n '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +\n 'message 에 에러 사유.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string', description: 'ImportSession id (importBoardAsync 가 반환).' }\n },\n required: ['sessionId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('getImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n if (!session) return { found: false }\n return {\n found: true,\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message,\n // result 는 큰 boardModel 을 포함할 수 있어 status 가 completed 일 때만.\n result: session.status === 'completed' ? session.result : undefined,\n totalEntities: session.totalEntities,\n completedAt: session.completedAt\n }\n }\n },\n {\n kind: 'external',\n name: 'materializeImportSession',\n privilege: IMPORT_TOOL_PRIVILEGE.materializeImportSession,\n description:\n '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +\n 'completed 인 세션만 처리. 결과 Board 는 state=\"draft\" 로 생성되므로 사용자가 검수 후 ' +\n '발행해야 한다.',\n themeNote:\n '⚠ 사용자의 명시적 confirm 후에만 호출. import 결과 미리보기를 보여주고 \"이 이름으로 ' +\n '저장하시겠어요?\" 같은 확인 받은 다음 발동.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n name: { type: 'string', description: '새 Board 이름 — 같은 도메인 내 unique.' },\n description: { type: 'string' },\n groupId: { type: 'string' },\n type: { type: 'string', enum: ['main', 'sub', 'popup'] },\n thumbnail: {\n type: 'string',\n description: 'Base64 thumbnail. 미지정 시 빈 placeholder 사용 (별도 thumbnail 생성 호출).'\n }\n },\n required: ['sessionId', 'name']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('materializeImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n const { Board, Group, materializeFromSession } = getDeps()\n const board = await materializeFromSession(\n session as any,\n {\n sessionId: args.sessionId,\n name: args.name,\n description: args.description,\n groupId: args.groupId,\n type: args.type,\n thumbnail: args.thumbnail\n },\n { domain, user },\n {\n boardRepo: getRepository(Board) as any,\n groupRepo: getRepository(Group) as any\n }\n )\n // 큰 model 본문은 LLM 회신에서 제외 — id 와 메타만.\n return {\n boardId: (board as any).id,\n name: (board as any).name,\n state: (board as any).state,\n sourceImportSessionId: (board as any).sourceImportSessionId\n }\n }\n }\n ] as ToolSpec[]\n}\n\n// 등록 — module 로드 시 side-effect.\nregisterToolCategory(importToolsCategory)\n\n/**\n * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가\n * 부담스러울 때 import 후 명시 호출 가능.\n */\nexport function registerBoardImportTools(): void {\n registerToolCategory(importToolsCategory)\n}\n\n/**\n * 테스트용 — registerToolCategory 의 입력으로 사용된 카테고리 객체. 외부에서 inspect 가능.\n */\nexport const BOARD_IMPORT_TOOL_CATEGORY = importToolsCategory\n"]}
|
|
1
|
+
{"version":3,"file":"import-tools.js","sourceRoot":"","sources":["../../server/service/import-tools.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * board-import 의 LLM tool 카테고리 등록.\n *\n * board-ai 의 chat 흐름 안에서 사용자가 \"이 도면을 보드로 만들어줘\" / \"import 결과 검수\n * 후 이 이름으로 저장\" 같은 자연어를 발화하면 LLM 이 본 카테고리의 tool 들을 호출한다.\n *\n * 노출되는 tool:\n * - listImportAdapters (read) : 등록된 FormatAdapter 능력 조회\n * - importBoardAsync (external) : Attachment → ImportSession 비동기 시작\n * - getImportSession (read) : ImportSession 진행/결과 조회\n * - materializeImportSession (external) : ImportSession.result → 새 Board entity\n *\n * dispatch wiring:\n * board-ai 의 chat 메서드가 ToolCallContext (resolver state — domain/user/tx) 를 closure 로\n * 주입한다. builder 는 ctx.state 에서 typeorm getRepository / domain / user 를 추출해\n * service-level 함수 (import-actions, materialize-from-session) 를 호출한다.\n *\n * 이 layer 가 없으면 builder 가 typeorm 에 직접 의존해야 했고, board-ai 가 typeorm 까지\n * 알게 되는 leakage 가 생겼을 것 — 의도적으로 builder 가 ctx.state.typeorm 같은 식으로\n * 접근하지 않고 things-factory 표준 getRepository 를 호출.\n */\nimport { getRepository } from '@things-factory/shell'\nimport {\n registerToolCategory,\n type ToolCategory,\n type ToolSpec\n} from '@things-factory/ai-client-base'\n\n// 모든 server-side 의존성 (entity / service 함수 / 외부 패키지) 은 module-level import\n// 없이 lazy require — module load 만으로 typeorm decorator / 외부 controllers chain\n// (예: board-service 의 puppeteer pool) 이 평가되어 jest 환경에서 깨지는 것을 회피.\n// 운영에서는 builder 첫 호출 시 cache 채워져 이후 호출은 직접 dispatch.\n//\n// 이 패턴은 LLM tool layer 의 일반 정책으로 가져갈 수 있다 — registerToolCategory 가\n// 평가되는 시점은 server bootstrap 의 매우 이른 단계라, 그때 typeorm DataSource init 이\n// 아직 끝나지 않았을 가능성이 있다.\nlet _depsCache: any | undefined\nfunction getDeps(): any {\n if (!_depsCache) {\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const isMod = require('./import-session/import-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const bsMod = require('@things-factory/board-service')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const actionsMod = require('./import-session/import-actions')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const materializeMod = require('./import-session/materialize-from-session')\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const authMod = require('@things-factory/auth-base')\n _depsCache = {\n checkPermission: authMod.checkPermission,\n ImportSession: isMod.ImportSession,\n Board: bsMod.Board,\n Group: bsMod.Group,\n startImportJobAsync: actionsMod.startImportJobAsync,\n fetchImportSession: actionsMod.fetchImportSession,\n describeAdapter: actionsMod.describeAdapter,\n materializeFromSession: materializeMod.materializeFromSession\n }\n }\n return _depsCache\n}\n\n/**\n * tool builder 의 ctx.state 가 도메인 식별을 갖는지 검증. 미설정이면 명확한 에러로 실패 —\n * board-ai 의 toolCallContext wiring 누락 / 비인증 호출 등을 빠르게 찾기 위해.\n */\nfunction requireDomainContext(state: any): { domain: any; user: any } {\n if (!state || !state.domain) {\n throw new Error(\n '[board-import] tool builder 의 ctx.state.domain 미설정. ' +\n 'board-ai 의 ChatOptions.toolCallContext 에 ResolverContext.state 가 전달됐는지 확인.'\n )\n }\n return { domain: state.domain, user: state.user }\n}\n\n/*\n * A tool is a second entrance to a door that already exists, so it asks for what that door asks for.\n *\n * `materializeImportSession` and `importBoardAsync` sit behind `board-import:mutation` as GraphQL\n * mutations, and `importSession` behind `board-import:query` (import-session-resolver.ts). The tools\n * called the same service functions with no check at all, so whoever could open a conversation —\n * `ai-assistant:mutation` — could create a Board through it.\n *\n * The check lives in the builder rather than in whichever chat door dispatches the tool: there is\n * more than one such door, and the effect has to be guarded wherever it is reached from.\n *\n * Whether the tool is **offered** at all is a separate question, and since 2026-09-18 the spec does\n * not answer it with a copy of this object (ADR-0071 decision 2). The spec names the GraphQL doors\n * it reaches and the gate resolves each one against that field's own `@privilege` — the same\n * declarations this constant mirrors (`import-session-resolver.ts`). This constant stays because it\n * guards the **effect**, which is reached from more places than the offering.\n */\nexport const IMPORT_TOOL_PRIVILEGE = {\n importBoardAsync: { category: 'board-import', privilege: 'mutation' },\n getImportSession: { category: 'board-import', privilege: 'query' },\n materializeImportSession: { category: 'board-import', privilege: 'mutation' }\n} as const\n\ntype GuardedImportTool = keyof typeof IMPORT_TOOL_PRIVILEGE\n\nasync function requireImportPrivilege(tool: GuardedImportTool, state: any): Promise<{ domain: any; user: any }> {\n const { domain, user } = requireDomainContext(state)\n const required = IMPORT_TOOL_PRIVILEGE[tool]\n const allowed = await getDeps().checkPermission(required, user, domain, state.unsafeIP, state.prohibitedPrivileges)\n if (!allowed) {\n throw new Error(\n `[board-import] '${tool}' needs ${required.category}:${required.privilege}. ` +\n 'Being allowed to talk to the assistant does not open it.'\n )\n }\n return { domain, user }\n}\n\nconst importToolsCategory: ToolCategory = {\n name: 'board-import',\n description: 'Drawing/CAD/IFC/image → Board 변환 파이프라인 (zero-to-twin).',\n /*\n * ⚠ 이 지침은 **아키텍트가 이 파일의 도구 설명만 보고 쓴 최소본**이다(2026-09-09).\n *\n * `registerToolCategory` 가 지침 없는 카테고리를 거절하게 되면서, 이 카테고리가 걸리는 유일한\n * 자리였다. 도구를 등록한 쪽이 규율을 소유하므로(§`ToolCategory.guidance`), **이 도구를 아는\n * 사람이 도메인 규율로 바꿔 주십시오.** 아래는 위 `description` 들에서 직접 읽어낸 것만 적었고\n * 도메인 지식을 지어내지 않았다.\n *\n * 두 줄이 특히 중요하다 — 이 카테고리에는 **즉시 돌아오지만 끝나지 않은** 도구와 **확인을 받아야\n * 하는** 도구가 있다. 그 둘이 지침이 필요한 전형이다.\n */\n guidance: [\n '- 어떤 형식을 변환할 수 있는지 물으면 추측하지 말고 `listImportAdapters` 를 호출한다. 등록된 어댑터만이 답이다.',\n '- `importBoardAsync` 는 **즉시 돌아오지만 변환은 끝나지 않았다.** 세션을 돌려줄 뿐이다 — `getImportSession` 으로 `completed` 를 확인하기 전에 \"변환했습니다\"라고 답하면 사용자는 없는 결과를 찾는다.',\n '- `getImportSession` 이 `failed` 면 그 `message` 를 그대로 옮긴다. 원인을 지어내지 않는다.',\n '- `materializeImportSession` 은 **사용자가 명시적으로 확인한 뒤에만** 호출한다. 미리보기를 보여 주고 저장 여부를 물은 다음이다. 만들어지는 Board 는 `draft` 이므로 발행은 사용자가 한다.',\n '- attachment id 를 지어내지 않는다. 미리 업로드된 것만 있고, 없으면 업로드가 먼저라고 말한다.'\n ].join('\\n'),\n specs: [\n {\n kind: 'read',\n name: 'listImportAdapters',\n /* 등록된 어댑터를 그 자리에서 셀 뿐이다 — 저장소에 닿지 않으므로 지나는 문이 없다. */\n doors: [],\n description:\n '등록된 도면 import 어댑터 (DXF / Image / 향후 IFC, glTF 등) 의 능력 조회. ' +\n '사용자가 어떤 형식 import 가 가능한지 묻거나, LLM 이 적절한 어댑터 선택을 inspect 할 때 사용.',\n schema: { type: 'object', properties: {} },\n builder: async () => {\n // 동적 import — pipeline 모듈의 DEFAULT_ADAPTERS 를 inspect.\n // eslint-disable-next-line @typescript-eslint/no-var-requires\n const pipelineModule = require('./pipeline/index')\n const adapters: any[] = pipelineModule.DEFAULT_ADAPTERS ?? []\n return adapters.map(getDeps().describeAdapter)\n }\n },\n {\n kind: 'external',\n name: 'importBoardAsync',\n doors: ['Mutation.importBoardAsync'],\n description:\n 'Attachment(도면/조감도/사진) 을 비동기로 보드 모델로 변환 시작. 즉시 ImportSession 을 ' +\n '반환하고 백그라운드에서 진행. status 가 completed 가 될 때까지 getImportSession 으로 ' +\n 'polling 해서 결과를 확인. 형식은 자동 감지 — DXF / GLTF / image 등.',\n schema: {\n type: 'object',\n properties: {\n attachmentId: {\n type: 'string',\n description: 'Attachment id (attachment-base 에 미리 업로드된 도면 파일).'\n },\n chatSessionId: {\n type: 'string',\n description: 'board-ai chat session id (선택). 진행 메시지가 chat 흐름에 합류.'\n },\n scopes: {\n type: 'array',\n items: { type: 'string' },\n description: 'ImportRule 의 scope (예: [\"fmsim\"]). 도메인별 매핑 규칙 활성화.'\n },\n parseOptions: {\n type: 'object',\n description: 'Adapter 파싱 옵션 (excludeLayers / maxEntities / context 등).',\n additionalProperties: true\n }\n },\n required: ['attachmentId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('importBoardAsync', ctx?.state)\n const session = await getDeps().startImportJobAsync(\n {\n attachmentId: args.attachmentId,\n chatSessionId: args.chatSessionId,\n scopes: args.scopes,\n parseOptions: args.parseOptions\n },\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n return {\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message\n }\n }\n },\n {\n kind: 'read',\n name: 'getImportSession',\n doors: ['Query.importSession'],\n description:\n 'ImportSession 의 진행/결과 조회. status 가 queued/parsing/mapping/assembling/binding 인 ' +\n '동안은 progress 와 message 를 반환, completed 면 result.boardModel 까지 포함. failed 면 ' +\n 'message 에 에러 사유.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string', description: 'ImportSession id (importBoardAsync 가 반환).' }\n },\n required: ['sessionId']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('getImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n if (!session) return { found: false }\n return {\n found: true,\n sessionId: session.id,\n status: session.status,\n progress: session.progress,\n message: session.message,\n // result 는 큰 boardModel 을 포함할 수 있어 status 가 completed 일 때만.\n result: session.status === 'completed' ? session.result : undefined,\n totalEntities: session.totalEntities,\n completedAt: session.completedAt\n }\n }\n },\n {\n kind: 'external',\n name: 'materializeImportSession',\n doors: ['Mutation.materializeImportSession'],\n description:\n '완료된 ImportSession 의 결과 boardModel 을 새 Board entity 로 영속화. status 가 ' +\n 'completed 인 세션만 처리. 결과 Board 는 state=\"draft\" 로 생성되므로 사용자가 검수 후 ' +\n '발행해야 한다.',\n themeNote:\n '⚠ 사용자의 명시적 confirm 후에만 호출. import 결과 미리보기를 보여주고 \"이 이름으로 ' +\n '저장하시겠어요?\" 같은 확인 받은 다음 발동.',\n schema: {\n type: 'object',\n properties: {\n sessionId: { type: 'string' },\n name: { type: 'string', description: '새 Board 이름 — 같은 도메인 내 unique.' },\n description: { type: 'string' },\n groupId: { type: 'string' },\n type: { type: 'string', enum: ['main', 'sub', 'popup'] },\n thumbnail: {\n type: 'string',\n description: 'Base64 thumbnail. 미지정 시 빈 placeholder 사용 (별도 thumbnail 생성 호출).'\n }\n },\n required: ['sessionId', 'name']\n },\n builder: async (args, ctx) => {\n const { domain, user } = await requireImportPrivilege('materializeImportSession', ctx?.state)\n const session = await getDeps().fetchImportSession(\n args.sessionId,\n { domain, user },\n getRepository(getDeps().ImportSession) as any\n )\n const { Board, Group, materializeFromSession } = getDeps()\n const board = await materializeFromSession(\n session as any,\n {\n sessionId: args.sessionId,\n name: args.name,\n description: args.description,\n groupId: args.groupId,\n type: args.type,\n thumbnail: args.thumbnail\n },\n { domain, user },\n {\n boardRepo: getRepository(Board) as any,\n groupRepo: getRepository(Group) as any\n }\n )\n // 큰 model 본문은 LLM 회신에서 제외 — id 와 메타만.\n return {\n boardId: (board as any).id,\n name: (board as any).name,\n state: (board as any).state,\n sourceImportSessionId: (board as any).sourceImportSessionId\n }\n }\n }\n ] as ToolSpec[]\n}\n\n// 등록 — module 로드 시 side-effect.\nregisterToolCategory(importToolsCategory)\n\n/**\n * 명시적 register 함수 — 테스트나 lazy bootstrap 에서 import 만으로 발동되는 side-effect 가\n * 부담스러울 때 import 후 명시 호출 가능.\n */\nexport function registerBoardImportTools(): void {\n registerToolCategory(importToolsCategory)\n}\n\n/**\n * 테스트용 — registerToolCategory 의 입력으로 사용된 카테고리 객체. 외부에서 inspect 가능.\n */\nexport const BOARD_IMPORT_TOOL_CATEGORY = importToolsCategory\n"]}
|