@things-factory/board-ai 10.1.27 → 10.1.29
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.
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/board-session-resolver.js +20 -2
- package/dist-server/service/board-session-resolver.js.map +1 -1
- package/dist-server/service/mention-marker.d.ts +1 -11
- package/dist-server/service/mention-marker.js +10 -41
- package/dist-server/service/mention-marker.js.map +1 -1
- package/dist-server/service/patch-entry/revert-permission.d.ts +5 -0
- package/dist-server/service/patch-entry/revert-permission.js +9 -0
- package/dist-server/service/patch-entry/revert-permission.js.map +1 -0
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -7
- package/server/service/board-session-resolver.ts +24 -2
- package/server/service/mention-marker.ts +7 -39
- package/server/service/patch-entry/revert-permission.ts +28 -0
- package/test/revert-permission.test.ts +41 -0
- package/test/fold-history-window.test.ts +0 -158
|
@@ -32,7 +32,10 @@ const ai_assistant_3 = require("@things-factory/ai-assistant");
|
|
|
32
32
|
const ai_assistant_4 = require("@things-factory/ai-assistant");
|
|
33
33
|
const ai_assistant_5 = require("@things-factory/ai-assistant");
|
|
34
34
|
const ai_assistant_6 = require("@things-factory/ai-assistant");
|
|
35
|
+
const ai_assistant_7 = require("@things-factory/ai-assistant");
|
|
36
|
+
const ai_assistant_8 = require("@things-factory/ai-assistant");
|
|
35
37
|
const patch_entry_js_1 = require("./patch-entry/patch-entry.js");
|
|
38
|
+
const revert_permission_js_1 = require("./patch-entry/revert-permission.js");
|
|
36
39
|
const board_patch_subscription_js_1 = require("./patch-entry/board-patch-subscription.js");
|
|
37
40
|
/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */
|
|
38
41
|
const sessions = new ai_assistant_6.ChatSessionResolver();
|
|
@@ -116,8 +119,23 @@ let BoardSessionResolver = class BoardSessionResolver {
|
|
|
116
119
|
return entry;
|
|
117
120
|
}
|
|
118
121
|
async revertPatch(patchId, context) {
|
|
119
|
-
const { tx } = context.state;
|
|
120
|
-
|
|
122
|
+
const { domain, user, tx } = context.state;
|
|
123
|
+
/* A patch id alone is not permission — see `patch-entry/revert-permission.ts`. */
|
|
124
|
+
const patch = await (0, shell_1.getRepository)(patch_entry_js_1.PatchEntry, tx).findOne({
|
|
125
|
+
where: { id: patchId, session: { domain: { id: domain.id } } },
|
|
126
|
+
relations: ['session']
|
|
127
|
+
});
|
|
128
|
+
const session = patch?.session;
|
|
129
|
+
const isParticipant = !!session && !!user?.id
|
|
130
|
+
? !!(await (0, shell_1.getRepository)(ai_assistant_7.ChatSessionParticipant, tx).findOneBy({
|
|
131
|
+
session: { id: session.id },
|
|
132
|
+
user: { id: user.id }
|
|
133
|
+
}))
|
|
134
|
+
: false;
|
|
135
|
+
if (!(0, revert_permission_js_1.mayRevertPatch)(patch ? { sessionVisibility: session?.visibility } : null, isParticipant, ai_assistant_8.mayReadSession)) {
|
|
136
|
+
throw new Error(`PatchEntry ${patchId} not found`);
|
|
137
|
+
}
|
|
138
|
+
await (0, shell_1.getRepository)(patch_entry_js_1.PatchEntry, tx).update(patch.id, { reverted: true });
|
|
121
139
|
return true;
|
|
122
140
|
}
|
|
123
141
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"board-session-resolver.js","sourceRoot":"","sources":["../../server/service/board-session-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 보드에 매인 대화 op — **보드 편집기의 것이라 여기 남는다.**\n *\n * ── 왜 갈랐나 (2026-09-10) ──────────────────────────────────────────────────\n * `chat-session-resolver.ts` 에 스물넷이 함께 있었다. 그 중 열여덟은 대화 자체의 일(세션·참가자·\n * 인박스·메시지)이라 보드가 없는 제품에도 뜻이 있고, 여섯은 보드의 일이다. 한 파일에 있는 동안은\n * 앞엣것을 중립 패키지로 옮길 수 없다 — 옮기면 `ai-assistant → board-ai` 의존이 생긴다.\n *\n * 그래서 **여섯을 이쪽으로 뗐다.** 남은 열여덟이 옮겨질 수 있는 상태가 되는 것이 이 갈이의 목적이다.\n *\n * ── 왜 여섯인가 ─────────────────────────────────────────────────────────────\n * chatSessionByBoard · chatSessionsByBoard 보드 id 로 찾는다 — 보드를 아는 질의\n * startBoardAISession 보드 앵커로 시작하는 얇은 래퍼\n * chatPatches · recordDirectPatch · revertPatch PatchEntry — 보드 편집 이력\n *\n * ── 위임을 `this` 로 하지 않는다 ────────────────────────────────────────────\n * 앞의 셋은 원래 `this.startAISession(...)` 처럼 같은 클래스의 중립 메서드를 불렀다. 이제 클래스가\n * 둘이므로 중립 리졸버 인스턴스에 위임한다. 리졸버는 상태가 없다(전부 `context.state` 에서 읽는다).\n *\n * **로직을 베끼지 않는다** — 같은 글이 두 곳에 있으면 한쪽만 고쳐진다.\n */\nimport { Resolver, Query, Mutation, Arg, Ctx, Directive, Int } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { activityPatch } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { ChatSessionResolver } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\n\n/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */\nconst sessions = new ChatSessionResolver()\n\n@Resolver()\nexport class BoardSessionResolver {\n @Query(() => ChatSession, { nullable: true, description: 'Get AI chat session by board id (returns first match — backward compat single-session lookup).' })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession | null> {\n const { domain } = context.state\n return (\n (await getRepository(ChatSession).findOneBy({ boardId, domain: { id: domain.id } })) ?? null\n )\n }\n\n /** 보드 전용 진입점 — 앵커 조회의 얇은 래퍼(기존 소비자 무변경). */\n @Query(() => [ChatSession], {\n description: 'List all AI chat sessions for a board (oldest first). Thin wrapper over chatSessionsByAnchor(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionsByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession[]> {\n return await sessions.chatSessionsByAnchor('board', boardId, context)\n }\n\n @Query(() => [PatchEntry], { description: 'List patch entries of a session, newest first.' })\n /*\n * ⚠ 이 셋은 **보드 편집기의 것**이라 `board-ai` 에 남는다 (2026-09-10).\n *\n * 나머지 스물둘을 `ai-assistant` 로 옮길 때 이것들도 함께 갔다가, 도는 스키마를 확인하면서\n * 되돌렸다 — `revertPatch` 가 `ai-assistant:mutation` 으로 떠 있었다. 즉 **대화 권한만 가진\n * 사람이 보드 패치를 되돌릴 수 있었다.** 좁히려던 변경이 넓히는 변경이 된 자리다.\n *\n * 가르는 물음: **이것이 보드를 바꾸나.** 패치는 바꾼다. 세션 조회는 안 바꾼다.\n */\n @Directive('@privilege(category: \"board-ai\", privilege: \"query\")')\n async chatPatches(\n @Arg('sessionId') sessionId: string,\n @Arg('limit', () => Int, { nullable: true, defaultValue: 100 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry[]> {\n const { domain } = context.state\n const session = await getRepository(ChatSession).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) return []\n return await getRepository(PatchEntry).find({\n where: { session: { id: sessionId } as any },\n order: { createdAt: 'DESC' },\n take: limit\n })\n }\n\n /** 보드 전용 진입점 — 앵커 시작의 얇은 래퍼(기존 소비자 무변경). */\n @Mutation(() => ChatSession, {\n description: 'Start (or get existing) AI chat session for a board. Thin wrapper over startAISession(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async startBoardAISession(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession> {\n return await sessions.startAISession('board', boardId, context)\n }\n\n @Directive('@transaction')\n @Mutation(() => PatchEntry, {\n description: 'Record a patch from user direct edit. Adds a system message so AI sees the change next turn.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async recordDirectPatch(\n @Arg('sessionId') sessionId: string,\n @Arg('ops', () => GraphQLJSON) ops: any[],\n @Arg('summary', { nullable: true }) summary: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry> {\n const { domain, tx, user } = context.state\n const session = await getRepository(ChatSession, tx).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n\n const entry = await getRepository(PatchEntry, tx).save({\n session: { id: session.id } as any,\n source: 'user-direct',\n ops: JSON.stringify(ops || []),\n summary,\n reverted: false\n } as any)\n\n // AI 컨텍스트 흐름 — system 메시지 자동 추가. creator = 직접 편집한 사용자.\n // parentMessage = 직전 in-session 메시지 (timeline 연속성 유지 — system 도 chain 에 합류).\n const previousLast = await getRepository(ChatMessage, tx).findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const sysMsg = await getRepository(ChatMessage, tx).save({\n session: { id: session.id } as any,\n role: 'system',\n content: `User directly edited the board: ${summary ?? `${(ops || []).length} op(s)`}`,\n relatedPatchId: entry.id,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n\n /* 세션 활동 갱신 — 직접 편집도 다른 참여자에게는 새 소식이다(목록 정렬·안 읽음 판정). */\n try {\n await getRepository(ChatSession, tx).update(session.id!, activityPatch(sysMsg.content) as any)\n } catch {\n /* best-effort — 편집 기록을 막지 않는다 */\n }\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: sysMsg.content,\n actorId: user?.id\n })\n\n // C1: user-direct 편집도 같은 board 구독자에게 방송 (AI·타참여자 실시간 반영). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: entry })\n // C4: 직접편집 system 메시지를 같은 세션 구독자에게 방송 (transcript 실시간 일관성). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: sysMsg })\n\n return entry\n }\n\n @Directive('@transaction')\n @Mutation(() => Boolean, { description: 'Mark a patch as reverted (does not undo, only flags).' })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async revertPatch(\n @Arg('patchId') patchId: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { tx } = context.state\n await getRepository(PatchEntry, tx).update(patchId, { reverted: true })\n return true\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"board-session-resolver.js","sourceRoot":"","sources":["../../server/service/board-session-resolver.ts"],"names":[],"mappings":"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","sourcesContent":["/*\n * 보드에 매인 대화 op — **보드 편집기의 것이라 여기 남는다.**\n *\n * ── 왜 갈랐나 (2026-09-10) ──────────────────────────────────────────────────\n * `chat-session-resolver.ts` 에 스물넷이 함께 있었다. 그 중 열여덟은 대화 자체의 일(세션·참가자·\n * 인박스·메시지)이라 보드가 없는 제품에도 뜻이 있고, 여섯은 보드의 일이다. 한 파일에 있는 동안은\n * 앞엣것을 중립 패키지로 옮길 수 없다 — 옮기면 `ai-assistant → board-ai` 의존이 생긴다.\n *\n * 그래서 **여섯을 이쪽으로 뗐다.** 남은 열여덟이 옮겨질 수 있는 상태가 되는 것이 이 갈이의 목적이다.\n *\n * ── 왜 여섯인가 ─────────────────────────────────────────────────────────────\n * chatSessionByBoard · chatSessionsByBoard 보드 id 로 찾는다 — 보드를 아는 질의\n * startBoardAISession 보드 앵커로 시작하는 얇은 래퍼\n * chatPatches · recordDirectPatch · revertPatch PatchEntry — 보드 편집 이력\n *\n * ── 위임을 `this` 로 하지 않는다 ────────────────────────────────────────────\n * 앞의 셋은 원래 `this.startAISession(...)` 처럼 같은 클래스의 중립 메서드를 불렀다. 이제 클래스가\n * 둘이므로 중립 리졸버 인스턴스에 위임한다. 리졸버는 상태가 없다(전부 `context.state` 에서 읽는다).\n *\n * **로직을 베끼지 않는다** — 같은 글이 두 곳에 있으면 한쪽만 고쳐진다.\n */\nimport { Resolver, Query, Mutation, Arg, Ctx, Directive, Int } from 'type-graphql'\nimport { GraphQLJSON } from 'graphql-scalars'\n\nimport { getRepository } from '@things-factory/shell'\n\nimport { ChatSession } from '@things-factory/ai-assistant'\nimport { activityPatch } from '@things-factory/ai-assistant'\nimport { publishSessionActivity } from '@things-factory/ai-assistant'\nimport { ChatMessage } from '@things-factory/ai-assistant'\nimport { publishChatMessage } from '@things-factory/ai-assistant'\nimport { ChatSessionResolver } from '@things-factory/ai-assistant'\nimport { ChatSessionParticipant } from '@things-factory/ai-assistant'\nimport { mayReadSession } from '@things-factory/ai-assistant'\nimport { PatchEntry } from './patch-entry/patch-entry.js'\nimport { mayRevertPatch } from './patch-entry/revert-permission.js'\nimport { publishBoardPatch } from './patch-entry/board-patch-subscription.js'\n\n/** 중립 op 로의 위임 대상. 리졸버는 상태가 없으므로 하나면 된다. */\nconst sessions = new ChatSessionResolver()\n\n@Resolver()\nexport class BoardSessionResolver {\n @Query(() => ChatSession, { nullable: true, description: 'Get AI chat session by board id (returns first match — backward compat single-session lookup).' })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession | null> {\n const { domain } = context.state\n return (\n (await getRepository(ChatSession).findOneBy({ boardId, domain: { id: domain.id } })) ?? null\n )\n }\n\n /** 보드 전용 진입점 — 앵커 조회의 얇은 래퍼(기존 소비자 무변경). */\n @Query(() => [ChatSession], {\n description: 'List all AI chat sessions for a board (oldest first). Thin wrapper over chatSessionsByAnchor(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"query\")')\n async chatSessionsByBoard(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession[]> {\n return await sessions.chatSessionsByAnchor('board', boardId, context)\n }\n\n @Query(() => [PatchEntry], { description: 'List patch entries of a session, newest first.' })\n /*\n * ⚠ 이 셋은 **보드 편집기의 것**이라 `board-ai` 에 남는다 (2026-09-10).\n *\n * 나머지 스물둘을 `ai-assistant` 로 옮길 때 이것들도 함께 갔다가, 도는 스키마를 확인하면서\n * 되돌렸다 — `revertPatch` 가 `ai-assistant:mutation` 으로 떠 있었다. 즉 **대화 권한만 가진\n * 사람이 보드 패치를 되돌릴 수 있었다.** 좁히려던 변경이 넓히는 변경이 된 자리다.\n *\n * 가르는 물음: **이것이 보드를 바꾸나.** 패치는 바꾼다. 세션 조회는 안 바꾼다.\n */\n @Directive('@privilege(category: \"board-ai\", privilege: \"query\")')\n async chatPatches(\n @Arg('sessionId') sessionId: string,\n @Arg('limit', () => Int, { nullable: true, defaultValue: 100 }) limit: number,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry[]> {\n const { domain } = context.state\n const session = await getRepository(ChatSession).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) return []\n return await getRepository(PatchEntry).find({\n where: { session: { id: sessionId } as any },\n order: { createdAt: 'DESC' },\n take: limit\n })\n }\n\n /** 보드 전용 진입점 — 앵커 시작의 얇은 래퍼(기존 소비자 무변경). */\n @Mutation(() => ChatSession, {\n description: 'Start (or get existing) AI chat session for a board. Thin wrapper over startAISession(board).'\n })\n @Directive('@privilege(category: \"ai-assistant\", privilege: \"mutation\")')\n async startBoardAISession(\n @Arg('boardId') boardId: string,\n @Ctx() context: ResolverContext\n ): Promise<ChatSession> {\n return await sessions.startAISession('board', boardId, context)\n }\n\n @Directive('@transaction')\n @Mutation(() => PatchEntry, {\n description: 'Record a patch from user direct edit. Adds a system message so AI sees the change next turn.'\n })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async recordDirectPatch(\n @Arg('sessionId') sessionId: string,\n @Arg('ops', () => GraphQLJSON) ops: any[],\n @Arg('summary', { nullable: true }) summary: string | undefined,\n @Ctx() context: ResolverContext\n ): Promise<PatchEntry> {\n const { domain, tx, user } = context.state\n const session = await getRepository(ChatSession, tx).findOneBy({\n id: sessionId,\n domain: { id: domain.id }\n })\n if (!session) throw new Error(`ChatSession ${sessionId} not found`)\n\n const entry = await getRepository(PatchEntry, tx).save({\n session: { id: session.id } as any,\n source: 'user-direct',\n ops: JSON.stringify(ops || []),\n summary,\n reverted: false\n } as any)\n\n // AI 컨텍스트 흐름 — system 메시지 자동 추가. creator = 직접 편집한 사용자.\n // parentMessage = 직전 in-session 메시지 (timeline 연속성 유지 — system 도 chain 에 합류).\n const previousLast = await getRepository(ChatMessage, tx).findOne({\n where: { session: { id: session.id } as any },\n order: { createdAt: 'DESC' }\n })\n const sysMsg = await getRepository(ChatMessage, tx).save({\n session: { id: session.id } as any,\n role: 'system',\n content: `User directly edited the board: ${summary ?? `${(ops || []).length} op(s)`}`,\n relatedPatchId: entry.id,\n creator: user,\n updater: user,\n parentMessage: previousLast ? ({ id: previousLast.id } as any) : undefined\n } as any)\n\n /* 세션 활동 갱신 — 직접 편집도 다른 참여자에게는 새 소식이다(목록 정렬·안 읽음 판정). */\n try {\n await getRepository(ChatSession, tx).update(session.id!, activityPatch(sysMsg.content) as any)\n } catch {\n /* best-effort — 편집 기록을 막지 않는다 */\n }\n publishSessionActivity({\n domainId: domain.id,\n sessionId: session.id!,\n kind: 'message',\n anchorType: (session as any).anchorType,\n anchorId: (session as any).anchorId,\n preview: sysMsg.content,\n actorId: user?.id\n })\n\n // C1: user-direct 편집도 같은 board 구독자에게 방송 (AI·타참여자 실시간 반영). best-effort.\n publishBoardPatch({ domainId: domain.id, boardId: session.boardId, patch: entry })\n // C4: 직접편집 system 메시지를 같은 세션 구독자에게 방송 (transcript 실시간 일관성). best-effort.\n publishChatMessage({ domainId: domain.id, sessionId: session.id, message: sysMsg })\n\n return entry\n }\n\n @Directive('@transaction')\n @Mutation(() => Boolean, { description: 'Mark a patch as reverted (does not undo, only flags).' })\n @Directive('@privilege(category: \"board-ai\", privilege: \"mutation\")')\n async revertPatch(\n @Arg('patchId') patchId: string,\n @Ctx() context: ResolverContext\n ): Promise<boolean> {\n const { domain, user, tx } = context.state\n\n /* A patch id alone is not permission — see `patch-entry/revert-permission.ts`. */\n const patch = await getRepository(PatchEntry, tx).findOne({\n where: { id: patchId, session: { domain: { id: domain.id } } } as any,\n relations: ['session']\n })\n const session: any = patch?.session\n const isParticipant =\n !!session && !!user?.id\n ? !!(await getRepository(ChatSessionParticipant, tx).findOneBy({\n session: { id: session.id },\n user: { id: user.id }\n } as any))\n : false\n\n if (!mayRevertPatch(patch ? { sessionVisibility: session?.visibility } : null, isParticipant, mayReadSession)) {\n throw new Error(`PatchEntry ${patchId} not found`)\n }\n\n await getRepository(PatchEntry, tx).update(patch!.id!, { reverted: true })\n return true\n }\n}\n"]}
|
|
@@ -24,17 +24,7 @@ import { type MentionPick } from './mention-resolver';
|
|
|
24
24
|
* 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.
|
|
25
25
|
*/
|
|
26
26
|
export declare const MENTION_MARKER_RE: RegExp;
|
|
27
|
-
|
|
28
|
-
* 멘션된 userId 중 *아직 참여자가 아닌* + *자기 자신이 아닌* 신규만 (C4 초대 — 차집합).
|
|
29
|
-
* 순수 — 누구를 새로 참여자 등록할지 판정. (resolver 가 repo 작업과 분리해 테스트 가능.)
|
|
30
|
-
*/
|
|
31
|
-
export declare function newParticipantUserIds(mentioned: ReadonlyArray<string>, existing: ReadonlyArray<string>, selfUserId?: string | null): string[];
|
|
32
|
-
/**
|
|
33
|
-
* 본문에서 `@user{userId:UUID}` 마커의 *userId 집합* 추출 (C4 — 초대 트리거).
|
|
34
|
-
* 같은 user 중복 제거, 순서 보존(첫 등장순). 마커 없으면 빈 배열.
|
|
35
|
-
* 순수 — content 만으로 "이 메시지가 누구를 멘션했나" 판정.
|
|
36
|
-
*/
|
|
37
|
-
export declare function extractMentionUserIds(text: string | null | undefined): string[];
|
|
27
|
+
export { extractMentionUserIds, newParticipantUserIds } from '@things-factory/ai-assistant';
|
|
38
28
|
/**
|
|
39
29
|
* 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.
|
|
40
30
|
* 토큰 자체 (#component, @user) 는 그대로 유지.
|
|
@@ -17,9 +17,7 @@
|
|
|
17
17
|
* 보다 우선) — 렌더링 깨졌을 때 로그 / DB 직접 보고 파악 가능
|
|
18
18
|
*/
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.MENTION_MARKER_RE = void 0;
|
|
21
|
-
exports.newParticipantUserIds = newParticipantUserIds;
|
|
22
|
-
exports.extractMentionUserIds = extractMentionUserIds;
|
|
20
|
+
exports.newParticipantUserIds = exports.extractMentionUserIds = exports.MENTION_MARKER_RE = void 0;
|
|
23
21
|
exports.stripMentionRefids = stripMentionRefids;
|
|
24
22
|
exports.injectInlineMarkers = injectInlineMarkers;
|
|
25
23
|
exports.injectMentionRefids = injectMentionRefids;
|
|
@@ -33,45 +31,16 @@ const mention_resolver_1 = require("./mention-resolver");
|
|
|
33
31
|
* 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.
|
|
34
32
|
*/
|
|
35
33
|
exports.MENTION_MARKER_RE = /\{[a-zA-Z][a-zA-Z0-9]*:[^}\s]+\}/g;
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
if (selfUserId)
|
|
43
|
-
have.add(selfUserId);
|
|
44
|
-
const out = [];
|
|
45
|
-
const seen = new Set();
|
|
46
|
-
for (const id of mentioned) {
|
|
47
|
-
if (!id || have.has(id) || seen.has(id))
|
|
48
|
-
continue;
|
|
49
|
-
seen.add(id);
|
|
50
|
-
out.push(id);
|
|
51
|
-
}
|
|
52
|
-
return out;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* 본문에서 `@user{userId:UUID}` 마커의 *userId 집합* 추출 (C4 — 초대 트리거).
|
|
56
|
-
* 같은 user 중복 제거, 순서 보존(첫 등장순). 마커 없으면 빈 배열.
|
|
57
|
-
* 순수 — content 만으로 "이 메시지가 누구를 멘션했나" 판정.
|
|
34
|
+
/*
|
|
35
|
+
* 이 둘은 **`@things-factory/ai-assistant` 로 옮겼다**(ADR-0071 결정 1).
|
|
36
|
+
*
|
|
37
|
+
* 멘션이 사람을 불러들이는 일은 보드만의 일이 아니다 — 트윈 협의에서 `@이름` 은 아무도 부르지 않고
|
|
38
|
+
* 있었다. 규칙이 이 제품 안에 있는 한 다른 문은 그것을 쓸 수 없다. 여기서는 이름만 다시 내보내
|
|
39
|
+
* 이 패키지의 부르는 자리가 그대로 돌게 한다.
|
|
58
40
|
*/
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const re = /\{userId:([^}\s]+)\}/g;
|
|
63
|
-
const seen = new Set();
|
|
64
|
-
const out = [];
|
|
65
|
-
let m;
|
|
66
|
-
while ((m = re.exec(text)) !== null) {
|
|
67
|
-
const id = m[1];
|
|
68
|
-
if (!seen.has(id)) {
|
|
69
|
-
seen.add(id);
|
|
70
|
-
out.push(id);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
return out;
|
|
74
|
-
}
|
|
41
|
+
var ai_assistant_1 = require("@things-factory/ai-assistant");
|
|
42
|
+
Object.defineProperty(exports, "extractMentionUserIds", { enumerable: true, get: function () { return ai_assistant_1.extractMentionUserIds; } });
|
|
43
|
+
Object.defineProperty(exports, "newParticipantUserIds", { enumerable: true, get: function () { return ai_assistant_1.newParticipantUserIds; } });
|
|
75
44
|
/**
|
|
76
45
|
* 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.
|
|
77
46
|
* 토큰 자체 (#component, @user) 는 그대로 유지.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mention-marker.js","sourceRoot":"","sources":["../../server/service/mention-marker.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;GAgBG;;;
|
|
1
|
+
{"version":3,"file":"mention-marker.js","sourceRoot":"","sources":["../../server/service/mention-marker.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `#token{refid:N}` inline marker — 채팅 메시지의 단일 source-of-truth.\n *\n * 설계 원칙:\n * - 메시지 영속은 `ChatMessage.content` 한 컬럼에 통합. token-refid 매핑은 본문 안에\n * `#motor1{refid:35}` 형태로 inline 마킹.\n * - 별도 컬럼이 없으니 두 데이터 사이 consistency 깨질 일 자체가 없음.\n * - LLM 한테는 `stripMentionRefids` 로 마커 제거 후 보냄 — 토큰 낭비 / 주의 분산 차단.\n * - 클라이언트 렌더는 `#token{refid:N}` 패턴을 추출해 `<span data-refid=\"N\">#token</span>`\n * 으로 변환 → 클릭 시 deterministic 한 컴포넌트 점프.\n *\n * 마커 형식:\n * - `#token{refid:N}` (N 은 정수)\n * - token 과 `{` 사이 공백 없음\n * - `{refid:N}` 자체는 사람도 디버그 시 읽기 쉬움 (zero-width 문자나 prefix 약어\n * 보다 우선) — 렌더링 깨졌을 때 로그 / DB 직접 보고 파악 가능\n */\n\nimport type { BoardModel } from '@things-factory/board-import'\nimport { findMentions, type MentionPick } from './mention-resolver'\n\n/**\n * 인라인 마커 정규식 — `{key:value}` 형태. 현재 사용:\n * - `{refid:N}` — `#component` 멘션 (things-scene refid)\n * - `{userId:UUID}` — `@user` 멘션 (도메인 사용자)\n * 향후 새 종류 (agentId / threadId / 외부 시스템 id 등) 도 동일 형식으로 추가 가능.\n */\nexport const MENTION_MARKER_RE = /\\{[a-zA-Z][a-zA-Z0-9]*:[^}\\s]+\\}/g\n\n/*\n * 이 둘은 **`@things-factory/ai-assistant` 로 옮겼다**(ADR-0071 결정 1).\n *\n * 멘션이 사람을 불러들이는 일은 보드만의 일이 아니다 — 트윈 협의에서 `@이름` 은 아무도 부르지 않고\n * 있었다. 규칙이 이 제품 안에 있는 한 다른 문은 그것을 쓸 수 없다. 여기서는 이름만 다시 내보내\n * 이 패키지의 부르는 자리가 그대로 돌게 한다.\n */\nexport { extractMentionUserIds, newParticipantUserIds } from '@things-factory/ai-assistant'\n\n\n/**\n * 본문에서 모든 `{key:value}` 인라인 마커 제거. LLM 입력 / textarea 편집 / 평문 표출 등.\n * 토큰 자체 (#component, @user) 는 그대로 유지.\n */\nexport function stripMentionRefids(text: string | null | undefined): string {\n if (!text) return ''\n return text.replace(MENTION_MARKER_RE, '')\n}\n\n/** 토큰 매칭 정규식 빌더 — trigger 별로 build. */\nfunction buildTokenRegex(trigger: string, markerKey: string): RegExp {\n const word =\n '\\\\w\\\\p{Script=Hangul}\\\\p{Script=Hiragana}\\\\p{Script=Katakana}\\\\p{Script=Han}'\n // (앞경계)(트리거)(토큰)(이미 같은 markerKey 의 마커가 붙어있는지 — skip 판정용)\n return new RegExp(\n `(^|[^${word}])(${escapeRegex(trigger)})([${word}-]+)(\\\\{${markerKey}:[^}\\\\s]+\\\\})?`,\n 'gu'\n )\n}\n\nfunction escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')\n}\n\n/**\n * 일반화된 inline marker 주입 — trigger 와 markerKey 별로 picks 를 본문에 박음.\n *\n * injectInlineMarkers(text, '#', 'refid', [{ token, value: 35 }])\n * injectInlineMarkers(text, '@', 'userId', [{ token, value: 'uuid' }])\n *\n * 룰:\n * - 정확 token 일치 (popup 명시 선택된 것만 — 자유 입력 토큰은 안 건드림)\n * - 동일 token 여러 번 등장하면 모두 marker 적용\n * - 이미 같은 markerKey 의 marker 가 붙어있으면 그대로 유지 (덮어쓰기 X)\n */\nexport function injectInlineMarkers(\n text: string,\n trigger: string,\n markerKey: string,\n picks: ReadonlyArray<{ token: string; value: string | number }> | null | undefined\n): string {\n if (!text || !picks || picks.length === 0) return text ?? ''\n const map = new Map<string, string | number>()\n for (const p of picks) map.set(p.token, p.value)\n const re = buildTokenRegex(trigger, markerKey)\n return text.replace(re, (match, leading, trig, token, existingMarker) => {\n if (existingMarker) return match\n const value = map.get(token)\n if (value === undefined) return match\n return `${leading}${trig}${token}{${markerKey}:${value}}`\n })\n}\n\n/**\n * `#component` 멘션 picks 를 `{refid:N}` 마커로 주입 (호환 유지).\n */\nexport function injectMentionRefids(\n text: string,\n picks: ReadonlyArray<MentionPick> | null | undefined\n): string {\n return injectInlineMarkers(\n text,\n '#',\n 'refid',\n picks?.map(p => ({ token: p.token, value: p.refid }))\n )\n}\n\n/**\n * `@user` 멘션 picks 를 `{userId:UUID}` 마커로 주입.\n * picks: [{token: 사용자_표시명, value: 사용자_id}]\n */\nexport function injectMentionUserIds(\n text: string,\n picks: ReadonlyArray<{ token: string; userId: string }> | null | undefined\n): string {\n return injectInlineMarkers(\n text,\n '@',\n 'userId',\n picks?.map(p => ({ token: p.token, value: p.userId }))\n )\n}\n\n/**\n * AI 응답 텍스트의 `#token` 들을 currentBoard 에 대해 resolve 해 inline marker 로 주입.\n *\n * - 매칭 룰: `findMentions` (= name/id/type/tag/class/text OR). 다중 일치 시 첫 결과.\n * - 매칭 안 되면 marker 없이 통과 (예: AI 가 patch 로 새로 만든 컴포넌트는 input.currentBoard\n * 에 없으므로 marker 못 박음 — 클라이언트 클릭 시 runtime fallback 으로 해석 가능).\n * - 이미 marker 있으면 그대로.\n */\nexport function injectAiMentionRefids(\n text: string,\n currentBoard: BoardModel | null | undefined\n): string {\n if (!text) return ''\n const matches = findMentions(text, currentBoard ?? null)\n if (matches.length === 0) return text\n\n // token → 첫 매칭 refid 맵\n const tokenToRefid = new Map<string, number>()\n for (const m of matches) {\n if (tokenToRefid.has(m.token)) continue\n if (m.matched.length === 0) continue\n tokenToRefid.set(m.token, m.matched[0].refid)\n }\n if (tokenToRefid.size === 0) return text\n\n // 사용자 picks 와 동일한 injection 룰로 주입.\n const picks = Array.from(tokenToRefid.entries()).map(([token, refid]) => ({ token, refid }))\n return injectMentionRefids(text, picks)\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export interface FoundPatch {
|
|
2
|
+
/** `ChatSession.visibility` of the session the patch belongs to. */
|
|
3
|
+
sessionVisibility?: string | null;
|
|
4
|
+
}
|
|
5
|
+
export declare function mayRevertPatch(found: FoundPatch | null | undefined, isParticipant: boolean, mayReadSession: (visibility: string | null | undefined, isParticipant: boolean) => boolean): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mayRevertPatch = mayRevertPatch;
|
|
4
|
+
function mayRevertPatch(found, isParticipant, mayReadSession) {
|
|
5
|
+
if (!found)
|
|
6
|
+
return false;
|
|
7
|
+
return mayReadSession(found.sessionVisibility, isParticipant);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=revert-permission.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"revert-permission.js","sourceRoot":"","sources":["../../../server/service/patch-entry/revert-permission.ts"],"names":[],"mappings":";;AAoBA,wCAOC;AAPD,SAAgB,cAAc,CAC5B,KAAoC,EACpC,aAAsB,EACtB,cAA0F;IAE1F,IAAI,CAAC,KAAK;QAAE,OAAO,KAAK,CAAA;IACxB,OAAO,cAAc,CAAC,KAAK,CAAC,iBAAiB,EAAE,aAAa,CAAC,CAAA;AAC/D,CAAC","sourcesContent":["/*\n * Who may flag a patch as reverted.\n *\n * `revertPatch` used to take a patch id and write — the privilege gate was the only check, so anyone\n * holding `board-ai:mutation` in ANY tenant could flag a patch in another tenant's conversation by\n * knowing (or guessing) its id, and a person could reach into a private conversation they are not\n * part of (operato-twin ADR-0061 \"what was left\", found again by the 2026-09-18 ADR audit).\n *\n * The rule is the conversation's own: a patch belongs to a session, and a person may act on it only\n * where they may read that session — the caller's tenant, and for a private session, a participant\n * (ADR-0073 decision 2). The lookup is scoped by tenant, so a patch elsewhere simply is not found.\n *\n * One answer for every refusal. Telling \"exists but not yours\" apart from \"does not exist\" would\n * confirm an id to someone who should not learn it.\n */\nexport interface FoundPatch {\n /** `ChatSession.visibility` of the session the patch belongs to. */\n sessionVisibility?: string | null\n}\n\nexport function mayRevertPatch(\n found: FoundPatch | null | undefined,\n isParticipant: boolean,\n mayReadSession: (visibility: string | null | undefined, isParticipant: boolean) => boolean\n): boolean {\n if (!found) return false\n return mayReadSession(found.sessionVisibility, isParticipant)\n}\n"]}
|