@things-factory/ai-assistant 10.1.28 โ†’ 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.
Files changed (110) hide show
  1. package/client/components/assistant-chat.ts +51 -7
  2. package/client/components/assistant-mode.test.ts +20 -1
  3. package/client/components/assistant-mode.ts +20 -0
  4. package/client/components/panel-aria.test.ts +20 -0
  5. package/client/utils/assistant-session-controller.ts +14 -1
  6. package/client/utils/assistant-session-transport.ts +2 -2
  7. package/dist-client/components/assistant-chat.d.ts +12 -0
  8. package/dist-client/components/assistant-chat.js +48 -6
  9. package/dist-client/components/assistant-chat.js.map +1 -1
  10. package/dist-client/components/assistant-mode.d.ts +22 -0
  11. package/dist-client/components/assistant-mode.js +16 -0
  12. package/dist-client/components/assistant-mode.js.map +1 -1
  13. package/dist-client/components/assistant-mode.test.js +17 -1
  14. package/dist-client/components/assistant-mode.test.js.map +1 -1
  15. package/dist-client/components/panel-aria.test.js +18 -0
  16. package/dist-client/components/panel-aria.test.js.map +1 -1
  17. package/dist-client/tsconfig.tsbuildinfo +1 -1
  18. package/dist-client/utils/assistant-session-controller.d.ts +10 -0
  19. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  20. package/dist-client/utils/assistant-session-transport.js +2 -2
  21. package/dist-client/utils/assistant-session-transport.js.map +1 -1
  22. package/dist-server/door-declaration-report.d.ts +14 -0
  23. package/dist-server/door-declaration-report.js +54 -0
  24. package/dist-server/door-declaration-report.js.map +1 -0
  25. package/dist-server/service/assistant-chat-resolver.js +19 -0
  26. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  27. package/dist-server/service/assistant-mode/assistant-mode-resolver.d.ts +8 -0
  28. package/dist-server/service/assistant-mode/assistant-mode-resolver.js +8 -0
  29. package/dist-server/service/assistant-mode/assistant-mode-resolver.js.map +1 -1
  30. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  31. package/dist-server/service/assistant-mode/index.js +1 -1
  32. package/dist-server/service/assistant-mode/index.js.map +1 -1
  33. package/dist-server/service/assistant-mode/resolve.d.ts +10 -0
  34. package/dist-server/service/assistant-mode/resolve.js +5 -0
  35. package/dist-server/service/assistant-mode/resolve.js.map +1 -1
  36. package/dist-server/service/assistant-mode/types.d.ts +21 -0
  37. package/dist-server/service/assistant-mode/types.js.map +1 -1
  38. package/dist-server/service/chat-message/carried-turns.js +14 -2
  39. package/dist-server/service/chat-message/carried-turns.js.map +1 -1
  40. package/dist-server/service/chat-message/door-access.d.ts +19 -7
  41. package/dist-server/service/chat-message/door-access.js +19 -11
  42. package/dist-server/service/chat-message/door-access.js.map +1 -1
  43. package/dist-server/service/chat-message/reader-gate.js +6 -4
  44. package/dist-server/service/chat-message/reader-gate.js.map +1 -1
  45. package/dist-server/service/chat-session/chat-session.d.ts +12 -0
  46. package/dist-server/service/chat-session/chat-session.js +12 -11
  47. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  48. package/dist-server/service/chat-session/session-anchor.d.ts +10 -1
  49. package/dist-server/service/chat-session/session-anchor.js +12 -4
  50. package/dist-server/service/chat-session/session-anchor.js.map +1 -1
  51. package/dist-server/service/chat-session/session-inbox.d.ts +2 -0
  52. package/dist-server/service/chat-session/session-inbox.js.map +1 -1
  53. package/dist-server/service/chat-session/session-kind.d.ts +14 -0
  54. package/dist-server/service/chat-session/session-kind.js +30 -0
  55. package/dist-server/service/chat-session/session-kind.js.map +1 -0
  56. package/dist-server/service/chat-session/session-visibility.js +8 -1
  57. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  58. package/dist-server/service/chat-session-resolver.d.ts +2 -0
  59. package/dist-server/service/chat-session-resolver.js +4 -0
  60. package/dist-server/service/chat-session-resolver.js.map +1 -1
  61. package/dist-server/service/index.d.ts +1 -0
  62. package/dist-server/service/index.js +2 -0
  63. package/dist-server/service/index.js.map +1 -1
  64. package/dist-server/service/testing/conversation-script.d.ts +7 -0
  65. package/dist-server/service/testing/conversation-script.js +1 -0
  66. package/dist-server/service/testing/conversation-script.js.map +1 -1
  67. package/dist-server/service/testing/mode-conformance.d.ts +10 -0
  68. package/dist-server/service/testing/mode-conformance.js +13 -0
  69. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  70. package/dist-server/service/tool-access.js +20 -7
  71. package/dist-server/service/tool-access.js.map +1 -1
  72. package/dist-server/tool-registry-boot-report.js +15 -9
  73. package/dist-server/tool-registry-boot-report.js.map +1 -1
  74. package/dist-server/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +5 -5
  76. package/server/door-declaration-report.test.ts +53 -0
  77. package/server/door-declaration-report.ts +61 -0
  78. package/server/service/assistant-chat-resolver.ts +21 -0
  79. package/server/service/assistant-mode/assistant-mode-resolver.ts +6 -0
  80. package/server/service/assistant-mode/index.ts +1 -1
  81. package/server/service/assistant-mode/resolve.test.ts +42 -0
  82. package/server/service/assistant-mode/resolve.ts +9 -0
  83. package/server/service/assistant-mode/types.ts +18 -0
  84. package/server/service/chat-message/carried-turns.test.ts +13 -2
  85. package/server/service/chat-message/carried-turns.ts +16 -3
  86. package/server/service/chat-message/door-access.test.ts +72 -0
  87. package/server/service/chat-message/door-access.ts +43 -23
  88. package/server/service/chat-message/fold-history-window.test.ts +184 -0
  89. package/server/service/chat-message/reader-gate.ts +6 -4
  90. package/server/service/chat-session/chat-session.ts +15 -0
  91. package/server/service/chat-session/session-anchor.ts +12 -3
  92. package/server/service/chat-session/session-inbox.ts +2 -0
  93. package/server/service/chat-session/session-kind.test.ts +24 -0
  94. package/server/service/chat-session/session-kind.ts +32 -0
  95. package/server/service/chat-session/session-visibility.test.ts +16 -0
  96. package/server/service/chat-session/session-visibility.ts +9 -1
  97. package/server/service/chat-session-resolver.ts +4 -0
  98. package/server/service/index.ts +2 -0
  99. package/server/service/testing/conversation-scenario.test.ts +2 -1
  100. package/server/service/testing/conversation-script.test.ts +4 -2
  101. package/server/service/testing/conversation-script.ts +8 -0
  102. package/server/service/testing/mode-conformance.ts +15 -0
  103. package/server/service/tool-access.test.ts +67 -19
  104. package/server/service/tool-access.ts +24 -15
  105. package/server/tool-registry-boot-report.ts +14 -9
  106. package/translations/en.json +3 -2
  107. package/translations/ja.json +3 -2
  108. package/translations/ko.json +3 -2
  109. package/translations/ms.json +3 -2
  110. package/translations/zh.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":";;AA6CA,kDAcC;AAmBD,8CAiBC;AA/FD;;;;;GAKG;AACH,yCAA6C;AAY7C;;;;GAIG;AACH,SAAS,gBAAgB,CAAC,IAAY,EAAE,CAAY;IAClD,MAAM,KAAK,GAAQ,IAAI,KAAK,CAC1B,CAAC,CAAC,gCAAgC,EAAE;QAClC,YAAY,EAAE,2FAA2F;QACzG,IAAI,EAAE,IAAI,IAAI,GAAG;KAClB,CAAC,CACH,CAAA;IACD,KAAK,CAAC,IAAI,GAAG,wBAAwB,CAAA;IACrC,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,EAAE,CAAA;IACvB,OAAO,KAAK,CAAA;AACd,CAAC;AAWD,kEAAkE;AAClE,SAAgB,mBAAmB,CAAC,GAAqB,EAAE,CAAY;IACrE,MAAM,IAAI,GAAG,MAAM,CAAC,GAAG,EAAE,IAAI,IAAI,EAAE,CAAC,CAAA;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,CAAC,IAAA,2BAAgB,EAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAEtD,IAAI,CAAC,IAAI;QAAE,MAAM,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAE1C,OAAO;QACL,YAAY,EAAE,IAAI,CAAC,YAAY;QAC/B,cAAc,EAAE,IAAI,CAAC,cAAc;QACnC,sBAAsB,EAAE,IAAI,CAAC,sBAAsB;QACnD,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,WAAW,EAAE,IAAI,CAAC,WAAW;QAC7B,SAAS,EAAE,IAAI,CAAC,SAAS;KAC1B,CAAA;AACH,CAAC;AAkBD,mEAAmE;AACnE,SAAgB,iBAAiB,CAAC,IAAY,EAAE,CAAY;IAC1D,MAAM,IAAI,GAAG,IAAA,2BAAgB,EAAC,IAAI,CAAC,CAAA;IACnC,IAAI,CAAC,IAAI;QAAE,MAAM,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAE1C,OAAO;QACL,IAAI,EAAE,IAAI,CAAC,IAAI;QACf,cAAc,EAAE,CAAC,IAAI,CAAC,cAAc,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAyB,EAAE,EAAE,CAAC,CAAC;YAC9E,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,WAAW,EAAE,CAAC,CAAC,KAAK,CAAC,cAAc,CAAC;YACpC,QAAQ,EAAE,CAAC,CAAC,KAAK,CAAC,WAAW,CAAC;SAC/B,CAAC,CAAC;QACH,cAAc,EAAE,IAAI,CAAC,cAAc,IAAI,EAAE;QACzC,eAAe,EAAE,IAAI,CAAC,eAAe,IAAI,EAAE;QAC3C,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,SAAS;QACrE,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS;QACxE,YAAY,EAAE,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC,SAAS;KACzE,CAAA;AACH,CAAC","sourcesContent":["/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is โ€” a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log โ€” a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || 'โ€”'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n toolCategories: mode.toolCategories,\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined\n }\n}\n"]}
1
+ {"version":3,"file":"resolve.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/resolve.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What the door and the screen each take from a mode.\n *\n * Both start from the same declaration, so a screen cannot show `/` sentences for a conversation\n * the server would answer differently.\n */\nimport { getAssistantMode } from './registry'\nimport type { AssistantModeSlash } from './types'\n\nexport type Translate = (key: string, params?: Record<string, any>) => string\n\nexport interface AssistantModeRef {\n /** Which conversation this is โ€” a registered `<app>.<surface>`. */\n kind: string\n /** What it is about. Part of the conversation's lifetime; the tools read it, not the settings. */\n target?: string\n}\n\n/**\n * A kind nobody registered. The sentence is the person's (ADR-0054); the code and the kind are for\n * whoever reads the log โ€” a screen naming a conversation the server does not have is a mismatched\n * deployment, not something the person did.\n */\nfunction unknownModeError(kind: string, t: Translate): Error {\n const error: any = new Error(\n t('ai-assistant.text.unknown-mode', {\n defaultValue: 'this screen asked for a conversation this server does not have ({kind}). reload the page.',\n kind: kind || 'โ€”'\n })\n )\n error.code = 'unknown-assistant-mode'\n error.params = { kind }\n return error\n}\n\nexport interface ModeSettings {\n systemPrompt: string\n toolCategories?: string[]\n requireToolOnFirstTurn?: boolean\n model?: string\n temperature?: number\n maxTokens?: number\n}\n\n/** The conversation's settings, from the server's declaration. */\nexport function resolveModeSettings(ref: AssistantModeRef, t: Translate): ModeSettings {\n const kind = String(ref?.kind ?? '')\n const mode = kind ? getAssistantMode(kind) : undefined\n\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n systemPrompt: mode.systemPrompt,\n toolCategories: mode.toolCategories,\n requireToolOnFirstTurn: mode.requireToolOnFirstTurn,\n model: mode.model,\n temperature: mode.temperature,\n maxTokens: mode.maxTokens\n }\n}\n\nexport interface AssistantModeSlashView {\n name: string\n description: string\n template: string\n}\n\nexport interface AssistantModeView {\n kind: string\n slashTemplates: AssistantModeSlashView[]\n catalogEntries: any[]\n proposalChoices: any[]\n placeholder?: string\n /** ๋นˆ ๋Œ€ํ™”์˜ ์ œ๋ชฉ๊ณผ ํ•œ ์ค„ โ€” ์ด ๋Œ€ํ™”๊ฐ€ ๋ฌด์—‡์„ ํ•˜๋Š” ์ž๋ฆฌ์ธ์ง€. */\n intro?: { title: string; subtitle: string }\n /** ๋นˆ ๋Œ€ํ™”์˜ ์˜ˆ์‹œ โ€” ์ด ๋Œ€ํ™”์—์„œ ์‹ค์ œ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ง. */\n examples?: Array<{ label: string; items: string[] }>\n footerNotice?: string\n stagedNotice?: string\n}\n\n/** What the screen shows, in the words of the request's locale. */\nexport function assistantModeView(kind: string, t: Translate): AssistantModeView {\n const mode = getAssistantMode(kind)\n if (!mode) throw unknownModeError(kind, t)\n\n return {\n kind: mode.kind,\n slashTemplates: (mode.slashTemplates ?? []).map((slash: AssistantModeSlash) => ({\n name: slash.name,\n description: t(slash.descriptionKey),\n template: t(slash.templateKey)\n })),\n catalogEntries: mode.catalogEntries ?? [],\n proposalChoices: mode.proposalChoices ?? [],\n placeholder: mode.placeholderKey ? t(mode.placeholderKey) : undefined,\n intro: mode.introKeys ? { title: t(mode.introKeys.title), subtitle: t(mode.introKeys.subtitle) } : undefined,\n examples: mode.exampleKeys?.map(group => ({\n label: t(group.labelKey),\n items: group.itemKeys.map(key => t(key))\n })),\n footerNotice: mode.footerNoticeKey ? t(mode.footerNoticeKey) : undefined,\n stagedNotice: mode.stagedNoticeKey ? t(mode.stagedNoticeKey) : undefined\n }\n}\n"]}
@@ -71,6 +71,27 @@ export interface AssistantModeSpec {
71
71
  * "describe the figure you want" while the person was there to change the figure already open.
72
72
  */
73
73
  placeholderKey?: string;
74
+ /**
75
+ * ๋นˆ ๋Œ€ํ™”๊ฐ€ ๋ฌด์—‡์„ ๋งํ•˜๋Š”๊ฐ€ โ€” ์ œ๋ชฉ๊ณผ ํ•œ ์ค„ (ADR-0061 ๊ฒฐ์ • 7).
76
+ *
77
+ * ์ž๋ฆฌํ‘œ์™€ `/` ๋Š” ๋Œ€ํ™”๋ฅผ ๋”ฐ๋ผ๊ฐ€๋Š”๋ฐ ์ด ๋ธ”๋ก๋งŒ host ์†์„ฑ์— ๋‚จ์•„ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋จธ๋ฆฌ์—๋Š” ใ€Œ์ด ๋„ํ˜•
78
+ * ๊ณ ์น˜๊ธฐใ€๊ฐ€ ์„œ ์žˆ๊ณ  ๋ณธ๋ฌธ์€ ใ€Œ๋งŒ๋“ค Figure ๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”ใ€๋ผ๊ณ  ์ ํ˜€ ์žˆ์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ
79
+ * :3300). ํ•œ ๋Œ€ํ™”๊ฐ€ ํ™”๋ฉด์— ๋ฌด์—‡์„ ๋งํ•˜๋Š”๊ฐ€๋Š” ํ•œ ์ž๋ฆฌ์—์„œ ๋‚˜์™€์•ผ ํ•œ๋‹ค.
80
+ */
81
+ introKeys?: {
82
+ title: string;
83
+ subtitle: string;
84
+ };
85
+ /**
86
+ * ๋นˆ ๋Œ€ํ™”์˜ ์˜ˆ์‹œ โ€” ใ€Œ์ด ๋Œ€ํ™”์—์„œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋งใ€.
87
+ *
88
+ * ์˜ˆ์‹œ๋Š” ๊ทธ ๋„๊ตฌ๋กœ **์‹ค์ œ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ง**์ด์–ด์•ผ ํ•œ๋‹ค. ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์„ ์˜ˆ์‹œ๋กœ ๋‘๋ฉด ๋นˆ ํ™”๋ฉด์ด ๊ฑฐ์ง“์„
89
+ * ๊ฐ€๋ฅด์นœ๋‹ค. ๋Œ€ํ™”๋งˆ๋‹ค ์—ด๋ฆฌ๋Š” ๋„๊ตฌ๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ์˜ˆ์‹œ๋„ ๋Œ€ํ™”๋ฅผ ๋”ฐ๋ผ๊ฐ„๋‹ค.
90
+ */
91
+ exampleKeys?: Array<{
92
+ labelKey: string;
93
+ itemKeys: string[];
94
+ }>;
74
95
  /** i18n key for the line under an empty conversation. */
75
96
  footerNoticeKey?: string;
76
97
  /** i18n key for the line shown once the host reports a proposal staged. */
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` โ€” `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for โ€” `['board', 'space']` (ADR-0072 A17 โ‘ ).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen โ€” one per subject type\n * (ADR-0072 A17 โ‘ก).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them โ€”\n * \"์ด ๊ทธ๋ฆผ ๊ณ ์น˜๊ธฐ\", \"์ด ํ˜„์žฅ ์ง€๊ธˆ ์–ด๋•Œ\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers โ€” for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` โ€” a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` โ€” a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` โ€” two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` โ€” a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[] }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../../server/service/assistant-mode/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;GAQG","sourcesContent":["/**\n * What a conversation is, declared by the app server that owns the screen (ADR-0061 decision 7).\n *\n * ADR-0058 put these five in one value so they could not fall out of step, but the value lived in\n * the browser and travelled on every request: the persona, the tool categories, the model and the\n * limits were the caller's to choose. A caller choosing its own tool categories is a caller\n * choosing its own authority, so the declaration moved here and the client sends only which one it\n * is talking in (`kind`) and what about (`target`).\n */\n\n/** A `/` sentence this mode offers, and what it needs to be answerable. */\nexport interface AssistantModeSlash {\n /** Short name shown in the list. */\n name: string\n /** i18n key for the one-line description. The server translates it into the request's locale. */\n descriptionKey: string\n /** i18n key for the sentence that lands in the input. */\n templateKey: string\n /**\n * The tools this sentence needs. A mode must not offer work it cannot do (ADR-0058 decision 3),\n * and a name outside `toolCategories` is reported at boot rather than when a person presses it.\n */\n tools?: string[]\n}\n\nexport interface AssistantModeSpec {\n /** `<app>.<surface>` โ€” `twin.space`, `plant.station`, `figure.modeller`. */\n kind: string\n\n /**\n * Which kinds of subject this conversation stands for โ€” `['board', 'space']` (ADR-0072 A17 โ‘ ).\n *\n * A screen says what it is showing (`context.subject`), and these say which conversations that\n * subject can carry. It is a list of types and not a predicate on purpose: a list can be counted\n * at boot and pinned by a test, while a function can only be run.\n *\n * On the twin modeller nothing could be asked about operations, because an open board pinned the\n * authoring conversation and that conversation opens one category. A conversation that also\n * stands for `board` is how the same screen carries both.\n */\n subjects?: string[]\n\n /**\n * The conversation a screen opens when the person has not chosen โ€” one per subject type\n * (ADR-0072 A17 โ‘ก).\n *\n * Registration order must not decide this: whichever module booted last would silently become\n * the default. Exactly one mode declares each subject type, and the boot report says so when a\n * type has none or more than one.\n */\n defaultFor?: string[]\n\n /**\n * i18n key for what this conversation is called where a person chooses between them โ€”\n * \"์ด ๊ทธ๋ฆผ ๊ณ ์น˜๊ธฐ\", \"์ด ํ˜„์žฅ ์ง€๊ธˆ ์–ด๋•Œ\". A mode a screen may offer needs one.\n */\n labelKey?: string\n /** Who the assistant is here, and what it must not claim to do. The server's, not the caller's. */\n systemPrompt: string\n /** Which tool categories this conversation may use. Absent or empty exposes none. */\n toolCategories?: string[]\n /** Make the model read before it answers โ€” for surfaces whose subject is live state. */\n requireToolOnFirstTurn?: boolean\n /** Model and limits are policy, not a request field. */\n model?: string\n temperature?: number\n maxTokens?: number\n /** What `/` offers. Declared as keys; the server translates. */\n slashTemplates?: AssistantModeSlash[]\n /** What `@` offers. */\n catalogEntries?: any[]\n /**\n * i18n key for the words in the empty input.\n *\n * The screen says what this conversation is for, and that differs by mode: figure's modeller had\n * \"describe the figure you want\" while the person was there to change the figure already open.\n */\n placeholderKey?: string\n\n /**\n * ๋นˆ ๋Œ€ํ™”๊ฐ€ ๋ฌด์—‡์„ ๋งํ•˜๋Š”๊ฐ€ โ€” ์ œ๋ชฉ๊ณผ ํ•œ ์ค„ (ADR-0061 ๊ฒฐ์ • 7).\n *\n * ์ž๋ฆฌํ‘œ์™€ `/` ๋Š” ๋Œ€ํ™”๋ฅผ ๋”ฐ๋ผ๊ฐ€๋Š”๋ฐ ์ด ๋ธ”๋ก๋งŒ host ์†์„ฑ์— ๋‚จ์•„ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋จธ๋ฆฌ์—๋Š” ใ€Œ์ด ๋„ํ˜•\n * ๊ณ ์น˜๊ธฐใ€๊ฐ€ ์„œ ์žˆ๊ณ  ๋ณธ๋ฌธ์€ ใ€Œ๋งŒ๋“ค Figure ๋ฅผ ์„ค๋ช…ํ•˜์„ธ์š”ใ€๋ผ๊ณ  ์ ํ˜€ ์žˆ์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ\n * :3300). ํ•œ ๋Œ€ํ™”๊ฐ€ ํ™”๋ฉด์— ๋ฌด์—‡์„ ๋งํ•˜๋Š”๊ฐ€๋Š” ํ•œ ์ž๋ฆฌ์—์„œ ๋‚˜์™€์•ผ ํ•œ๋‹ค.\n */\n introKeys?: { title: string; subtitle: string }\n\n /**\n * ๋นˆ ๋Œ€ํ™”์˜ ์˜ˆ์‹œ โ€” ใ€Œ์ด ๋Œ€ํ™”์—์„œ ํ•  ์ˆ˜ ์žˆ๋Š” ๋งใ€.\n *\n * ์˜ˆ์‹œ๋Š” ๊ทธ ๋„๊ตฌ๋กœ **์‹ค์ œ๋กœ ํ•  ์ˆ˜ ์žˆ๋Š” ๋ง**์ด์–ด์•ผ ํ•œ๋‹ค. ํ•  ์ˆ˜ ์—†๋Š” ๊ฒƒ์„ ์˜ˆ์‹œ๋กœ ๋‘๋ฉด ๋นˆ ํ™”๋ฉด์ด ๊ฑฐ์ง“์„\n * ๊ฐ€๋ฅด์นœ๋‹ค. ๋Œ€ํ™”๋งˆ๋‹ค ์—ด๋ฆฌ๋Š” ๋„๊ตฌ๊ฐ€ ๋‹ค๋ฅด๋ฏ€๋กœ ์˜ˆ์‹œ๋„ ๋Œ€ํ™”๋ฅผ ๋”ฐ๋ผ๊ฐ„๋‹ค.\n */\n exampleKeys?: Array<{ labelKey: string; itemKeys: string[] }>\n\n /** i18n key for the line under an empty conversation. */\n footerNoticeKey?: string\n /** i18n key for the line shown once the host reports a proposal staged. */\n stagedNoticeKey?: string\n /** Follow-up chips for a proposal that arrived without its own. */\n proposalChoices?: any[]\n}\n\n/** A declaration that registered but is not what the contract asks for. Code + params, not prose. */\nexport interface AssistantModeWarning {\n /**\n * `assistant-mode-slash-outside-tools` โ€” a `/` sentence needs a tool this mode does not open.\n * `assistant-mode-subject-without-default` โ€” a subject type no conversation claims as its default.\n * `assistant-mode-subject-default-twice` โ€” two conversations claim the same subject type.\n * `assistant-mode-offered-without-label` โ€” a conversation a screen may offer with nothing to call it.\n */\n code:\n | 'assistant-mode-slash-outside-tools'\n | 'assistant-mode-subject-without-default'\n | 'assistant-mode-subject-default-twice'\n | 'assistant-mode-offered-without-label'\n params: { kind: string; slash?: string; tool?: string; subject?: string; kinds?: string[] }\n /** For a person reading the boot log; callers act on the code and params. */\n message: string\n}\n"]}
@@ -31,7 +31,20 @@ function planCarriedTurns(turns) {
31
31
  const content = (turn?.content ?? '').trim().slice(0, exports.CARRIED_CONTENT_LIMIT);
32
32
  if (!content)
33
33
  continue;
34
- const role = turn?.role === 'ai' || turn?.role === 'assistant' ? 'assistant' : turn?.role === 'user' ? 'user' : 'system';
34
+ /*
35
+ * ์ด ๋ฌธ์€ **์‚ฌ๋žŒ์˜ ๋ง๊ณผ ์–ด์‹œ์Šคํ„ดํŠธ์˜ ๋‹ต**๋งŒ ์˜ฎ๊ธด๋‹ค(ADR-0070 ๊ฒฐ์ • โ‘ข). ๊ทธ ๋ฐ–์˜ ์—ญํ• ์€ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค โ€”
36
+ * `system` ํ–‰์€ ์„œ๋ฒ„๊ฐ€ ์“ฐ๋Š” ์ž๋ฆฌ์ด๊ณ , ๋ชจ๋ธ์€ ๊ทธ ์ค„์„ ์‚ฌ๋žŒ์˜ ๋ง๋ณด๋‹ค ๋ฌด๊ฒ๊ฒŒ ์ฝ๋Š”๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€
37
+ * ใ€Œ์ด๊ฒƒ์€ ์‹œ์Šคํ…œ์ด ํ•œ ๋งใ€์ด๋ผ๊ณ  ์ ์„ ์ˆ˜ ์žˆ์œผ๋ฉด ์—ญํ• ์„ ์„œ๋ฒ„๊ฐ€ ์“ด๋‹ค๋Š” ๊ฒฐ์ •์ด ๋ฌด๋„ˆ์ง„๋‹ค.
38
+ *
39
+ * ์‚ฌ๋žŒ์ด ์นœ ๊ฒƒ์ด ์•„๋‹Œ ๋งฅ๋ฝ์€ `appendSessionNote` ๊ฐ€ ์ž๋ฆฌ๋‹ค โ€” ๊ทธ ๋ฌธ์€ ์„œ๋ฒ„๊ฐ€ `system` ์œผ๋กœ ์ ๋Š”๋‹ค.
40
+ */
41
+ const asked = turn?.role === 'ai' ? 'assistant' : turn?.role;
42
+ if (asked !== 'assistant' && asked !== 'user') {
43
+ throw new Error(`[ai-assistant/carried-turns] role '${String(turn?.role)}' cannot be carried. This door carries what ` +
44
+ "people said and what the assistant answered. Context that nobody typed goes through `appendSessionNote`, " +
45
+ 'which writes the system row itself.');
46
+ }
47
+ const role = asked;
35
48
  if (role === 'user') {
36
49
  planned.push({ role: 'user', content, addressee: 'room' });
37
50
  continue;
@@ -49,7 +62,6 @@ function planCarriedTurns(turns) {
49
62
  }
50
63
  continue;
51
64
  }
52
- planned.push({ role: 'system', content });
53
65
  }
54
66
  return planned.slice(-exports.CARRIED_TURN_LIMIT);
55
67
  }
@@ -1 +1 @@
1
- {"version":3,"file":"carried-turns.js","sourceRoot":"","sources":["../../../server/service/chat-message/carried-turns.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AA6BH,4CA+BC;AAQD,gDAEC;AAWD,oDAKC;AAlED,4EAA4E;AAC/D,QAAA,kBAAkB,GAAG,EAAE,CAAA;AACvB,QAAA,qBAAqB,GAAG,IAAI,CAAA;AAEzC;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAiB,EAAE,CAAA;IAEhC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,6BAAqB,CAAC,CAAA;QAC5E,IAAI,CAAC,OAAO;YAAE,SAAQ;QAEtB,MAAM,IAAI,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,IAAI,IAAI,EAAE,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAA;QAExH,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;YAC1D,SAAQ;QACV,CAAC;QAED,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YACzB,IAAI,IAAI,EAAE,eAAe,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,WAAW;oBACjB,OAAO;oBACP,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;iBAC7E,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACxE,CAAC;YACD,SAAQ;QACV,CAAC;QAED,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAA;IAC3C,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,0BAAkB,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,OAAe;IAChD,OAAO,qFAAqF,OAAO,GAAG,CAAA;AACxG,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAAC,QAAgB,EAAE,MAAiD;IACtG,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,SAAS;QACpB,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;KAC9G,CAAA;AACH,CAAC","sourcesContent":["/*\n * A conversation carried in from somewhere else, turned into rows this session can hold.\n *\n * โ”€โ”€ Why the assistant's turns are not simply written (ADR-0070 A15 decision โ‘ข) โ”€โ”€\n * `appendSessionTurns` took whatever roles the caller sent, so a client could write a row that\n * reads as the assistant speaking. Every participant then sees a sentence the assistant never\n * said, and the model reads it back as its own earlier turn. Roles are the server's to write.\n *\n * So a carried assistant turn is kept only when its original can be found โ€” the caller names it\n * (`sourceSessionId` ยท `sourceMessageId`) and the server copies the stored text. Without an\n * original it is kept as a quotation on a `system` row: the content is preserved, and nobody is\n * told the assistant said it here.\n */\n\n/** One turn as the caller carried it over. */\nexport interface CarriedTurn {\n role?: string\n content?: string\n /** Where an assistant turn came from, so the server can read the original instead of trusting it. */\n sourceSessionId?: string\n sourceMessageId?: string\n}\n\nexport interface PlannedRow {\n role: 'user' | 'assistant' | 'system'\n content: string\n /** People's words are addressed to the room โ€” this door is not the one that asks the assistant. */\n addressee?: 'room'\n /** Set when the row must be filled from a stored message before it is written. */\n copyOf?: { sessionId?: string; messageId: string }\n}\n\n/** How much of a carried conversation is kept โ€” context, not an archive. */\nexport const CARRIED_TURN_LIMIT = 40\nexport const CARRIED_CONTENT_LIMIT = 4000\n\n/**\n * What to write, before anything is looked up.\n *\n * Pure: the caller resolves `copyOf` against storage and decides what a missing original becomes.\n */\nexport function planCarriedTurns(turns: CarriedTurn[]): PlannedRow[] {\n const planned: PlannedRow[] = []\n\n for (const turn of Array.isArray(turns) ? turns : []) {\n const content = (turn?.content ?? '').trim().slice(0, CARRIED_CONTENT_LIMIT)\n if (!content) continue\n\n const role = turn?.role === 'ai' || turn?.role === 'assistant' ? 'assistant' : turn?.role === 'user' ? 'user' : 'system'\n\n if (role === 'user') {\n planned.push({ role: 'user', content, addressee: 'room' })\n continue\n }\n\n if (role === 'assistant') {\n if (turn?.sourceMessageId) {\n planned.push({\n role: 'assistant',\n content,\n copyOf: { sessionId: turn.sourceSessionId, messageId: turn.sourceMessageId }\n })\n } else {\n planned.push({ role: 'system', content: quoteCarriedAnswer(content) })\n }\n continue\n }\n\n planned.push({ role: 'system', content })\n }\n\n return planned.slice(-CARRIED_TURN_LIMIT)\n}\n\n/**\n * An answer with no original, kept as a quotation.\n *\n * English, like the other lines the model reads (`[system]`, the omission notices): this row is\n * context for the model first, and a person reading it sees the quoted text either way.\n */\nexport function quoteCarriedAnswer(content: string): string {\n return `carried from another conversation, quoted โ€” the assistant did not say this here: \"${content}\"`\n}\n\n/**\n * ์›๋ณธ์„ ์ฐพ๋Š” ์กฐ๊ฑด.\n *\n * ๋ฌธ์˜ ์„ค๋ช…์€ ใ€Œ`sourceSessionId` ์™€ `sourceMessageId` ๋กœ ์›๋ณธ์„ ๋Œ„๋‹คใ€๊ณ  ๋งํ•œ๋‹ค. ์กฐํšŒ๊ฐ€ ๋ฉ”์‹œ์ง€ id ์™€\n * ํ…Œ๋„ŒํŠธ๋งŒ ๋ณด๋ฉด, ์„œ๋กœ ์–ด๊ธ‹๋‚œ ๋‘ ์ธ์ž๊ฐ€ ์™€๋„ ์„ค๋ช…๊ณผ ๋‹ค๋ฅด๊ฒŒ ํ†ต๊ณผํ•œ๋‹ค โ€” ์„ค๋ช…๊ณผ ์ฝ”๋“œ๊ฐ€ ๊ฐˆ๋ฆฌ๋Š” ์ž๋ฆฌ๋‹ค\n * [์ˆ˜์„ ์•„ํ‚คํ…ํŠธ๊ฐ€ ADR-0070 ๊ตฌํ˜„ ๊ฒ€ํ† ์—์„œ ์ฐพ์Œ, 2026-09-18].\n *\n * ํ…Œ๋„ŒํŠธ๋Š” ์–ธ์ œ๋‚˜ ๊ฑด๋‹ค. ๋Œ€ํ™”๋ฅผ ๋Œ€๋ฉด ๊ทธ๊ฒƒ๋„ ๊ฑด๋‹ค.\n */\nexport function carriedOriginalWhere(domainId: string, copyOf: { sessionId?: string; messageId: string }): any {\n return {\n id: copyOf.messageId,\n session: copyOf.sessionId ? { id: copyOf.sessionId, domain: { id: domainId } } : { domain: { id: domainId } }\n }\n}\n"]}
1
+ {"version":3,"file":"carried-turns.js","sourceRoot":"","sources":["../../../server/service/chat-message/carried-turns.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;AA6BH,4CA4CC;AAQD,gDAEC;AAWD,oDAKC;AA/ED,4EAA4E;AAC/D,QAAA,kBAAkB,GAAG,EAAE,CAAA;AACvB,QAAA,qBAAqB,GAAG,IAAI,CAAA;AAEzC;;;;GAIG;AACH,SAAgB,gBAAgB,CAAC,KAAoB;IACnD,MAAM,OAAO,GAAiB,EAAE,CAAA;IAEhC,KAAK,MAAM,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC;QACrD,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,6BAAqB,CAAC,CAAA;QAC5E,IAAI,CAAC,OAAO;YAAE,SAAQ;QAEtB;;;;;;WAMG;QACH,MAAM,KAAK,GAAG,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAA;QAC5D,IAAI,KAAK,KAAK,WAAW,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAC9C,MAAM,IAAI,KAAK,CACb,sCAAsC,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,8CAA8C;gBACpG,2GAA2G;gBAC3G,qCAAqC,CACxC,CAAA;QACH,CAAC;QACD,MAAM,IAAI,GAAG,KAAK,CAAA;QAElB,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAA;YAC1D,SAAQ;QACV,CAAC;QAED,IAAI,IAAI,KAAK,WAAW,EAAE,CAAC;YACzB,IAAI,IAAI,EAAE,eAAe,EAAE,CAAC;gBAC1B,OAAO,CAAC,IAAI,CAAC;oBACX,IAAI,EAAE,WAAW;oBACjB,OAAO;oBACP,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,EAAE,IAAI,CAAC,eAAe,EAAE;iBAC7E,CAAC,CAAA;YACJ,CAAC;iBAAM,CAAC;gBACN,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;YACxE,CAAC;YACD,SAAQ;QACV,CAAC;IACH,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,0BAAkB,CAAC,CAAA;AAC3C,CAAC;AAED;;;;;GAKG;AACH,SAAgB,kBAAkB,CAAC,OAAe;IAChD,OAAO,qFAAqF,OAAO,GAAG,CAAA;AACxG,CAAC;AAED;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAAC,QAAgB,EAAE,MAAiD;IACtG,OAAO;QACL,EAAE,EAAE,MAAM,CAAC,SAAS;QACpB,OAAO,EAAE,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE;KAC9G,CAAA;AACH,CAAC","sourcesContent":["/*\n * A conversation carried in from somewhere else, turned into rows this session can hold.\n *\n * โ”€โ”€ Why the assistant's turns are not simply written (ADR-0070 A15 decision โ‘ข) โ”€โ”€\n * `appendSessionTurns` took whatever roles the caller sent, so a client could write a row that\n * reads as the assistant speaking. Every participant then sees a sentence the assistant never\n * said, and the model reads it back as its own earlier turn. Roles are the server's to write.\n *\n * So a carried assistant turn is kept only when its original can be found โ€” the caller names it\n * (`sourceSessionId` ยท `sourceMessageId`) and the server copies the stored text. Without an\n * original it is kept as a quotation on a `system` row: the content is preserved, and nobody is\n * told the assistant said it here.\n */\n\n/** One turn as the caller carried it over. */\nexport interface CarriedTurn {\n role?: string\n content?: string\n /** Where an assistant turn came from, so the server can read the original instead of trusting it. */\n sourceSessionId?: string\n sourceMessageId?: string\n}\n\nexport interface PlannedRow {\n role: 'user' | 'assistant' | 'system'\n content: string\n /** People's words are addressed to the room โ€” this door is not the one that asks the assistant. */\n addressee?: 'room'\n /** Set when the row must be filled from a stored message before it is written. */\n copyOf?: { sessionId?: string; messageId: string }\n}\n\n/** How much of a carried conversation is kept โ€” context, not an archive. */\nexport const CARRIED_TURN_LIMIT = 40\nexport const CARRIED_CONTENT_LIMIT = 4000\n\n/**\n * What to write, before anything is looked up.\n *\n * Pure: the caller resolves `copyOf` against storage and decides what a missing original becomes.\n */\nexport function planCarriedTurns(turns: CarriedTurn[]): PlannedRow[] {\n const planned: PlannedRow[] = []\n\n for (const turn of Array.isArray(turns) ? turns : []) {\n const content = (turn?.content ?? '').trim().slice(0, CARRIED_CONTENT_LIMIT)\n if (!content) continue\n\n /*\n * ์ด ๋ฌธ์€ **์‚ฌ๋žŒ์˜ ๋ง๊ณผ ์–ด์‹œ์Šคํ„ดํŠธ์˜ ๋‹ต**๋งŒ ์˜ฎ๊ธด๋‹ค(ADR-0070 ๊ฒฐ์ • โ‘ข). ๊ทธ ๋ฐ–์˜ ์—ญํ• ์€ ๋ฐ›์ง€ ์•Š๋Š”๋‹ค โ€”\n * `system` ํ–‰์€ ์„œ๋ฒ„๊ฐ€ ์“ฐ๋Š” ์ž๋ฆฌ์ด๊ณ , ๋ชจ๋ธ์€ ๊ทธ ์ค„์„ ์‚ฌ๋žŒ์˜ ๋ง๋ณด๋‹ค ๋ฌด๊ฒ๊ฒŒ ์ฝ๋Š”๋‹ค. ํด๋ผ์ด์–ธํŠธ๊ฐ€\n * ใ€Œ์ด๊ฒƒ์€ ์‹œ์Šคํ…œ์ด ํ•œ ๋งใ€์ด๋ผ๊ณ  ์ ์„ ์ˆ˜ ์žˆ์œผ๋ฉด ์—ญํ• ์„ ์„œ๋ฒ„๊ฐ€ ์“ด๋‹ค๋Š” ๊ฒฐ์ •์ด ๋ฌด๋„ˆ์ง„๋‹ค.\n *\n * ์‚ฌ๋žŒ์ด ์นœ ๊ฒƒ์ด ์•„๋‹Œ ๋งฅ๋ฝ์€ `appendSessionNote` ๊ฐ€ ์ž๋ฆฌ๋‹ค โ€” ๊ทธ ๋ฌธ์€ ์„œ๋ฒ„๊ฐ€ `system` ์œผ๋กœ ์ ๋Š”๋‹ค.\n */\n const asked = turn?.role === 'ai' ? 'assistant' : turn?.role\n if (asked !== 'assistant' && asked !== 'user') {\n throw new Error(\n `[ai-assistant/carried-turns] role '${String(turn?.role)}' cannot be carried. This door carries what ` +\n \"people said and what the assistant answered. Context that nobody typed goes through `appendSessionNote`, \" +\n 'which writes the system row itself.'\n )\n }\n const role = asked\n\n if (role === 'user') {\n planned.push({ role: 'user', content, addressee: 'room' })\n continue\n }\n\n if (role === 'assistant') {\n if (turn?.sourceMessageId) {\n planned.push({\n role: 'assistant',\n content,\n copyOf: { sessionId: turn.sourceSessionId, messageId: turn.sourceMessageId }\n })\n } else {\n planned.push({ role: 'system', content: quoteCarriedAnswer(content) })\n }\n continue\n }\n }\n\n return planned.slice(-CARRIED_TURN_LIMIT)\n}\n\n/**\n * An answer with no original, kept as a quotation.\n *\n * English, like the other lines the model reads (`[system]`, the omission notices): this row is\n * context for the model first, and a person reading it sees the quoted text either way.\n */\nexport function quoteCarriedAnswer(content: string): string {\n return `carried from another conversation, quoted โ€” the assistant did not say this here: \"${content}\"`\n}\n\n/**\n * ์›๋ณธ์„ ์ฐพ๋Š” ์กฐ๊ฑด.\n *\n * ๋ฌธ์˜ ์„ค๋ช…์€ ใ€Œ`sourceSessionId` ์™€ `sourceMessageId` ๋กœ ์›๋ณธ์„ ๋Œ„๋‹คใ€๊ณ  ๋งํ•œ๋‹ค. ์กฐํšŒ๊ฐ€ ๋ฉ”์‹œ์ง€ id ์™€\n * ํ…Œ๋„ŒํŠธ๋งŒ ๋ณด๋ฉด, ์„œ๋กœ ์–ด๊ธ‹๋‚œ ๋‘ ์ธ์ž๊ฐ€ ์™€๋„ ์„ค๋ช…๊ณผ ๋‹ค๋ฅด๊ฒŒ ํ†ต๊ณผํ•œ๋‹ค โ€” ์„ค๋ช…๊ณผ ์ฝ”๋“œ๊ฐ€ ๊ฐˆ๋ฆฌ๋Š” ์ž๋ฆฌ๋‹ค\n * [์ˆ˜์„ ์•„ํ‚คํ…ํŠธ๊ฐ€ ADR-0070 ๊ตฌํ˜„ ๊ฒ€ํ† ์—์„œ ์ฐพ์Œ, 2026-09-18].\n *\n * ํ…Œ๋„ŒํŠธ๋Š” ์–ธ์ œ๋‚˜ ๊ฑด๋‹ค. ๋Œ€ํ™”๋ฅผ ๋Œ€๋ฉด ๊ทธ๊ฒƒ๋„ ๊ฑด๋‹ค.\n */\nexport function carriedOriginalWhere(domainId: string, copyOf: { sessionId?: string; messageId: string }): any {\n return {\n id: copyOf.messageId,\n session: copyOf.sessionId ? { id: copyOf.sessionId, domain: { id: domainId } } : { domain: { id: domainId } }\n }\n}\n"]}
@@ -1,12 +1,24 @@
1
- import type { ToolPrivilege, ToolSpec } from '@things-factory/ai-client-base';
2
- /** ์ด ๋ฌธ์„ ์ง€ํ‚ค๋Š” ๊ถŒํ•œ โ€” ๊ทธ ๋ฌธ์— ๋‹ฟ๋Š”๋‹ค๊ณ  ์„ ์–ธํ•œ ๋„๊ตฌ์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค. */
3
- export declare function doorPrivilege(door: string, specs: readonly ToolSpec[]): ToolPrivilege | undefined;
1
+ import type { FieldGate } from '@things-factory/auth-base';
2
+ /** `privilegeOfField` ์™€ ๊ฐ™์€ ๋ชจ์–‘ โ€” ์ด ํŒŒ์ผ์€ ํ‘œ๋ฅผ **์ฝ๊ธฐ๋งŒ** ํ•˜๊ณ  ์–ด๋””์„œ ์˜ค๋Š”์ง€๋Š” ๋ชจ๋ฅธ๋‹ค. */
3
+ export type FieldLookup = (typeName: string, fieldName: string) => {
4
+ known: false;
5
+ } | {
6
+ known: true;
7
+ gate: FieldGate | null;
8
+ };
4
9
  export interface DoorCheckDeps {
5
- /** ๋“ฑ๋ก๋œ ๋„๊ตฌ ๋ช…์„ธ ์ „๋ถ€ โ€” ๋ฌธโ†’๊ถŒํ•œ์„ ์ฐพ๋Š” ํ‘œ ๋…ธ๋ฆ‡. */
6
- specs: readonly ToolSpec[];
7
- /** AUTH ์กฐํšŒ. `User.hasPrivilege` ๋ฅผ ๊ทธ๋Œ€๋กœ ์‹ธ์„œ ๋„˜๊ธด๋‹ค. */
8
- hasPrivilege(privilege: ToolPrivilege): Promise<boolean>;
10
+ /** AUTH ์˜ ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ. ์šด์˜์—์„œ๋Š” `privilegeOfField` ๊ฐ€ ๋“ค์–ด์˜จ๋‹ค(`reader-gate.ts`). */
11
+ lookup: FieldLookup;
12
+ /** `checkPermission` ์„ ๊ทธ๋Œ€๋กœ ์‹ธ์„œ ๋„˜๊ธด๋‹ค โ€” ํŒ์ •์€ AUTH ๊ฐ€ ํ•œ๋‹ค. */
13
+ permits(gate: FieldGate): Promise<boolean>;
9
14
  }
15
+ /** `Query.jobOrders` ๋ฅผ ํƒ€์ž…๊ณผ ํ•„๋“œ๋กœ ๊ฐ€๋ฅธ๋‹ค. ๋ชจ์–‘์ด ์•„๋‹ˆ๋ฉด undefined โ€” ์•„๋ฌด๋„ ๋ชป ์ง€๋‚œ๋‹ค. */
16
+ export declare function doorField(door: string): {
17
+ typeName: string;
18
+ fieldName: string;
19
+ } | undefined;
20
+ /** ์ด ๋ฌธ์ด ์Šคํ‚ค๋งˆ์— ์žˆ๋Š”๊ฐ€ โ€” ์—†์œผ๋ฉด ์„ ์–ธ์ด ํ‹€๋ฆฐ ๊ฒƒ์ด๊ณ , ๋ถ€ํŒ… ๋ณด๊ณ ๊ฐ€ ๊ทธ๊ฒƒ์„ ๋งํ•œ๋‹ค. */
21
+ export declare function isKnownDoor(door: string, lookup: FieldLookup): boolean;
10
22
  /**
11
23
  * ๋ฌธ ํ•˜๋‚˜๋ฅผ ์ด ์‚ฌ๋žŒ์ด ์ง€๋‚˜๋Š”๊ฐ€ โ€” `doorGate` ์— ๋„ฃ์„ ๊ฒ€์‚ฌ.
12
24
  *
@@ -1,14 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.doorPrivilege = doorPrivilege;
3
+ exports.doorField = doorField;
4
+ exports.isKnownDoor = isKnownDoor;
4
5
  exports.doorCheck = doorCheck;
5
- /** ์ด ๋ฌธ์„ ์ง€ํ‚ค๋Š” ๊ถŒํ•œ โ€” ๊ทธ ๋ฌธ์— ๋‹ฟ๋Š”๋‹ค๊ณ  ์„ ์–ธํ•œ ๋„๊ตฌ์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค. */
6
- function doorPrivilege(door, specs) {
7
- for (const spec of specs) {
8
- if ((spec.doors ?? []).includes(door))
9
- return spec.privilege;
10
- }
11
- return undefined;
6
+ /** `Query.jobOrders` ๋ฅผ ํƒ€์ž…๊ณผ ํ•„๋“œ๋กœ ๊ฐ€๋ฅธ๋‹ค. ๋ชจ์–‘์ด ์•„๋‹ˆ๋ฉด undefined โ€” ์•„๋ฌด๋„ ๋ชป ์ง€๋‚œ๋‹ค. */
7
+ function doorField(door) {
8
+ const match = /^(Query|Mutation|Subscription)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(door ?? '');
9
+ return match ? { typeName: match[1], fieldName: match[2] } : undefined;
10
+ }
11
+ /** ์ด ๋ฌธ์ด ์Šคํ‚ค๋งˆ์— ์žˆ๋Š”๊ฐ€ โ€” ์—†์œผ๋ฉด ์„ ์–ธ์ด ํ‹€๋ฆฐ ๊ฒƒ์ด๊ณ , ๋ถ€ํŒ… ๋ณด๊ณ ๊ฐ€ ๊ทธ๊ฒƒ์„ ๋งํ•œ๋‹ค. */
12
+ function isKnownDoor(door, lookup) {
13
+ const field = doorField(door);
14
+ return !!field && lookup(field.typeName, field.fieldName).known;
12
15
  }
13
16
  /**
14
17
  * ๋ฌธ ํ•˜๋‚˜๋ฅผ ์ด ์‚ฌ๋žŒ์ด ์ง€๋‚˜๋Š”๊ฐ€ โ€” `doorGate` ์— ๋„ฃ์„ ๊ฒ€์‚ฌ.
@@ -17,10 +20,15 @@ function doorPrivilege(door, specs) {
17
20
  */
18
21
  function doorCheck(deps) {
19
22
  return async (door) => {
20
- const privilege = doorPrivilege(door, deps.specs);
21
- if (!privilege)
23
+ const field = doorField(door);
24
+ if (!field)
25
+ return false;
26
+ const asked = deps.lookup(field.typeName, field.fieldName);
27
+ if (!asked.known)
22
28
  return false;
23
- return await deps.hasPrivilege(privilege);
29
+ if (asked.gate === null)
30
+ return true;
31
+ return await deps.permits(asked.gate);
24
32
  };
25
33
  }
26
34
  //# sourceMappingURL=door-access.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"door-access.js","sourceRoot":"","sources":["../../../server/service/chat-message/door-access.ts"],"names":[],"mappings":";;AAkBA,sCAKC;AAcD,8BAOC;AA3BD,6CAA6C;AAC7C,SAAgB,aAAa,CAAC,IAAY,EAAE,KAA0B;IACpE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,IAAI,CAAE,IAAY,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAQ,IAAY,CAAC,SAAS,CAAA;IAChF,CAAC;IACD,OAAO,SAAS,CAAA;AAClB,CAAC;AASD;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAmB;IAC3C,OAAO,KAAK,EAAE,IAAY,EAAE,EAAE;QAC5B,MAAM,SAAS,GAAG,aAAa,CAAC,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAA;QACjD,IAAI,CAAC,SAAS;YAAE,OAAO,KAAK,CAAA;QAE5B,OAAO,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAA;IAC3C,CAAC,CAAA;AACH,CAAC","sourcesContent":["/*\n * ๋ฌธ โ†’ ๊ทธ ๋ฌธ์„ ์ง€ํ‚ค๋Š” ๊ถŒํ•œ โ€” ADR-0073 ํŒ์ • 1 ์˜ ๊ด€๋ฌธ์„ ์„ธ์šฐ๋Š” ์ž๋ฆฌ.\n *\n * โ”€โ”€ ์™œ ๋„๊ตฌ ๋“ฑ๋ก๊ธฐ์—์„œ ์ฐพ๋‚˜ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ํ–‰์— ์ ํžŒ ๊ฒƒ์€ ๋ฌธ ์ด๋ฆ„(`Query.jobOrders`)์ด๊ณ , ์‚ฌ๋žŒ์ด ๊ฐ€์ง„ ๊ฒƒ์€ ๊ถŒํ•œ(`job-order:query`)์ด๋‹ค. ๋‘˜์„\n * ์ž‡๋Š” ์ •๋ณธ์€ AUTH ์˜ ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ์ด๊ณ , ๊ทธ ํ‘œ๋Š” ์•„์ง ์—†๋‹ค(ADR-0072 ๊ฒฐ์ • 2 (b) โ€” ์ˆ˜์„ ์•„ํ‚คํ…ํŠธ๊ฐ€ ์ฒญํ•˜๊ธฐ๋กœ\n * ๋˜์–ด ์žˆ๋‹ค). ๊ทธ๋•Œ๊นŒ์ง€๋Š” ๋„๊ตฌ ๋ช…์„ธ๊ฐ€ ์ด๋ฏธ ๋“ค๊ณ  ์žˆ๋Š” ์ง์„ ์“ด๋‹ค: ๊ทธ ๋ฌธ์— ๋‹ฟ๋Š”๋‹ค๊ณ  ์„ ์–ธํ•œ ๋„๊ตฌ๊ฐ€ ์ž๊ธฐ\n * ๊ถŒํ•œ๋„ ํ•จ๊ป˜ ์ ์–ด ๋‘์—ˆ๋‹ค.\n *\n * **์ด๊ฒƒ์€ ์‚ฌ๋ณธ์ด๋‹ค.** ํ‘œ๊ฐ€ ์„œ๋ฉด `doorPrivilege` ํ•˜๋‚˜๋งŒ ๊ทธ ํ‘œ๋ฅผ ๋ณด๊ฒŒ ๋ฐ”๊พธ๊ณ  ๋‚˜๋จธ์ง€๋Š” ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.\n *\n * โ”€โ”€ ๋ชจ๋ฅด๋Š” ๋ฌธ์€ ๋‹ซ๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ์–ด๋А ๋„๊ตฌ๋„ ๊ทธ ๋ฌธ์„ ์„ ์–ธํ•˜์ง€ ์•Š์•˜์œผ๋ฉด ๋ฌด์—‡์ด ๊ทธ ๋ฌธ์„ ์ง€ํ‚ค๋Š”์ง€ ์•Œ ์ˆ˜ ์—†๋‹ค. ๋ชจ๋ฅผ ๋•Œ ์—ด๋ฉด, ๋„๊ตฌ๊ฐ€ ์‚ฌ๋ผ์ง„\n * ๋’ค์— ๋‚จ์€ ์˜› ๋‹ต์ด ์ „๋ถ€ ๊ณต๊ฐœ๋œ๋‹ค โ€” ์ง€์šฐ๋Š” ์ผ์ด ์—ฌ๋Š” ์ผ์ด ๋œ๋‹ค. ๋‹ซ๋Š” ์ชฝ์œผ๋กœ ๋„˜์–ด๋œจ๋ฆฐ๋‹ค.\n */\nimport type { ToolPrivilege, ToolSpec } from '@things-factory/ai-client-base'\n\n/** ์ด ๋ฌธ์„ ์ง€ํ‚ค๋Š” ๊ถŒํ•œ โ€” ๊ทธ ๋ฌธ์— ๋‹ฟ๋Š”๋‹ค๊ณ  ์„ ์–ธํ•œ ๋„๊ตฌ์—์„œ ๊ฐ€์ ธ์˜จ๋‹ค. */\nexport function doorPrivilege(door: string, specs: readonly ToolSpec[]): ToolPrivilege | undefined {\n for (const spec of specs) {\n if (((spec as any).doors ?? []).includes(door)) return (spec as any).privilege\n }\n return undefined\n}\n\nexport interface DoorCheckDeps {\n /** ๋“ฑ๋ก๋œ ๋„๊ตฌ ๋ช…์„ธ ์ „๋ถ€ โ€” ๋ฌธโ†’๊ถŒํ•œ์„ ์ฐพ๋Š” ํ‘œ ๋…ธ๋ฆ‡. */\n specs: readonly ToolSpec[]\n /** AUTH ์กฐํšŒ. `User.hasPrivilege` ๋ฅผ ๊ทธ๋Œ€๋กœ ์‹ธ์„œ ๋„˜๊ธด๋‹ค. */\n hasPrivilege(privilege: ToolPrivilege): Promise<boolean>\n}\n\n/**\n * ๋ฌธ ํ•˜๋‚˜๋ฅผ ์ด ์‚ฌ๋žŒ์ด ์ง€๋‚˜๋Š”๊ฐ€ โ€” `doorGate` ์— ๋„ฃ์„ ๊ฒ€์‚ฌ.\n *\n * ๋˜ํ’€์ด ์กฐํšŒ๋ฅผ ์ค„์ด๋Š” ๊ธฐ์–ต์€ `doorGate` ๊ฐ€ ํ•œ๋‹ค. ์—ฌ๊ธฐ๋Š” ํ•œ ๋ฒˆ์˜ ํŒ์ •๋งŒ ํ•œ๋‹ค.\n */\nexport function doorCheck(deps: DoorCheckDeps): (door: string) => Promise<boolean> {\n return async (door: string) => {\n const privilege = doorPrivilege(door, deps.specs)\n if (!privilege) return false\n\n return await deps.hasPrivilege(privilege)\n }\n}\n"]}
1
+ {"version":3,"file":"door-access.js","sourceRoot":"","sources":["../../../server/service/chat-message/door-access.ts"],"names":[],"mappings":";;AAqCA,8BAGC;AAGD,kCAGC;AAOD,8BAWC;AA5BD,sEAAsE;AACtE,SAAgB,SAAS,CAAC,IAAY;IACpC,MAAM,KAAK,GAAG,2DAA2D,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAC1F,OAAO,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;AACxE,CAAC;AAED,sDAAsD;AACtD,SAAgB,WAAW,CAAC,IAAY,EAAE,MAAmB;IAC3D,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;IAC7B,OAAO,CAAC,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC,KAAK,CAAA;AACjE,CAAC;AAED;;;;GAIG;AACH,SAAgB,SAAS,CAAC,IAAmB;IAC3C,OAAO,KAAK,EAAE,IAAY,EAAE,EAAE;QAC5B,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,CAAC,CAAA;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAExB,MAAM,KAAK,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;QAC1D,IAAI,CAAC,KAAK,CAAC,KAAK;YAAE,OAAO,KAAK,CAAA;QAC9B,IAAI,KAAK,CAAC,IAAI,KAAK,IAAI;YAAE,OAAO,IAAI,CAAA;QAEpC,OAAO,MAAM,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IACvC,CAAC,CAAA;AACH,CAAC","sourcesContent":["/*\n * ๋ฌธ โ†’ ๊ทธ ๋ฌธ์„ ์ง€ํ‚ค๋Š” ๊ถŒํ•œ โ€” ADR-0073 ํŒ์ • 1 ์˜ ๊ด€๋ฌธ์„ ์„ธ์šฐ๋Š” ์ž๋ฆฌ.\n *\n * โ”€โ”€ ์ •๋ณธ์€ AUTH ์˜ ํ‘œ๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ํ–‰์— ์ ํžŒ ๊ฒƒ์€ ๋ฌธ ์ด๋ฆ„(`Query.jobOrders`)์ด๊ณ , ์‚ฌ๋žŒ์ด ๊ฐ€์ง„ ๊ฒƒ์€ ๊ถŒํ•œ(`job-order:query`)์ด๋‹ค. ๋‘˜์„\n * ์ž‡๋Š” ๊ฒƒ์€ `privilegeOfField(typeName, fieldName)` ์ด๋‹ค(ADR-0072 ๊ฒฐ์ • 5). ์Šคํ‚ค๋งˆ๋ฅผ ์ง€์„ ๋•Œ ๋ชจ๋“  ๋ฃจํŠธ\n * ํ•„๋“œ๊ฐ€ ๊ธฐ๋ก๋˜๋ฏ€๋กœ, ์ด ํ‘œ๋Š” **๊ทธ ๋ฌธ์ด ์‹ค์ œ๋กœ ๋ฌด์—‡์„ ๋ฌป๋Š”์ง€**๋ฅผ ๊ทธ๋Œ€๋กœ ๋‹ตํ•œ๋‹ค.\n *\n * ๊ทธ ์ „์—๋Š” ๋„๊ตฌ ๋ช…์„ธ๊ฐ€ ๋“ค๊ณ  ์žˆ๋Š” ์ง์„ ๋ฒ ๊ปด ์ผ๋‹ค. ๋ฒ ๋‚€ ๊ฐ’์€ ๋ฌธ์ด ์„ ์–ธ์„ ๋ฐ”๊พธ๋ฉด ์กฐ์šฉํžˆ ์–ด๊ธ‹๋‚˜๊ณ ,\n * ์–ด๊ธ‹๋‚˜๋ฉด ๋А์Šจํ•œ ์ชฝ์ด ์ด๊ธด๋‹ค. ํ‘œ๊ฐ€ ์„œ๋ฉด์„œ ๊ทธ ์‚ฌ๋ณธ์„ ๊ฑท์—ˆ๋‹ค.\n *\n * โ”€โ”€ ํŒ์ •ํ•˜์ง€ ์•Š๊ณ  AUTH ์—๊ฒŒ ๋ฌป๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ํ‘œ๊ฐ€ ์ฃผ๋Š” gate ๋Š” `checkPermission` ์ด ๋ฐ›๋Š” ๋„ท(category ยท privilege ยท owner ยท super)๊ณผ ๊ฐ™์€ ๋ชจ์–‘์ด๊ณ ,\n * ๊ทธ๋Œ€๋กœ `checkPermission` ์— ๋„˜๊ธด๋‹ค. ์—ฌ๊ธฐ์„œ ใ€Œcategory ๊ฐ€ ์—†์œผ๋ฉด ์†Œ์œ ๊ถŒ๋งŒ ๋ณธ๋‹คใ€ ๊ฐ™์€ ๊ทœ์น™์„ ๋‹ค์‹œ ์“ฐ๋ฉด\n * ๊ทธ ์ˆœ๊ฐ„ AUTH ์™€ ์ด ํŒŒ์ผ์ด ๋‘ ๋ฒŒ์ด ๋œ๋‹ค. ๋ฌธ์—์„œ ํ†ตํ•˜๋Š” ๊ฒƒ์ด ์—ฌ๊ธฐ์„œ๋„ ํ†ตํ•ด์•ผ ํ•œ๋‹ค.\n *\n * โ”€โ”€ ์…‹ ์ค‘ ๋‘˜์€ ๋‹ซ๋Š”๋‹ค โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * `{ known: false }` ๊ทธ๋Ÿฐ ๋ฃจํŠธ ํ•„๋“œ๊ฐ€ ์—†๋‹ค. ๋„๊ตฌ๊ฐ€ ๋ฌธ ์ด๋ฆ„์„ ํ‹€๋ฆฌ๊ฒŒ ์ ์€ ๊ฒƒ์ด๋‹ค โ€”\n * **์—ด๋ฆผ์œผ๋กœ ์ฝ์ง€ ์•Š๋Š”๋‹ค.** ๋ถ€ํŒ… ๋ณด๊ณ ๊ฐ€ ๋”ฐ๋กœ ๋งํ•œ๋‹ค.\n * `{ known: true, gate: null }` ํ•„๋“œ๋Š” ์žˆ๊ณ  ์„ ์–ธ์ด ์—†๋‹ค. ๊ทธ ๋ฌธ์€ ์—ด๋ ค ์žˆ์œผ๋ฏ€๋กœ ์ด ํ–‰๋„ ์—ด๋ฆฐ๋‹ค.\n * `{ known: true, gate }` ๊ทธ ๋ฌธ์ด ๋ฌป๋Š” ๊ฒƒ์„ ์ด ์‚ฌ๋žŒ์ด ๊ฐ€์กŒ๋Š”์ง€ AUTH ์—๊ฒŒ ๋ฌป๋Š”๋‹ค.\n *\n * ์Šคํ‚ค๋งˆ๋ฅผ ์ง“๊ธฐ ์ „์—๋Š” ํ‘œ๊ฐ€ ๋น„์–ด ์žˆ์–ด ๋ชจ๋“  ๋ฌธ์ด `known: false` ๋กœ ๋‹ตํ•œ๋‹ค โ€” ๊ฐ€๋ ค์ง€๋Š” ์ชฝ์ด๋‹ค.\n */\nimport type { FieldGate } from '@things-factory/auth-base'\n\n/** `privilegeOfField` ์™€ ๊ฐ™์€ ๋ชจ์–‘ โ€” ์ด ํŒŒ์ผ์€ ํ‘œ๋ฅผ **์ฝ๊ธฐ๋งŒ** ํ•˜๊ณ  ์–ด๋””์„œ ์˜ค๋Š”์ง€๋Š” ๋ชจ๋ฅธ๋‹ค. */\nexport type FieldLookup = (typeName: string, fieldName: string) => { known: false } | { known: true; gate: FieldGate | null }\n\nexport interface DoorCheckDeps {\n /** AUTH ์˜ ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ. ์šด์˜์—์„œ๋Š” `privilegeOfField` ๊ฐ€ ๋“ค์–ด์˜จ๋‹ค(`reader-gate.ts`). */\n lookup: FieldLookup\n /** `checkPermission` ์„ ๊ทธ๋Œ€๋กœ ์‹ธ์„œ ๋„˜๊ธด๋‹ค โ€” ํŒ์ •์€ AUTH ๊ฐ€ ํ•œ๋‹ค. */\n permits(gate: FieldGate): Promise<boolean>\n}\n\n/** `Query.jobOrders` ๋ฅผ ํƒ€์ž…๊ณผ ํ•„๋“œ๋กœ ๊ฐ€๋ฅธ๋‹ค. ๋ชจ์–‘์ด ์•„๋‹ˆ๋ฉด undefined โ€” ์•„๋ฌด๋„ ๋ชป ์ง€๋‚œ๋‹ค. */\nexport function doorField(door: string): { typeName: string; fieldName: string } | undefined {\n const match = /^(Query|Mutation|Subscription)\\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(door ?? '')\n return match ? { typeName: match[1], fieldName: match[2] } : undefined\n}\n\n/** ์ด ๋ฌธ์ด ์Šคํ‚ค๋งˆ์— ์žˆ๋Š”๊ฐ€ โ€” ์—†์œผ๋ฉด ์„ ์–ธ์ด ํ‹€๋ฆฐ ๊ฒƒ์ด๊ณ , ๋ถ€ํŒ… ๋ณด๊ณ ๊ฐ€ ๊ทธ๊ฒƒ์„ ๋งํ•œ๋‹ค. */\nexport function isKnownDoor(door: string, lookup: FieldLookup): boolean {\n const field = doorField(door)\n return !!field && lookup(field.typeName, field.fieldName).known\n}\n\n/**\n * ๋ฌธ ํ•˜๋‚˜๋ฅผ ์ด ์‚ฌ๋žŒ์ด ์ง€๋‚˜๋Š”๊ฐ€ โ€” `doorGate` ์— ๋„ฃ์„ ๊ฒ€์‚ฌ.\n *\n * ๋˜ํ’€์ด ์กฐํšŒ๋ฅผ ์ค„์ด๋Š” ๊ธฐ์–ต์€ `doorGate` ๊ฐ€ ํ•œ๋‹ค. ์—ฌ๊ธฐ๋Š” ํ•œ ๋ฒˆ์˜ ํŒ์ •๋งŒ ํ•œ๋‹ค.\n */\nexport function doorCheck(deps: DoorCheckDeps): (door: string) => Promise<boolean> {\n return async (door: string) => {\n const field = doorField(door)\n if (!field) return false\n\n const asked = deps.lookup(field.typeName, field.fieldName)\n if (!asked.known) return false\n if (asked.gate === null) return true\n\n return await deps.permits(asked.gate)\n }\n}\n"]}
@@ -8,7 +8,6 @@ exports.readerGate = readerGate;
8
8
  * ์„ธ์…˜์ด private ์ธ์ง€๋„ ์—ฌ๊ธฐ์„œ ํ•จ๊ป˜ ๋‹ตํ•œ๋‹ค. ๋ฆฌ์กธ๋ฒ„๋งˆ๋‹ค ๊ฐ™์€ ํŒ์ •์„ ๋‹ค์‹œ ์ ์œผ๋ฉด ํ•œ ๊ณณ์ด ๋น ์ง„ ๋‚ 
9
9
  * ๊ทธ ๋ฌธ์œผ๋กœ ์ƒŒ๋‹ค.
10
10
  */
11
- const ai_client_base_1 = require("@things-factory/ai-client-base");
12
11
  const auth_base_1 = require("@things-factory/auth-base");
13
12
  const door_access_js_1 = require("./door-access.js");
14
13
  const message_doors_js_1 = require("./message-doors.js");
@@ -17,9 +16,12 @@ const request_translator_js_1 = require("../request-translator.js");
17
16
  function readerGate(context) {
18
17
  const { domain, user } = context.state;
19
18
  const t = (0, request_translator_js_1.requestTranslator)(context);
20
- const gate = (0, message_doors_js_1.doorGate)((0, door_access_js_1.doorCheck)({
21
- specs: (0, ai_client_base_1.getAllToolSpecs)(),
22
- hasPrivilege: privilege => auth_base_1.User.hasPrivilege(privilege.privilege, privilege.category, domain, user)
19
+ const { unsafeIP, prohibitedPrivileges } = context.state;
20
+ const gate = (0, message_doors_js_1.doorGate)(
21
+ /* ๋ฌธ์—์„œ ์“ฐ๋Š” ํŒ์ •์„ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค โ€” ๊ฐ™์€ ์ธ์ž, ๊ฐ™์€ ํ•จ์ˆ˜(ADR-0072 ๊ฒฐ์ • 5). */
22
+ (0, door_access_js_1.doorCheck)({
23
+ lookup: auth_base_1.privilegeOfField,
24
+ permits: fieldGate => (0, auth_base_1.checkPermission)(fieldGate, user, domain, unsafeIP, prohibitedPrivileges)
23
25
  }));
24
26
  /*
25
27
  * ๋ฌธ ์ด๋ฆ„(`Query.productionReports`)์€ ์ด ๋ฌธ์žฅ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.
@@ -1 +1 @@
1
- {"version":3,"file":"reader-gate.js","sourceRoot":"","sources":["../../../server/service/chat-message/reader-gate.ts"],"names":[],"mappings":";;AAeA,gCA0BC;AAzCD;;;;;;GAMG;AACH,mEAAgE;AAChE,yDAAgD;AAEhD,qDAA4C;AAC5C,yDAAkF;AAClF,oEAA4D;AAE5D,6CAA6C;AAC7C,SAAgB,UAAU,CAAC,OAAY;IACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACtC,MAAM,CAAC,GAAG,IAAA,yCAAiB,EAAC,OAAO,CAAC,CAAA;IAEpC,MAAM,IAAI,GAAG,IAAA,2BAAQ,EACnB,IAAA,0BAAS,EAAC;QACR,KAAK,EAAE,IAAA,gCAAe,GAAE;QACxB,YAAY,EAAE,SAAS,CAAC,EAAE,CAAC,gBAAI,CAAC,YAAY,CAAC,SAAS,CAAC,SAAS,EAAE,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC;KACpG,CAAC,CACH,CAAA;IAED;;;;;;;OAOG;IACH,OAAO,IAAI,CAAC,EAAE,CACZ,IAAA,gCAAa,EAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAC7B,CAAC,CAAC,2CAA2C,EAAE;QAC7C,mDAAmD;QACnD,YAAY,EAAE,iFAAiF;KAChG,CAAC,CACH,CAAA;AACL,CAAC","sourcesContent":["/*\n * ์ฝ๋Š” ์‚ฌ๋žŒ์˜ ๊ด€๋ฌธ โ€” ADR-0073 ํŒ์ • 1ยท2 ๋ฅผ ์š”์ฒญ ํ•˜๋‚˜์— ์„ธ์šด๋‹ค.\n *\n * ๋ฆฌ์กธ๋ฒ„๋Š” ์ด๊ฒƒ์„ ํ•œ ๋ฒˆ ๋งŒ๋“ค๊ณ  ํ–‰ ๋ชฉ๋ก์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค. ๋ฌธ ํŒ์ •์€ (์‚ฌ๋žŒ, ๋ฌธ) ๋ณ„๋กœ ํ•œ ๋ฒˆ๋งŒ ์ผ์–ด๋‚˜๊ณ ,\n * ์„ธ์…˜์ด private ์ธ์ง€๋„ ์—ฌ๊ธฐ์„œ ํ•จ๊ป˜ ๋‹ตํ•œ๋‹ค. ๋ฆฌ์กธ๋ฒ„๋งˆ๋‹ค ๊ฐ™์€ ํŒ์ •์„ ๋‹ค์‹œ ์ ์œผ๋ฉด ํ•œ ๊ณณ์ด ๋น ์ง„ ๋‚ \n * ๊ทธ ๋ฌธ์œผ๋กœ ์ƒŒ๋‹ค.\n */\nimport { getAllToolSpecs } from '@things-factory/ai-client-base'\nimport { User } from '@things-factory/auth-base'\n\nimport { doorCheck } from './door-access.js'\nimport { doorGate, redactByDoors, type DoorReadableRow } from './message-doors.js'\nimport { requestTranslator } from '../request-translator.js'\n\n/** ์ด ์š”์ฒญ์—์„œ ํ–‰์„ ๊ฐ€๋ ค ๋‚ด๋ณด๋‚ด๋Š” ํ•จ์ˆ˜. ๊ฐ™์€ ์š”์ฒญ ์•ˆ์—์„œ ๊ณ„์† ์“ด๋‹ค. */\nexport function readerGate(context: any): <T extends DoorReadableRow>(rows: readonly T[]) => Promise<T[]> {\n const { domain, user } = context.state\n const t = requestTranslator(context)\n\n const gate = doorGate(\n doorCheck({\n specs: getAllToolSpecs(),\n hasPrivilege: privilege => User.hasPrivilege(privilege.privilege, privilege.category, domain, user)\n })\n )\n\n /*\n * ๋ฌธ ์ด๋ฆ„(`Query.productionReports`)์€ ์ด ๋ฌธ์žฅ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.\n *\n * ํ˜„์žฅ์—์„œ ์ฝ๋Š” ์‚ฌ๋žŒ์—๊ฒŒ GraphQL ์ด๋ฆ„์€ ์•„๋ฌด ๋ง๋„ ์•„๋‹ˆ๋‹ค. ๋ฌธ ์ด๋ฆ„์„ ์‚ฌ๋žŒ์˜ ๋ง๋กœ ๋ฐ”๊ฟ€ ํ‘œ๋Š” AUTH ์˜\n * ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ์™€ ๊ฐ™์€ ์ž๋ฆฌ์—์„œ ๋‚˜์™€์•ผ ํ•˜๊ณ , ๊ทธ๊ฒƒ์€ ์•„์ง ์—†๋‹ค(ADR-0072 ๊ฒฐ์ • 2 (b)). ์—†๋Š” ํ‘œ๋ฅผ ํ‰๋‚ด\n * ๋‚ด๋А๋‹ˆ ๋ฌธ์žฅ์€ ์‚ฌ์‹ค๋งŒ ๋งํ•˜๊ณ , ๋ชฉ๋ก์€ `ChatMessage.doors` ๋กœ ๋”ฐ๋กœ ๋‚˜๊ฐ„๋‹ค โ€” ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌด์—‡์„\n * ์ฒญํ• ์ง€ ์•Œ์•„์•ผ ํ•˜๋Š” ํ™”๋ฉด์€ ๊ฑฐ๊ธฐ์„œ ์ฝ๋Š”๋‹ค. ๋ฌธ๊ตฌ๋Š” UX ๋ ˆ์ธ์ด ์ •ํ•œ๋‹ค.\n */\n return rows =>\n redactByDoors(rows, gate, () =>\n t('ai-assistant.text.answer-needs-permission', {\n /* ๊ตฌ๋…์—๋Š” ์š”์ฒญ ๋ฒˆ์—ญ๊ธฐ๊ฐ€ ์—†๋‹ค โ€” `request-translator.ts` ์ฐธ์กฐ. */\n defaultValue: 'you do not have permission to see this answer. ask an administrator for access.'\n })\n )\n}\n"]}
1
+ {"version":3,"file":"reader-gate.js","sourceRoot":"","sources":["../../../server/service/chat-message/reader-gate.ts"],"names":[],"mappings":";;AAcA,gCA6BC;AA3CD;;;;;;GAMG;AACH,yDAA6E;AAE7E,qDAA4C;AAC5C,yDAAkF;AAClF,oEAA4D;AAE5D,6CAA6C;AAC7C,SAAgB,UAAU,CAAC,OAAY;IACrC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IACtC,MAAM,CAAC,GAAG,IAAA,yCAAiB,EAAC,OAAO,CAAC,CAAA;IAEpC,MAAM,EAAE,QAAQ,EAAE,oBAAoB,EAAE,GAAG,OAAO,CAAC,KAAK,CAAA;IAExD,MAAM,IAAI,GAAG,IAAA,2BAAQ;IACnB,sDAAsD;IACtD,IAAA,0BAAS,EAAC;QACR,MAAM,EAAE,4BAAgB;QACxB,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC,IAAA,2BAAe,EAAC,SAAgB,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,oBAAoB,CAAC;KACtG,CAAC,CACH,CAAA;IAED;;;;;;;OAOG;IACH,OAAO,IAAI,CAAC,EAAE,CACZ,IAAA,gCAAa,EAAC,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,CAC7B,CAAC,CAAC,2CAA2C,EAAE;QAC7C,mDAAmD;QACnD,YAAY,EAAE,iFAAiF;KAChG,CAAC,CACH,CAAA;AACL,CAAC","sourcesContent":["/*\n * ์ฝ๋Š” ์‚ฌ๋žŒ์˜ ๊ด€๋ฌธ โ€” ADR-0073 ํŒ์ • 1ยท2 ๋ฅผ ์š”์ฒญ ํ•˜๋‚˜์— ์„ธ์šด๋‹ค.\n *\n * ๋ฆฌ์กธ๋ฒ„๋Š” ์ด๊ฒƒ์„ ํ•œ ๋ฒˆ ๋งŒ๋“ค๊ณ  ํ–‰ ๋ชฉ๋ก์„ ํ†ต๊ณผ์‹œํ‚จ๋‹ค. ๋ฌธ ํŒ์ •์€ (์‚ฌ๋žŒ, ๋ฌธ) ๋ณ„๋กœ ํ•œ ๋ฒˆ๋งŒ ์ผ์–ด๋‚˜๊ณ ,\n * ์„ธ์…˜์ด private ์ธ์ง€๋„ ์—ฌ๊ธฐ์„œ ํ•จ๊ป˜ ๋‹ตํ•œ๋‹ค. ๋ฆฌ์กธ๋ฒ„๋งˆ๋‹ค ๊ฐ™์€ ํŒ์ •์„ ๋‹ค์‹œ ์ ์œผ๋ฉด ํ•œ ๊ณณ์ด ๋น ์ง„ ๋‚ \n * ๊ทธ ๋ฌธ์œผ๋กœ ์ƒŒ๋‹ค.\n */\nimport { checkPermission, privilegeOfField } from '@things-factory/auth-base'\n\nimport { doorCheck } from './door-access.js'\nimport { doorGate, redactByDoors, type DoorReadableRow } from './message-doors.js'\nimport { requestTranslator } from '../request-translator.js'\n\n/** ์ด ์š”์ฒญ์—์„œ ํ–‰์„ ๊ฐ€๋ ค ๋‚ด๋ณด๋‚ด๋Š” ํ•จ์ˆ˜. ๊ฐ™์€ ์š”์ฒญ ์•ˆ์—์„œ ๊ณ„์† ์“ด๋‹ค. */\nexport function readerGate(context: any): <T extends DoorReadableRow>(rows: readonly T[]) => Promise<T[]> {\n const { domain, user } = context.state\n const t = requestTranslator(context)\n\n const { unsafeIP, prohibitedPrivileges } = context.state\n\n const gate = doorGate(\n /* ๋ฌธ์—์„œ ์“ฐ๋Š” ํŒ์ •์„ ๊ทธ๋Œ€๋กœ ์“ด๋‹ค โ€” ๊ฐ™์€ ์ธ์ž, ๊ฐ™์€ ํ•จ์ˆ˜(ADR-0072 ๊ฒฐ์ • 5). */\n doorCheck({\n lookup: privilegeOfField,\n permits: fieldGate => checkPermission(fieldGate as any, user, domain, unsafeIP, prohibitedPrivileges)\n })\n )\n\n /*\n * ๋ฌธ ์ด๋ฆ„(`Query.productionReports`)์€ ์ด ๋ฌธ์žฅ์— ๋„ฃ์ง€ ์•Š๋Š”๋‹ค.\n *\n * ํ˜„์žฅ์—์„œ ์ฝ๋Š” ์‚ฌ๋žŒ์—๊ฒŒ GraphQL ์ด๋ฆ„์€ ์•„๋ฌด ๋ง๋„ ์•„๋‹ˆ๋‹ค. ๋ฌธ ์ด๋ฆ„์„ ์‚ฌ๋žŒ์˜ ๋ง๋กœ ๋ฐ”๊ฟ€ ํ‘œ๋Š” AUTH ์˜\n * ํ•„๋“œโ†’๊ถŒํ•œ ํ‘œ์™€ ๊ฐ™์€ ์ž๋ฆฌ์—์„œ ๋‚˜์™€์•ผ ํ•˜๊ณ , ๊ทธ๊ฒƒ์€ ์•„์ง ์—†๋‹ค(ADR-0072 ๊ฒฐ์ • 2 (b)). ์—†๋Š” ํ‘œ๋ฅผ ํ‰๋‚ด\n * ๋‚ด๋А๋‹ˆ ๋ฌธ์žฅ์€ ์‚ฌ์‹ค๋งŒ ๋งํ•˜๊ณ , ๋ชฉ๋ก์€ `ChatMessage.doors` ๋กœ ๋”ฐ๋กœ ๋‚˜๊ฐ„๋‹ค โ€” ๊ด€๋ฆฌ์ž์—๊ฒŒ ๋ฌด์—‡์„\n * ์ฒญํ• ์ง€ ์•Œ์•„์•ผ ํ•˜๋Š” ํ™”๋ฉด์€ ๊ฑฐ๊ธฐ์„œ ์ฝ๋Š”๋‹ค. ๋ฌธ๊ตฌ๋Š” UX ๋ ˆ์ธ์ด ์ •ํ•œ๋‹ค.\n */\n return rows =>\n redactByDoors(rows, gate, () =>\n t('ai-assistant.text.answer-needs-permission', {\n /* ๊ตฌ๋…์—๋Š” ์š”์ฒญ ๋ฒˆ์—ญ๊ธฐ๊ฐ€ ์—†๋‹ค โ€” `request-translator.ts` ์ฐธ์กฐ. */\n defaultValue: 'you do not have permission to see this answer. ask an administrator for access.'\n })\n )\n}\n"]}
@@ -40,6 +40,18 @@ export declare class ChatSession {
40
40
  * ๋“œ๋ผ์ด๋ฒ„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•  ๋•Œ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•œ๋‹ค(activityPatch).
41
41
  */
42
42
  lastMessageAt?: Date;
43
+ /**
44
+ * ์–ด๋А ๋Œ€ํ™”์ธ๊ฐ€ โ€” `twin.board` ยท `figure.catalog` ๊ฐ™์€ ๋ชจ๋“œ์˜ kind (ADR-0072 ๊ฒฐ์ • 3).
45
+ *
46
+ * **์„œ๋ฒ„๊ฐ€ ์ ๋Š”๋‹ค.** ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ด๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋‹ค โ€” ๋“ฑ๋ก๋œ ๋ชจ๋“œ์—์„œ ๋‚˜์˜จ ๊ฒƒ๋งŒ ์ ํžŒ๋‹ค(`addressee`
47
+ * ์™€ ๊ฐ™์€ ๊ฒฐ). ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์„ธ์…˜์ด ์ž๊ธฐ๊ฐ€ ์–ด๋А ๋Œ€ํ™”์ธ์ง€ ์•Œ์•„์•ผ
48
+ * ํ•œ๋‹ค. ๋ชฐ๋ž์„ ๋•Œ ํ™”๋ฉด์—์„œ ์ด๋ ‡๊ฒŒ ๋‚˜์™”๋‹ค: ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟ”๋„ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„, ใ€Œ๋„ํ˜• ์ฐพ๊ธฐใ€๋กœ
49
+ * ๋ฐ”๊พผ ๋’ค ๋ณด๋‚ธ ๋ง์ด ์•ž์˜ ๊ณ ์น˜๊ธฐ ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).
50
+ *
51
+ * ๊ฐ’์ด ์—†๋Š” ์„ธ์…˜์€ ์ด ์นธ์ด ์„œ๊ธฐ ์ „์— ๋‚œ ๊ฒƒ์ด๋‹ค. ๋‹ค์Œ ํ„ด์— ์š”์ฒญํ•œ kind ๋กœ ํ•œ ๋ฒˆ ์ฐ๊ณ , ์ฐํžŒ ๋’ค์—๋Š”
52
+ * ใ€Œ์š”์ฒญํ•œ ๋ชจ๋“œ์™€ ๋‹ค๋ฅด๋ฉด ๊ฑฐ์ ˆใ€ ํ•˜๋‚˜๋งŒ ๋‚จ๋Š”๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.
53
+ */
54
+ kind?: string;
43
55
  /**
44
56
  * ์ด ๋Œ€ํ™”๋ฅผ ๋„๋ฉ”์ธ ์•ˆ ๋ˆ„๊ตฌ๋‚˜ ์ฝ๋Š”๊ฐ€ โ€” ADR-0073 ํŒ์ • 2.
45
57
  *
@@ -91,22 +91,23 @@ tslib_1.__decorate([
91
91
  (0, type_graphql_1.Field)({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison โ€” avoids per-session aggregation across the supported DB drivers).' }),
92
92
  tslib_1.__metadata("design:type", Date
93
93
  /**
94
- * ์ด ๋Œ€ํ™”๋ฅผ ๋„๋ฉ”์ธ ์•ˆ ๋ˆ„๊ตฌ๋‚˜ ์ฝ๋Š”๊ฐ€ โ€” ADR-0073 ํŒ์ • 2.
94
+ * ์–ด๋А ๋Œ€ํ™”์ธ๊ฐ€ โ€” `twin.board` ยท `figure.catalog` ๊ฐ™์€ ๋ชจ๋“œ์˜ kind (ADR-0072 ๊ฒฐ์ • 3).
95
95
  *
96
- * `open` ๋„๋ฉ”์ธ ์•ˆ์ด๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋“ค์–ด์™€ ์ฝ๊ณ  ๋งํ•œ๋‹ค. ์ดˆ๋Œ€ ์ ˆ์ฐจ๋Š” ์—†๋‹ค.
97
- * `private` ์ฐธ์—ฌ์ž๋งŒ ์ฝ๋Š”๋‹ค.
96
+ * **์„œ๋ฒ„๊ฐ€ ์ ๋Š”๋‹ค.** ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ด๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋‹ค โ€” ๋“ฑ๋ก๋œ ๋ชจ๋“œ์—์„œ ๋‚˜์˜จ ๊ฒƒ๋งŒ ์ ํžŒ๋‹ค(`addressee`
97
+ * ์™€ ๊ฐ™์€ ๊ฒฐ). ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์„ธ์…˜์ด ์ž๊ธฐ๊ฐ€ ์–ด๋А ๋Œ€ํ™”์ธ์ง€ ์•Œ์•„์•ผ
98
+ * ํ•œ๋‹ค. ๋ชฐ๋ž์„ ๋•Œ ํ™”๋ฉด์—์„œ ์ด๋ ‡๊ฒŒ ๋‚˜์™”๋‹ค: ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟ”๋„ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„, ใ€Œ๋„ํ˜• ์ฐพ๊ธฐใ€๋กœ
99
+ * ๋ฐ”๊พผ ๋’ค ๋ณด๋‚ธ ๋ง์ด ์•ž์˜ ๊ณ ์น˜๊ธฐ ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).
98
100
  *
99
- * ์•ต์ปค๊ฐ€ ์žˆ๋Š” ์„ธ์…˜(space ยท board ยท station)์€ ๊ณต์œ ๋œ ๋ฌด์—‡์„ ๋†“๊ณ  ํ•˜๋Š” ์ด์•ผ๊ธฐ์ด๋ฏ€๋กœ `open` ์œผ๋กœ
100
- * ์‹œ์ž‘ํ•œ๋‹ค. ์•ต์ปค๊ฐ€ ์—†๋Š” ์„ธ์…˜๊ณผ **์˜ฎ๊ฒจ ์ ์–ด ๋งŒ๋“  ์„ธ์…˜**์€ `private` ์œผ๋กœ ์‹œ์ž‘ํ•œ๋‹ค โ€” ๊ฐ€๋ฒผ์šด ์งˆ๋ฌธ์„
101
- * ์Šน๊ฒฉํ•˜๋ฉด์„œ ์•ž์„  ๋Œ€ํ™”๊ฐ€ ์ƒˆ ์„ธ์…˜์— ์˜ฎ๊ฒจ ์ ํžˆ๋Š”๋ฐ(`appendSessionTurns`), ๊ทธ๊ฒƒ์ด ๊ณต๊ฐœ๋กœ ํƒœ์–ด๋‚˜๋ฉด
102
- * ์‚ฌ์ ์œผ๋กœ ์˜ค๊ฐ„ ์›๋ฌธ์ด ์†Œ๊ธ‰ํ•ด์„œ ๊ณต๊ฐœ๋œ๋‹ค.
103
- *
104
- * owner ๊ฐ€ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ๊ณต๊ฐœ๋กœ ๋ฐ”๊พธ๊ธฐ ์ „์— ํ™”๋ฉด์ด ํ•œ ์ค„์„ ๋จผ์ € ๋ณด์ธ๋‹ค.
105
- *
106
- * open ์ด์–ด๋„ ๋‹ต ํ–‰์˜ ๋ฌธ ํŒ์ •(ADR-0073 ํŒ์ • 1)์€ ๊ทธ๋Œ€๋กœ๋‹ค โ€” ๋ฐฉ์— ๋“ค์–ด์˜จ ๊ฒƒ๊ณผ ๊ฐ’์„ ์ฝ๋Š” ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค.
101
+ * ๊ฐ’์ด ์—†๋Š” ์„ธ์…˜์€ ์ด ์นธ์ด ์„œ๊ธฐ ์ „์— ๋‚œ ๊ฒƒ์ด๋‹ค. ๋‹ค์Œ ํ„ด์— ์š”์ฒญํ•œ kind ๋กœ ํ•œ ๋ฒˆ ์ฐ๊ณ , ์ฐํžŒ ๋’ค์—๋Š”
102
+ * ใ€Œ์š”์ฒญํ•œ ๋ชจ๋“œ์™€ ๋‹ค๋ฅด๋ฉด ๊ฑฐ์ ˆใ€ ํ•˜๋‚˜๋งŒ ๋‚จ๋Š”๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.
107
103
  */
108
104
  )
109
105
  ], ChatSession.prototype, "lastMessageAt", void 0);
106
+ tslib_1.__decorate([
107
+ (0, typeorm_1.Column)({ type: 'varchar', length: 64, nullable: true }),
108
+ (0, type_graphql_1.Field)({ nullable: true, description: "Which conversation this session is โ€” the registered mode kind. Written by the server, never sent by the client." }),
109
+ tslib_1.__metadata("design:type", String)
110
+ ], ChatSession.prototype, "kind", void 0);
110
111
  tslib_1.__decorate([
111
112
  (0, typeorm_1.Column)({ type: 'varchar', length: 16, default: 'open' }),
112
113
  (0, type_graphql_1.Field)({ description: "Who may read this conversation: 'open' (anyone in the domain) or 'private' (participants only)." }),
@@ -1 +1 @@
1
- {"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":";;;;AAAA;;;;;;;;;;;;;;GAcG;AACH,qCAWgB;AAChB,+CAAoD;AAEpD,iDAA8C;AAC9C,yDAAgD;AAChD,6CAA4C;AAE5C,qEAA6D;AAE7D,MAAM,SAAS,GAAG,YAAM,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;AAC7C,MAAM,aAAa,GAAG,SAAS,CAAC,IAAI,CAAA;AAW7B,IAAM,WAAW,GAAjB,MAAM,WAAW;CA+KvB,CAAA;AA/KY,kCAAW;AAGb;IAFR,IAAA,gCAAsB,EAAC,MAAM,CAAC;IAC9B,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,iBAAE,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;uCAClB;AAIpB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,CAAC;IACzB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,cAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACjC,cAAM;2CAAA;AAGf;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,MAAM,CAAC;;6CACpC;AAYjB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,gMAAgM;KACnM,CAAC;;4CACc;AAehB;IANC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC;QACL,QAAQ,EAAE,IAAI;QACd,WAAW,EACT,gMAAgM;KACnM,CAAC;;+CACiB;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gEAAgE,EAAE,CAAC;;6CACxF;AAUjB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,+DAA+D,EAAE,CAAC;;yCAC3F;AAIb;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gCAAgC,EAAE,CAAC;sCAC7E,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAIlB;IAFC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,CAAC;IACvB,IAAA,oBAAK,EAAC,IAAI,CAAC,EAAE,CAAC,gBAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,qCAAqC,EAAE,CAAC;sCAClF,gBAAI;4CAAA;AAGd;IADC,IAAA,oBAAU,EAAC,CAAC,OAAoB,EAAE,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;8CACpC;AAYlB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,wKAAwK,EAAE,CAAC;sCACjM,IAAI;IAEpB;;;;;;;;;;;;;;OAcG;;kDAhBiB;AAmBpB;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC;IACxD,IAAA,oBAAK,EAAC,EAAE,WAAW,EAAE,iGAAiG,EAAE,CAAC;;+CACvG;AAKnB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;IACvC,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,gGAAgG,EAAE,CAAC;;uDAC9G;AAW3B;IAFC,IAAA,gBAAM,EAAC,EAAE,IAAI,EAAE,cAAc,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAChD,IAAA,oBAAK,EAAC,GAAG,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,yGAAyG,EAAE,CAAC;;qDACvI;AAe3B;IAbC,IAAA,gBAAM,EAAC;QACN,QAAQ,EAAE,IAAI;QACd,IAAI,EACF,aAAa,IAAI,OAAO,IAAI,aAAa,IAAI,SAAS;YACpD,CAAC,CAAC,UAAU;YACZ,CAAC,CAAC,aAAa,IAAI,QAAQ;gBACzB,CAAC,CAAC,MAAM;gBACR,CAAC,CAAC,aAAa,IAAI,OAAO;oBACxB,CAAC,CAAC,UAAU;oBACZ,CAAC,CAAC,MAAM;QAChB,MAAM,EAAE,aAAa,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS;KACrD,CAAC;IACD,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,0DAA0D,EAAE,CAAC;;gDAC/E;AAWpB;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,6HAA6H,EAAE,CAAC;;yDACzI;AAK7B;IAFC,IAAA,gBAAM,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;IAC1B,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;;+CACP;AAInB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAIhB;IAFC,IAAA,0BAAgB,GAAE;IAClB,IAAA,oBAAK,EAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC;sCACd,IAAI;8CAAA;AAGhB;IADC,IAAA,0BAAgB,GAAE;sCACP,IAAI;IAEhB,oDAAoD;;8CAFpC;AAIhB;IADC,IAAA,mBAAS,EAAC,IAAI,CAAC,EAAE,CAAC,6BAAW,EAAE,OAAO,CAAC,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC;;6CACnC;sBA/Jb,WAAW;IATvB,IAAA,gBAAM,GAAE;IACT,qEAAqE;IACrE,kDAAkD;;IACjD,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;IACxF,uEAAuE;;IACtE,IAAA,eAAK,EAAC,mBAAmB,EAAE,CAAC,OAAoB,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,UAAU,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC5G,IAAA,yBAAU,EAAC;QACV,WAAW,EAAE,2HAA2H;KACzI,CAAC;GACW,WAAW,CA+KvB","sourcesContent":["/**\n * ChatSession โ€” Board ์™€ ๊ฒฐํ•ฉ๋˜๋Š” AI ํ˜‘๋ ฅ ์„ธ์…˜.\n *\n * ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜ ๊ฐ€๋Šฅ โ€” DB ๋ ˆ๋ฒจ unique ์ œ์•ฝ ์—†์Œ.\n *\n * ํ–ฅํ›„ ํญ์ฆํ•  ์„ธ์…˜ ๋ถ„ํ™” ์ฐจ์› (์˜ˆ: thread / ์‚ฌ์šฉ์ž๋ณ„ / ์ปจํ…์ŠคํŠธ๋ณ„ design vs ops /\n * archived / forked from clone / agent๋ณ„) ์„ ๋ฏธ๋ฆฌ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ ์ž์ฒด๋Š” 1:N\n * ์œผ๋กœ ์—ด์–ด๋‘ . ๋‹จ์ผ ์„ธ์…˜ ์ •์ฑ… (ํ˜„์žฌ UX) ์€ app ๋ ˆ๋ฒจ idempotent ๋กœ์ง โ€” `startBoardAISession`\n * ์ด boardId ๋งค์นญ ์‹œ ๊ธฐ์กด ์„ธ์…˜ ๋ฐ˜ํ™˜, `chatSessionByBoard` ๊ฐ€ ์ฒซ ๋งค์นญ ๋ฐ˜ํ™˜.\n *\n * ๋ฏธ๋ž˜์— ๋‹ค์ค‘ ์„ธ์…˜ ํ™œ์„ฑํ™” ์‹œ discriminator ์ปฌ๋Ÿผ (kind / name / threadId / creatorId ๋“ฑ)\n * ์„ ๊ทธ๋•Œ ์ถ”๊ฐ€. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ€๋‹ด ์ตœ์†Œํ™” โ€” ํ˜„์žฌ๋Š” unique ๋งŒ ํ’€์–ด ์ž๋ฆฌ ๋งˆ๋ จ.\n *\n * board-service ์˜ Board ๊ฐ€ owning side ๊ฐ€ ๋  ์˜ˆ์ • (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// ์ธ๋ฑ์Šค๋Š” ์กฐํšŒ ์„ฑ๋Šฅ์šฉ์œผ๋กœ ์œ ์ง€ (chatSessionByBoard ๊ฐ€ [domain, boardId] ๋กœ ์ž์ฃผ ์ฟผ๋ฆฌ).\n// unique ์ œ์•ฝ์€ ์˜๋„์ ์œผ๋กœ ์ œ๊ฑฐ โ€” ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜์„ ๋ฏธ๋ž˜์— ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•จ.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// ์•ต์ปค ์กฐํšŒ์šฉ โ€” chatSessionsByAnchor ๊ฐ€ [domain, anchorType, anchorId] ๋กœ ์งˆ์˜.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several โ€” threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id ์™€ ์—ฐ๊ฒฐ. Board ๊ฐ€ owning side ๊ฐ€ ๋  ์˜ˆ์ •. 1:N โ€” ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜ ๊ฐ€๋Šฅ.\n *\n * **์•ต์ปค ์ผ๋ฐ˜ํ™” ์ดํ›„์—๋„ ์œ ์ง€๋˜๋Š” ํ•˜์œ„ํ˜ธํ™˜ ์ปฌ๋Ÿผ**์ด๋‹ค(์ œ๊ฑฐํ•˜์ง€ ๋ง ๊ฒƒ). ์•ต์ปค๊ฐ€ board ์ธ ์„ธ์…˜์€\n * anchorId ์™€ ํ•จ๊ป˜ ์ด ์ปฌ๋Ÿผ์—๋„ ๊ฐ™์€ ๊ฐ’์„ ์“ด๋‹ค(dual-write) โ€” ๊ธฐ์กด ์กฐํšŒ/์ธ๋ฑ์Šค/์†Œ๋น„์ž๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility โ€” board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * ๋Œ€ํ™”๊ฐ€ ๋งค๋‹ฌ๋ฆฐ ๋Œ€์ƒ์˜ **์ข…๋ฅ˜**. ์„ธ์…˜์€ ๋ณด๋“œ์—๋งŒ ๋งค๋‹ฌ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€”\n * ์ €์ž‘ ๋Œ€ํ™”๋Š” ๋ณด๋“œ์—, ์šด์˜ ๋Œ€ํ™”๋Š” ๊ณต๊ฐ„(์‚ฌ์ดํŠธ)์— ๋งค๋‹ฌ๋ฆฐ๋‹ค. ์•ต์ปค๊ฐ€ ๋Œ€ํ™”์˜ ์„ฑ๊ฒฉ๊ณผ ์ด๋ ฅ ๊ฒฝ๊ณ„๋ฅผ ์ •ํ•œ๋‹ค.\n *\n * 'board' | 'space' | 'instance' | 'none'. ๋ฏธ์„ค์ •(๊ธฐ์กด ๋ฐ์ดํ„ฐ)์€ boardId ๊ฐ€ ์žˆ์œผ๋ฉด board ๋กœ ์ฝ๋Š”๋‹ค โ€”\n * ๋ฐฑํ•„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์—†์ด lazy ํ•˜์œ„ํ˜ธํ™˜(์ปฌ๋Ÿผ ์ถ”๊ฐ€๋งŒ).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'space' | 'instance' | 'domain' | 'none'. Absent on legacy rows โ€” treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** ์•ต์ปค ๋Œ€์ƒ์˜ id(๋ณด๋“œ id ยท ๊ณต๊ฐ„ id ยท ์ธ์Šคํ„ด์Šค id). anchorType ๊ณผ ํ•จ๊ป˜ ์˜๋ฏธ๋ฅผ ๊ฐ–๋Š”๋‹ค. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * ์„ธ์…˜์˜ ์‚ฌ์šฉ์ž ์ •์˜ ์ด๋ฆ„ (ํƒญ ๋ ˆ์ด๋ธ” / ์„ธ์…˜ ์‹๋ณ„ ํ‘œ์‹œ์šฉ).\n *\n * nullable โ€” ๋ฏธ์„ค์ • ์‹œ UI ๊ฐ€ fallback ๋ผ๋ฒจ (\"์„ธ์…˜ N\" ๋˜๋Š” createdAt ๊ธฐ๋ฐ˜) ์‚ฌ์šฉ. ์‚ฌ์šฉ์ž๊ฐ€\n * ๋‹ค์ค‘ ์„ธ์…˜ ์šด์˜ ์‹œ ์˜๋ฏธ ๋ถ€์—ฌ (์˜ˆ: \"์ดˆ๊ธฐ ์„ค๊ณ„\", \"์šด์˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\", \"์‚ฌ์šฉ์žA ์˜ ์‚ฌ์  ์„ธ์…˜\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** ํ† ํฐ ์ ˆ๊ฐ์šฉ โ€” ์˜ค๋ž˜๋œ ๋ฉ”์‹œ์ง€๋ฅผ LLM ์œผ๋กœ ์••์ถ•ํ•œ ์š”์•ฝ. Phase 2 ์—์„œ ์ž‘์„ฑ. */\n /**\n * ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ ์‹œ๊ฐ โ€” **๋น„์ •๊ทœํ™”**. ๋ชฉ๋ก ์ •๋ ฌ(์ตœ๊ทผ ํ™œ๋™์ˆœ)๊ณผ ์•ˆ ์ฝ์Œ ํŒ์ •์˜ ๊ธฐ์ค€.\n *\n * ์™œ ๋น„์ •๊ทœํ™”์ธ๊ฐ€: ์„ธ์…˜๋งˆ๋‹ค ๋ฉ”์‹œ์ง€๋ฅผ ์ง‘๊ณ„ํ•˜๋ฉด(MAX/COUNT + group by) ๋ชฉ๋ก ํ•œ ๋ฒˆ์— ์ง‘๊ณ„ ์ฟผ๋ฆฌ๊ฐ€ ๋ถ™๊ณ ,\n * ์ด ํ”„๋ ˆ์ž„์›Œํฌ๋Š” 5๊ฐœ DB ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ง€์›ํ•ด์•ผ ํ•œ๋‹ค. ์„ธ์…˜ ์ปฌ๋Ÿผ ํ•˜๋‚˜๋กœ ๋‘๋ฉด ์ •๋ ฌยท๋น„๊ต๊ฐ€ ๊ฐ’์‹ธ๊ฒŒ ๋๋‚˜๊ณ \n * ๋“œ๋ผ์ด๋ฒ„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•  ๋•Œ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•œ๋‹ค(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison โ€” avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * ์ด ๋Œ€ํ™”๋ฅผ ๋„๋ฉ”์ธ ์•ˆ ๋ˆ„๊ตฌ๋‚˜ ์ฝ๋Š”๊ฐ€ โ€” ADR-0073 ํŒ์ • 2.\n *\n * `open` ๋„๋ฉ”์ธ ์•ˆ์ด๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋“ค์–ด์™€ ์ฝ๊ณ  ๋งํ•œ๋‹ค. ์ดˆ๋Œ€ ์ ˆ์ฐจ๋Š” ์—†๋‹ค.\n * `private` ์ฐธ์—ฌ์ž๋งŒ ์ฝ๋Š”๋‹ค.\n *\n * ์•ต์ปค๊ฐ€ ์žˆ๋Š” ์„ธ์…˜(space ยท board ยท station)์€ ๊ณต์œ ๋œ ๋ฌด์—‡์„ ๋†“๊ณ  ํ•˜๋Š” ์ด์•ผ๊ธฐ์ด๋ฏ€๋กœ `open` ์œผ๋กœ\n * ์‹œ์ž‘ํ•œ๋‹ค. ์•ต์ปค๊ฐ€ ์—†๋Š” ์„ธ์…˜๊ณผ **์˜ฎ๊ฒจ ์ ์–ด ๋งŒ๋“  ์„ธ์…˜**์€ `private` ์œผ๋กœ ์‹œ์ž‘ํ•œ๋‹ค โ€” ๊ฐ€๋ฒผ์šด ์งˆ๋ฌธ์„\n * ์Šน๊ฒฉํ•˜๋ฉด์„œ ์•ž์„  ๋Œ€ํ™”๊ฐ€ ์ƒˆ ์„ธ์…˜์— ์˜ฎ๊ฒจ ์ ํžˆ๋Š”๋ฐ(`appendSessionTurns`), ๊ทธ๊ฒƒ์ด ๊ณต๊ฐœ๋กœ ํƒœ์–ด๋‚˜๋ฉด\n * ์‚ฌ์ ์œผ๋กœ ์˜ค๊ฐ„ ์›๋ฌธ์ด ์†Œ๊ธ‰ํ•ด์„œ ๊ณต๊ฐœ๋œ๋‹ค.\n *\n * owner ๊ฐ€ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ๊ณต๊ฐœ๋กœ ๋ฐ”๊พธ๊ธฐ ์ „์— ํ™”๋ฉด์ด ํ•œ ์ค„์„ ๋จผ์ € ๋ณด์ธ๋‹ค.\n *\n * open ์ด์–ด๋„ ๋‹ต ํ–‰์˜ ๋ฌธ ํŒ์ •(ADR-0073 ํŒ์ • 1)์€ ๊ทธ๋Œ€๋กœ๋‹ค โ€” ๋ฐฉ์— ๋“ค์–ด์˜จ ๊ฒƒ๊ณผ ๊ฐ’์„ ์ฝ๋Š” ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค.\n */\n @Column({ type: 'varchar', length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain) or 'private' (participants only).\" })\n visibility!: string\n\n /** ๋ชฉ๋ก์— ๋ณด์—ฌ์ค„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ โ€” ๋ชฉ๋ก์—์„œ ๋ฉ”์‹œ์ง€ ํ…Œ์ด๋ธ”์„ ๋‹ค์‹œ ์ฝ์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์งง๊ฒŒ ์ €์žฅ. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * ๊ทธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ง€๋‚˜์˜จ ๋ฌธ โ€” ADR-0073 ํŒ์ • 1.\n *\n * ๋ชฉ๋ก์€ ๋‹ต์„ **๋‹ค์‹œ ์ฝ๋Š” ๋‘ ๋ฒˆ์งธ ๊ธธ**์ด๋‹ค. ๋ฉ”์‹œ์ง€ ํ™”๋ฉด์—์„œ๋งŒ ๊ฐ€๋ฆฌ๋ฉด ๊ฐ™์€ ๋ฌธ์žฅ์ด ๋ชฉ๋ก์— ๊ทธ๋Œ€๋กœ ๋œฌ๋‹ค.\n * ๋ฌธ์ด ๋‘˜์ด๊ณ  ํ•œ์ชฝ์ด ๋А์Šจํ•˜๋ฉด ๋А์Šจํ•œ ์ชฝ์ด ์ด๊ธด๋‹ค(ADR-0061 ๊ฒฐ์ • 1). ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ €์žฅํ•  ๋•Œ ๊ทธ ํ–‰์˜ ๋ฌธ์„\n * ํ•จ๊ป˜ ์˜ฎ๊ฒจ ์ ๊ณ , ๋ชฉ๋ก๋„ ๊ฐ™์€ ๊ด€๋ฌธ์„ ์ง€๋‚œ๋‹ค.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from โ€” the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` ๊ฐ€ **์–ด๋””๊นŒ์ง€ ๋ฎ๋Š”์ง€** โ€” ์ด ๋ฉ”์‹œ์ง€๊นŒ์ง€ ์š”์•ฝ์— ๋ฐ˜์˜๋๋‹ค.\n *\n * ์ด๊ฒŒ ์—†์œผ๋ฉด ๋ˆ„์  ์š”์•ฝ์„ ํ•  ์ˆ˜ ์—†๋‹ค: ๋งค ํ„ด ์ „์ฒด๋ฅผ ๋‹ค์‹œ ์š”์•ฝํ•˜๊ฑฐ๋‚˜(๋น„์šฉ) ์ด๋ฏธ ์š”์•ฝํ•œ ๊ตฌ๊ฐ„์„\n * ๋˜ ์ด์–ด ๋ถ™์ธ๋‹ค(์ค‘๋ณต). ์ด๋ ฅ์—์„œ ์ด id ๋ฅผ ๋ชป ์ฐพ์œผ๋ฉด(์‚ญ์ œยทํŽธ์ง‘) ๋ฎ์€ ๋ฒ”์œ„๋ฅผ ์•Œ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ\n * ์ „๋ถ€ ๋ฏธ๋ฐ˜์˜์œผ๋กœ ๋ณด๊ณ  ๋‹ค์‹œ ์š”์•ฝํ•œ๋‹ค โ€” ๋น ๋œจ๋ฆฌ๋Š” ์ชฝ๋ณด๋‹ค ์•ˆ์ „ํ•˜๋‹ค.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary โ€” enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** ์‚ฌ์šฉ ๋ชจ๋ธ ์‹๋ณ„ โ€” 'anthropic:claude-sonnet-4-6' ๋“ฑ */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // โ”€โ”€ ๊ด€๊ณ„ (TypeORM ๋งŒ, GraphQL ๋…ธ์ถœ์€ ๋ณ„๋„ query ํ†ตํ•ด ํŽ˜์ด์ง•) โ”€โ”€\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * โ”€โ”€ ๋ณด๋“œ ํŒจ์น˜ ๊ด€๊ณ„๋ฅผ ๋Š์—ˆ๋‹ค (2026-09-10) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ์—ฌ๊ธฐ `@OneToMany(PatchEntry)` ๊ฐ€ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ **๋Œ€ํ™” ์„ธ์…˜์ด ๋ณด๋“œ ํŽธ์ง‘๊ธฐ๋ฅผ ์•Œ์•„์•ผ** ํ–ˆ๊ณ ,\n * ์„ธ์…˜ ๊ณ„์—ด์„ ์ค‘๋ฆฝ ํŒจํ‚ค์ง€๋กœ ์˜ฎ๊ธฐ๋ ค๋Š” ์ˆœ๊ฐ„ `ai-assistant โ†’ board-ai โ†’ ai-assistant` ์ˆœํ™˜์ด\n * ๋œ๋‹ค.\n *\n * ๋Š์–ด๋„ ์žƒ๋Š” ๊ฒƒ์ด ์—†๋‹ค โ€” ์ด ๊ด€๊ณ„๋ฅผ **์ž๋ฃŒ๋กœ ์ฝ๋Š” ์ž๋ฆฌ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์—ˆ๋‹ค**(์ „ ์ €์žฅ์†Œ์—์„œ\n * `session.patches` ๋ฅผ ์ฝ๋Š” ์ฝ”๋“œ 0 ๊ฑด). ์œ ์ผํ•œ ์“ฐ์ž„์ด `PatchEntry` ์ชฝ์˜ ์—ญ๋ฐฉํ–ฅ ์„ ์–ธ ํ•œ ์ค„์ด์—ˆ๊ณ ,\n * TypeORM ์€ ์—ญ๋ฐฉํ–ฅ ์—†์ด๋„ `@ManyToOne` ์„ ๋งŒ๋“ ๋‹ค. ํŒจ์น˜๋Š” `sessionId` ๋กœ ์ฐพ๋Š”๋‹ค\n * (`chatPatches` ๊ฐ€ ์ด๋ฏธ ๊ทธ๋ ‡๊ฒŒ ํ•œ๋‹ค).\n *\n * ๋ฐฉํ–ฅ์€ ๋‚จ๋Š”๋‹ค: **๋ณด๋“œ ํŒจ์น˜๊ฐ€ ์„ธ์…˜์„ ์•ˆ๋‹ค. ์„ธ์…˜์€ ๋ณด๋“œ ํŒจ์น˜๋ฅผ ๋ชจ๋ฅธ๋‹ค.** ์ข…์†์ด ํ•œ์ชฝ์ด๋ฉด\n * ์ค‘๋ฆฝ ์ชฝ์ด ์˜ฎ๊ฒจ์งˆ ์ˆ˜ ์žˆ๋‹ค.\n */\n}\n"]}
1
+ {"version":3,"file":"chat-session.js","sourceRoot":"","sources":["../../../server/service/chat-session/chat-session.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * ChatSession โ€” Board ์™€ ๊ฒฐํ•ฉ๋˜๋Š” AI ํ˜‘๋ ฅ ์„ธ์…˜.\n *\n * ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜ ๊ฐ€๋Šฅ โ€” DB ๋ ˆ๋ฒจ unique ์ œ์•ฝ ์—†์Œ.\n *\n * ํ–ฅํ›„ ํญ์ฆํ•  ์„ธ์…˜ ๋ถ„ํ™” ์ฐจ์› (์˜ˆ: thread / ์‚ฌ์šฉ์ž๋ณ„ / ์ปจํ…์ŠคํŠธ๋ณ„ design vs ops /\n * archived / forked from clone / agent๋ณ„) ์„ ๋ฏธ๋ฆฌ ์ฐจ๋‹จํ•˜์ง€ ์•Š๊ธฐ ์œ„ํ•ด ๊ตฌ์กฐ ์ž์ฒด๋Š” 1:N\n * ์œผ๋กœ ์—ด์–ด๋‘ . ๋‹จ์ผ ์„ธ์…˜ ์ •์ฑ… (ํ˜„์žฌ UX) ์€ app ๋ ˆ๋ฒจ idempotent ๋กœ์ง โ€” `startBoardAISession`\n * ์ด boardId ๋งค์นญ ์‹œ ๊ธฐ์กด ์„ธ์…˜ ๋ฐ˜ํ™˜, `chatSessionByBoard` ๊ฐ€ ์ฒซ ๋งค์นญ ๋ฐ˜ํ™˜.\n *\n * ๋ฏธ๋ž˜์— ๋‹ค์ค‘ ์„ธ์…˜ ํ™œ์„ฑํ™” ์‹œ discriminator ์ปฌ๋Ÿผ (kind / name / threadId / creatorId ๋“ฑ)\n * ์„ ๊ทธ๋•Œ ์ถ”๊ฐ€. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๋ถ€๋‹ด ์ตœ์†Œํ™” โ€” ํ˜„์žฌ๋Š” unique ๋งŒ ํ’€์–ด ์ž๋ฆฌ ๋งˆ๋ จ.\n *\n * board-service ์˜ Board ๊ฐ€ owning side ๊ฐ€ ๋  ์˜ˆ์ • (ChatSession FK).\n */\nimport {\n Column,\n CreateDateColumn,\n DeleteDateColumn,\n Entity,\n Index,\n ManyToOne,\n OneToMany,\n PrimaryGeneratedColumn,\n RelationId,\n UpdateDateColumn\n} from 'typeorm'\nimport { Field, ID, ObjectType } from 'type-graphql'\n\nimport { Domain } from '@things-factory/shell'\nimport { User } from '@things-factory/auth-base'\nimport { config } from '@things-factory/env'\n\nimport { ChatMessage } from '../chat-message/chat-message.js'\n\nconst ORMCONFIG = config.get('ormconfig', {})\nconst DATABASE_TYPE = ORMCONFIG.type\n\n@Entity()\n// ์ธ๋ฑ์Šค๋Š” ์กฐํšŒ ์„ฑ๋Šฅ์šฉ์œผ๋กœ ์œ ์ง€ (chatSessionByBoard ๊ฐ€ [domain, boardId] ๋กœ ์ž์ฃผ ์ฟผ๋ฆฌ).\n// unique ์ œ์•ฝ์€ ์˜๋„์ ์œผ๋กœ ์ œ๊ฑฐ โ€” ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜์„ ๋ฏธ๋ž˜์— ํ—ˆ์šฉํ•˜๊ธฐ ์œ„ํ•จ.\n@Index('ix_chat_session_1', (session: ChatSession) => [session.domain, session.boardId])\n// ์•ต์ปค ์กฐํšŒ์šฉ โ€” chatSessionsByAnchor ๊ฐ€ [domain, anchorType, anchorId] ๋กœ ์งˆ์˜.\n@Index('ix_chat_session_2', (session: ChatSession) => [session.domain, session.anchorType, session.anchorId])\n@ObjectType({\n description: 'One AI conversation, anchored to what it is about. An anchor may have several โ€” threads, one per person, one per context.'\n})\nexport class ChatSession {\n @PrimaryGeneratedColumn('uuid')\n @Field(type => ID, { nullable: true })\n readonly id?: string\n\n @ManyToOne(type => Domain)\n @Field(type => Domain, { nullable: true })\n domain?: Domain\n\n @RelationId((session: ChatSession) => session.domain)\n domainId?: string\n\n /** Board.id ์™€ ์—ฐ๊ฒฐ. Board ๊ฐ€ owning side ๊ฐ€ ๋  ์˜ˆ์ •. 1:N โ€” ํ•œ ๋ณด๋“œ์— ์—ฌ๋Ÿฌ ์„ธ์…˜ ๊ฐ€๋Šฅ.\n *\n * **์•ต์ปค ์ผ๋ฐ˜ํ™” ์ดํ›„์—๋„ ์œ ์ง€๋˜๋Š” ํ•˜์œ„ํ˜ธํ™˜ ์ปฌ๋Ÿผ**์ด๋‹ค(์ œ๊ฑฐํ•˜์ง€ ๋ง ๊ฒƒ). ์•ต์ปค๊ฐ€ board ์ธ ์„ธ์…˜์€\n * anchorId ์™€ ํ•จ๊ป˜ ์ด ์ปฌ๋Ÿผ์—๋„ ๊ฐ™์€ ๊ฐ’์„ ์“ด๋‹ค(dual-write) โ€” ๊ธฐ์กด ์กฐํšŒ/์ธ๋ฑ์Šค/์†Œ๋น„์ž๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค. */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n 'Connected Board id. Multiple sessions per board allowed (future: threads / per-user / contexts). Kept for backward compatibility โ€” board-anchored sessions dual-write this alongside anchorId.'\n })\n boardId?: string\n\n /**\n * ๋Œ€ํ™”๊ฐ€ ๋งค๋‹ฌ๋ฆฐ ๋Œ€์ƒ์˜ **์ข…๋ฅ˜**. ์„ธ์…˜์€ ๋ณด๋“œ์—๋งŒ ๋งค๋‹ฌ๋ฆฌ์ง€ ์•Š๋Š”๋‹ค โ€”\n * ์ €์ž‘ ๋Œ€ํ™”๋Š” ๋ณด๋“œ์—, ์šด์˜ ๋Œ€ํ™”๋Š” ๊ณต๊ฐ„(์‚ฌ์ดํŠธ)์— ๋งค๋‹ฌ๋ฆฐ๋‹ค. ์•ต์ปค๊ฐ€ ๋Œ€ํ™”์˜ ์„ฑ๊ฒฉ๊ณผ ์ด๋ ฅ ๊ฒฝ๊ณ„๋ฅผ ์ •ํ•œ๋‹ค.\n *\n * 'board' | 'space' | 'instance' | 'none'. ๋ฏธ์„ค์ •(๊ธฐ์กด ๋ฐ์ดํ„ฐ)์€ boardId ๊ฐ€ ์žˆ์œผ๋ฉด board ๋กœ ์ฝ๋Š”๋‹ค โ€”\n * ๋ฐฑํ•„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์—†์ด lazy ํ•˜์œ„ํ˜ธํ™˜(์ปฌ๋Ÿผ ์ถ”๊ฐ€๋งŒ).\n */\n @Column({ nullable: true })\n @Field({\n nullable: true,\n description:\n \"What this conversation is anchored to: 'board' | 'figure' (authoring a Figure) | 'space' | 'instance' | 'domain' | 'none'. Absent on legacy rows โ€” treated as 'board' when boardId is present.\"\n })\n anchorType?: string\n\n /** ์•ต์ปค ๋Œ€์ƒ์˜ id(๋ณด๋“œ id ยท ๊ณต๊ฐ„ id ยท ์ธ์Šคํ„ด์Šค id). anchorType ๊ณผ ํ•จ๊ป˜ ์˜๋ฏธ๋ฅผ ๊ฐ–๋Š”๋‹ค. */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the anchored target (board id / space id / instance id).' })\n anchorId?: string\n\n /**\n * ์„ธ์…˜์˜ ์‚ฌ์šฉ์ž ์ •์˜ ์ด๋ฆ„ (ํƒญ ๋ ˆ์ด๋ธ” / ์„ธ์…˜ ์‹๋ณ„ ํ‘œ์‹œ์šฉ).\n *\n * nullable โ€” ๋ฏธ์„ค์ • ์‹œ UI ๊ฐ€ fallback ๋ผ๋ฒจ (\"์„ธ์…˜ N\" ๋˜๋Š” createdAt ๊ธฐ๋ฐ˜) ์‚ฌ์šฉ. ์‚ฌ์šฉ์ž๊ฐ€\n * ๋‹ค์ค‘ ์„ธ์…˜ ์šด์˜ ์‹œ ์˜๋ฏธ ๋ถ€์—ฌ (์˜ˆ: \"์ดˆ๊ธฐ ์„ค๊ณ„\", \"์šด์˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜\", \"์‚ฌ์šฉ์žA ์˜ ์‚ฌ์  ์„ธ์…˜\").\n */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'User-given name of this session (tab label / identification).' })\n name?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who created this session.' })\n creator?: User\n\n @RelationId((session: ChatSession) => session.creator)\n creatorId?: string\n\n @ManyToOne(type => User)\n @Field(type => User, { nullable: true, description: 'User who last updated this session.' })\n updater?: User\n\n @RelationId((session: ChatSession) => session.updater)\n updaterId?: string\n\n /** ํ† ํฐ ์ ˆ๊ฐ์šฉ โ€” ์˜ค๋ž˜๋œ ๋ฉ”์‹œ์ง€๋ฅผ LLM ์œผ๋กœ ์••์ถ•ํ•œ ์š”์•ฝ. Phase 2 ์—์„œ ์ž‘์„ฑ. */\n /**\n * ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ ์‹œ๊ฐ โ€” **๋น„์ •๊ทœํ™”**. ๋ชฉ๋ก ์ •๋ ฌ(์ตœ๊ทผ ํ™œ๋™์ˆœ)๊ณผ ์•ˆ ์ฝ์Œ ํŒ์ •์˜ ๊ธฐ์ค€.\n *\n * ์™œ ๋น„์ •๊ทœํ™”์ธ๊ฐ€: ์„ธ์…˜๋งˆ๋‹ค ๋ฉ”์‹œ์ง€๋ฅผ ์ง‘๊ณ„ํ•˜๋ฉด(MAX/COUNT + group by) ๋ชฉ๋ก ํ•œ ๋ฒˆ์— ์ง‘๊ณ„ ์ฟผ๋ฆฌ๊ฐ€ ๋ถ™๊ณ ,\n * ์ด ํ”„๋ ˆ์ž„์›Œํฌ๋Š” 5๊ฐœ DB ๋“œ๋ผ์ด๋ฒ„๋ฅผ ์ง€์›ํ•ด์•ผ ํ•œ๋‹ค. ์„ธ์…˜ ์ปฌ๋Ÿผ ํ•˜๋‚˜๋กœ ๋‘๋ฉด ์ •๋ ฌยท๋น„๊ต๊ฐ€ ๊ฐ’์‹ธ๊ฒŒ ๋๋‚˜๊ณ \n * ๋“œ๋ผ์ด๋ฒ„ ์ฐจ์ด๊ฐ€ ์—†๋‹ค. ๋ฉ”์‹œ์ง€๋ฅผ ์ €์žฅํ•  ๋•Œ ํ•จ๊ป˜ ๊ฐฑ์‹ ํ•œ๋‹ค(activityPatch).\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Timestamp of the last message in this session (denormalized for list ordering and unread comparison โ€” avoids per-session aggregation across the supported DB drivers).' })\n lastMessageAt?: Date\n\n /**\n * ์–ด๋А ๋Œ€ํ™”์ธ๊ฐ€ โ€” `twin.board` ยท `figure.catalog` ๊ฐ™์€ ๋ชจ๋“œ์˜ kind (ADR-0072 ๊ฒฐ์ • 3).\n *\n * **์„œ๋ฒ„๊ฐ€ ์ ๋Š”๋‹ค.** ํด๋ผ์ด์–ธํŠธ๊ฐ€ ๋ณด๋‚ด๋Š” ๊ฐ’์ด ์•„๋‹ˆ๋‹ค โ€” ๋“ฑ๋ก๋œ ๋ชจ๋“œ์—์„œ ๋‚˜์˜จ ๊ฒƒ๋งŒ ์ ํžŒ๋‹ค(`addressee`\n * ์™€ ๊ฐ™์€ ๊ฒฐ). ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์„ธ์…˜์ด ์ž๊ธฐ๊ฐ€ ์–ด๋А ๋Œ€ํ™”์ธ์ง€ ์•Œ์•„์•ผ\n * ํ•œ๋‹ค. ๋ชฐ๋ž์„ ๋•Œ ํ™”๋ฉด์—์„œ ์ด๋ ‡๊ฒŒ ๋‚˜์™”๋‹ค: ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟ”๋„ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„, ใ€Œ๋„ํ˜• ์ฐพ๊ธฐใ€๋กœ\n * ๋ฐ”๊พผ ๋’ค ๋ณด๋‚ธ ๋ง์ด ์•ž์˜ ๊ณ ์น˜๊ธฐ ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).\n *\n * ๊ฐ’์ด ์—†๋Š” ์„ธ์…˜์€ ์ด ์นธ์ด ์„œ๊ธฐ ์ „์— ๋‚œ ๊ฒƒ์ด๋‹ค. ๋‹ค์Œ ํ„ด์— ์š”์ฒญํ•œ kind ๋กœ ํ•œ ๋ฒˆ ์ฐ๊ณ , ์ฐํžŒ ๋’ค์—๋Š”\n * ใ€Œ์š”์ฒญํ•œ ๋ชจ๋“œ์™€ ๋‹ค๋ฅด๋ฉด ๊ฑฐ์ ˆใ€ ํ•˜๋‚˜๋งŒ ๋‚จ๋Š”๋‹ค. ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜์€ ๋งŒ๋“ค์ง€ ์•Š๋Š”๋‹ค.\n */\n @Column({ type: 'varchar', length: 64, nullable: true })\n @Field({ nullable: true, description: \"Which conversation this session is โ€” the registered mode kind. Written by the server, never sent by the client.\" })\n kind?: string\n\n /**\n * ์ด ๋Œ€ํ™”๋ฅผ ๋„๋ฉ”์ธ ์•ˆ ๋ˆ„๊ตฌ๋‚˜ ์ฝ๋Š”๊ฐ€ โ€” ADR-0073 ํŒ์ • 2.\n *\n * `open` ๋„๋ฉ”์ธ ์•ˆ์ด๋ฉด ๋ˆ„๊ตฌ๋‚˜ ๋“ค์–ด์™€ ์ฝ๊ณ  ๋งํ•œ๋‹ค. ์ดˆ๋Œ€ ์ ˆ์ฐจ๋Š” ์—†๋‹ค.\n * `private` ์ฐธ์—ฌ์ž๋งŒ ์ฝ๋Š”๋‹ค.\n *\n * ์•ต์ปค๊ฐ€ ์žˆ๋Š” ์„ธ์…˜(space ยท board ยท station)์€ ๊ณต์œ ๋œ ๋ฌด์—‡์„ ๋†“๊ณ  ํ•˜๋Š” ์ด์•ผ๊ธฐ์ด๋ฏ€๋กœ `open` ์œผ๋กœ\n * ์‹œ์ž‘ํ•œ๋‹ค. ์•ต์ปค๊ฐ€ ์—†๋Š” ์„ธ์…˜๊ณผ **์˜ฎ๊ฒจ ์ ์–ด ๋งŒ๋“  ์„ธ์…˜**์€ `private` ์œผ๋กœ ์‹œ์ž‘ํ•œ๋‹ค โ€” ๊ฐ€๋ฒผ์šด ์งˆ๋ฌธ์„\n * ์Šน๊ฒฉํ•˜๋ฉด์„œ ์•ž์„  ๋Œ€ํ™”๊ฐ€ ์ƒˆ ์„ธ์…˜์— ์˜ฎ๊ฒจ ์ ํžˆ๋Š”๋ฐ(`appendSessionTurns`), ๊ทธ๊ฒƒ์ด ๊ณต๊ฐœ๋กœ ํƒœ์–ด๋‚˜๋ฉด\n * ์‚ฌ์ ์œผ๋กœ ์˜ค๊ฐ„ ์›๋ฌธ์ด ์†Œ๊ธ‰ํ•ด์„œ ๊ณต๊ฐœ๋œ๋‹ค.\n *\n * owner ๊ฐ€ ์–‘๋ฐฉํ–ฅ์œผ๋กœ ๋ฐ”๊ฟ€ ์ˆ˜ ์žˆ๋‹ค. ๊ณต๊ฐœ๋กœ ๋ฐ”๊พธ๊ธฐ ์ „์— ํ™”๋ฉด์ด ํ•œ ์ค„์„ ๋จผ์ € ๋ณด์ธ๋‹ค.\n *\n * open ์ด์–ด๋„ ๋‹ต ํ–‰์˜ ๋ฌธ ํŒ์ •(ADR-0073 ํŒ์ • 1)์€ ๊ทธ๋Œ€๋กœ๋‹ค โ€” ๋ฐฉ์— ๋“ค์–ด์˜จ ๊ฒƒ๊ณผ ๊ฐ’์„ ์ฝ๋Š” ๊ฒƒ์€ ๋‹ค๋ฅด๋‹ค.\n */\n @Column({ type: 'varchar', length: 16, default: 'open' })\n @Field({ description: \"Who may read this conversation: 'open' (anyone in the domain) or 'private' (participants only).\" })\n visibility!: string\n\n /** ๋ชฉ๋ก์— ๋ณด์—ฌ์ค„ ๋งˆ์ง€๋ง‰ ๋ฉ”์‹œ์ง€ ๋ฏธ๋ฆฌ๋ณด๊ธฐ โ€” ๋ชฉ๋ก์—์„œ ๋ฉ”์‹œ์ง€ ํ…Œ์ด๋ธ”์„ ๋‹ค์‹œ ์ฝ์ง€ ์•Š๊ธฐ ์œ„ํ•ด ์งง๊ฒŒ ์ €์žฅ. */\n @Column({ nullable: true, length: 200 })\n @Field({ nullable: true, description: 'Short preview of the last message, stored so the session list does not need to query messages.' })\n lastMessagePreview?: string\n\n /**\n * ๊ทธ ๋ฏธ๋ฆฌ๋ณด๊ธฐ๊ฐ€ ์ง€๋‚˜์˜จ ๋ฌธ โ€” ADR-0073 ํŒ์ • 1.\n *\n * ๋ชฉ๋ก์€ ๋‹ต์„ **๋‹ค์‹œ ์ฝ๋Š” ๋‘ ๋ฒˆ์งธ ๊ธธ**์ด๋‹ค. ๋ฉ”์‹œ์ง€ ํ™”๋ฉด์—์„œ๋งŒ ๊ฐ€๋ฆฌ๋ฉด ๊ฐ™์€ ๋ฌธ์žฅ์ด ๋ชฉ๋ก์— ๊ทธ๋Œ€๋กœ ๋œฌ๋‹ค.\n * ๋ฌธ์ด ๋‘˜์ด๊ณ  ํ•œ์ชฝ์ด ๋А์Šจํ•˜๋ฉด ๋А์Šจํ•œ ์ชฝ์ด ์ด๊ธด๋‹ค(ADR-0061 ๊ฒฐ์ • 1). ๋ฏธ๋ฆฌ๋ณด๊ธฐ๋ฅผ ์ €์žฅํ•  ๋•Œ ๊ทธ ํ–‰์˜ ๋ฌธ์„\n * ํ•จ๊ป˜ ์˜ฎ๊ฒจ ์ ๊ณ , ๋ชฉ๋ก๋„ ๊ฐ™์€ ๊ด€๋ฌธ์„ ์ง€๋‚œ๋‹ค.\n */\n @Column({ type: 'simple-array', nullable: true })\n @Field(() => [String], { nullable: true, description: 'Doors of the message the preview came from โ€” the session list passes the same gate as the conversation.' })\n lastMessageDoors?: string[]\n\n @Column({\n nullable: true,\n type:\n DATABASE_TYPE == 'mysql' || DATABASE_TYPE == 'mariadb'\n ? 'longtext'\n : DATABASE_TYPE == 'oracle'\n ? 'clob'\n : DATABASE_TYPE == 'mssql'\n ? 'nvarchar'\n : 'text',\n length: DATABASE_TYPE == 'mssql' ? 'MAX' : undefined\n })\n @Field({ nullable: true, description: 'Compressed summary of older messages (for token saving).' })\n lastSummary?: string\n\n /**\n * `lastSummary` ๊ฐ€ **์–ด๋””๊นŒ์ง€ ๋ฎ๋Š”์ง€** โ€” ์ด ๋ฉ”์‹œ์ง€๊นŒ์ง€ ์š”์•ฝ์— ๋ฐ˜์˜๋๋‹ค.\n *\n * ์ด๊ฒŒ ์—†์œผ๋ฉด ๋ˆ„์  ์š”์•ฝ์„ ํ•  ์ˆ˜ ์—†๋‹ค: ๋งค ํ„ด ์ „์ฒด๋ฅผ ๋‹ค์‹œ ์š”์•ฝํ•˜๊ฑฐ๋‚˜(๋น„์šฉ) ์ด๋ฏธ ์š”์•ฝํ•œ ๊ตฌ๊ฐ„์„\n * ๋˜ ์ด์–ด ๋ถ™์ธ๋‹ค(์ค‘๋ณต). ์ด๋ ฅ์—์„œ ์ด id ๋ฅผ ๋ชป ์ฐพ์œผ๋ฉด(์‚ญ์ œยทํŽธ์ง‘) ๋ฎ์€ ๋ฒ”์œ„๋ฅผ ์•Œ ์ˆ˜ ์—†์œผ๋ฏ€๋กœ\n * ์ „๋ถ€ ๋ฏธ๋ฐ˜์˜์œผ๋กœ ๋ณด๊ณ  ๋‹ค์‹œ ์š”์•ฝํ•œ๋‹ค โ€” ๋น ๋œจ๋ฆฌ๋Š” ์ชฝ๋ณด๋‹ค ์•ˆ์ „ํ•˜๋‹ค.\n */\n @Column({ nullable: true })\n @Field({ nullable: true, description: 'Id of the last message folded into lastSummary โ€” enables incremental summarization instead of re-reading the whole history.' })\n summaryUpToMessageId?: string\n\n /** ์‚ฌ์šฉ ๋ชจ๋ธ ์‹๋ณ„ โ€” 'anthropic:claude-sonnet-4-6' ๋“ฑ */\n @Column({ nullable: true })\n @Field({ nullable: true })\n aiClientId?: string\n\n @CreateDateColumn()\n @Field({ nullable: true })\n createdAt?: Date\n\n @UpdateDateColumn()\n @Field({ nullable: true })\n updatedAt?: Date\n\n @DeleteDateColumn()\n deletedAt?: Date\n\n // โ”€โ”€ ๊ด€๊ณ„ (TypeORM ๋งŒ, GraphQL ๋…ธ์ถœ์€ ๋ณ„๋„ query ํ†ตํ•ด ํŽ˜์ด์ง•) โ”€โ”€\n @OneToMany(type => ChatMessage, message => message.session)\n messages?: ChatMessage[]\n\n /*\n * โ”€โ”€ ๋ณด๋“œ ํŒจ์น˜ ๊ด€๊ณ„๋ฅผ ๋Š์—ˆ๋‹ค (2026-09-10) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * ์—ฌ๊ธฐ `@OneToMany(PatchEntry)` ๊ฐ€ ์žˆ์—ˆ๋‹ค. ๊ทธ๋ž˜์„œ **๋Œ€ํ™” ์„ธ์…˜์ด ๋ณด๋“œ ํŽธ์ง‘๊ธฐ๋ฅผ ์•Œ์•„์•ผ** ํ–ˆ๊ณ ,\n * ์„ธ์…˜ ๊ณ„์—ด์„ ์ค‘๋ฆฝ ํŒจํ‚ค์ง€๋กœ ์˜ฎ๊ธฐ๋ ค๋Š” ์ˆœ๊ฐ„ `ai-assistant โ†’ board-ai โ†’ ai-assistant` ์ˆœํ™˜์ด\n * ๋œ๋‹ค.\n *\n * ๋Š์–ด๋„ ์žƒ๋Š” ๊ฒƒ์ด ์—†๋‹ค โ€” ์ด ๊ด€๊ณ„๋ฅผ **์ž๋ฃŒ๋กœ ์ฝ๋Š” ์ž๋ฆฌ๊ฐ€ ํ•˜๋‚˜๋„ ์—†์—ˆ๋‹ค**(์ „ ์ €์žฅ์†Œ์—์„œ\n * `session.patches` ๋ฅผ ์ฝ๋Š” ์ฝ”๋“œ 0 ๊ฑด). ์œ ์ผํ•œ ์“ฐ์ž„์ด `PatchEntry` ์ชฝ์˜ ์—ญ๋ฐฉํ–ฅ ์„ ์–ธ ํ•œ ์ค„์ด์—ˆ๊ณ ,\n * TypeORM ์€ ์—ญ๋ฐฉํ–ฅ ์—†์ด๋„ `@ManyToOne` ์„ ๋งŒ๋“ ๋‹ค. ํŒจ์น˜๋Š” `sessionId` ๋กœ ์ฐพ๋Š”๋‹ค\n * (`chatPatches` ๊ฐ€ ์ด๋ฏธ ๊ทธ๋ ‡๊ฒŒ ํ•œ๋‹ค).\n *\n * ๋ฐฉํ–ฅ์€ ๋‚จ๋Š”๋‹ค: **๋ณด๋“œ ํŒจ์น˜๊ฐ€ ์„ธ์…˜์„ ์•ˆ๋‹ค. ์„ธ์…˜์€ ๋ณด๋“œ ํŒจ์น˜๋ฅผ ๋ชจ๋ฅธ๋‹ค.** ์ข…์†์ด ํ•œ์ชฝ์ด๋ฉด\n * ์ค‘๋ฆฝ ์ชฝ์ด ์˜ฎ๊ฒจ์งˆ ์ˆ˜ ์žˆ๋‹ค.\n */\n}\n"]}
@@ -23,4 +23,13 @@ export declare function anchorColumns(anchorType: string | undefined, anchorId:
23
23
  * ์—ฌ๊ธฐ์„œ๋„ ์•ต์ปค ์ด๋ฆ„์„ ๊ฒ€์‚ฌํ•œ๋‹ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ์—์„œ ํ‹€๋ฆฐ ์ด๋ฆ„์€ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ **0 ํ–‰**์œผ๋กœ ๋‚˜์˜ค๊ณ ,
24
24
  * ํ™”๋ฉด์€ ใ€Œ๋Œ€ํ™”๊ฐ€ ์—†๋‹คใ€๋กœ ๊ทธ๋ฆฐ๋‹ค. ๊ทธ ์ฆ์ƒ์œผ๋กœ๋Š” ์›์ธ์„ ๋ชป ์ฐพ๋Š”๋‹ค.
25
25
  */
26
- export declare function anchorWhere(domainId: string, anchorType: string, anchorId: string): any;
26
+ /**
27
+ * ์ด ์•ต์ปค์˜ ์„ธ์…˜์„ ์ฐพ๋Š” ์กฐ๊ฑด โ€” `kind` ๋ฅผ ์ฃผ๋ฉด **๊ทธ ๋Œ€ํ™”์˜** ์„ธ์…˜๋งŒ ๋ณธ๋‹ค(ADR-0072 ๊ฒฐ์ • 3).
28
+ *
29
+ * ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์•ต์ปค๋งŒ์œผ๋กœ ์ฐพ์œผ๋ฉด ์ €์ž‘ ๋Œ€ํ™”๋ฅผ ์ฐพ๋Š” ์ž๋ฆฌ์—์„œ ์šด์˜
30
+ * ๋Œ€ํ™”๊ฐ€ ์žกํžŒ๋‹ค. figure ์—์„œ ๊ทธ๊ฒƒ์ด ํ™”๋ฉด์œผ๋กœ ๋‚˜์™”๋‹ค โ€” ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟจ๋Š”๋ฐ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์ƒˆ ๋ง์ด
31
+ * ์•ž ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).
32
+ *
33
+ * `kind` ๋ฅผ ์•ˆ ์ฃผ๋ฉด ์•ต์ปค์˜ ๋Œ€ํ™”๋ฅผ ๋ชจ๋‘ ๋ณธ๋‹ค โ€” ๋ชฉ๋ก์ฒ˜๋Ÿผ ๋Œ€ํ™”๋ฅผ ๊ฐ€๋ฆฌ์ง€ ์•Š๊ณ  ํ›‘๋Š” ์ž๋ฆฌ๋‹ค.
34
+ */
35
+ export declare function anchorWhere(domainId: string, anchorType: string, anchorId: string, kind?: string): any;
@@ -93,10 +93,18 @@ function anchorColumns(anchorType, anchorId) {
93
93
  * ์—ฌ๊ธฐ์„œ๋„ ์•ต์ปค ์ด๋ฆ„์„ ๊ฒ€์‚ฌํ•œ๋‹ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ์—์„œ ํ‹€๋ฆฐ ์ด๋ฆ„์€ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ **0 ํ–‰**์œผ๋กœ ๋‚˜์˜ค๊ณ ,
94
94
  * ํ™”๋ฉด์€ ใ€Œ๋Œ€ํ™”๊ฐ€ ์—†๋‹คใ€๋กœ ๊ทธ๋ฆฐ๋‹ค. ๊ทธ ์ฆ์ƒ์œผ๋กœ๋Š” ์›์ธ์„ ๋ชป ์ฐพ๋Š”๋‹ค.
95
95
  */
96
- function anchorWhere(domainId, anchorType, anchorId) {
96
+ /**
97
+ * ์ด ์•ต์ปค์˜ ์„ธ์…˜์„ ์ฐพ๋Š” ์กฐ๊ฑด โ€” `kind` ๋ฅผ ์ฃผ๋ฉด **๊ทธ ๋Œ€ํ™”์˜** ์„ธ์…˜๋งŒ ๋ณธ๋‹ค(ADR-0072 ๊ฒฐ์ • 3).
98
+ *
99
+ * ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์•ต์ปค๋งŒ์œผ๋กœ ์ฐพ์œผ๋ฉด ์ €์ž‘ ๋Œ€ํ™”๋ฅผ ์ฐพ๋Š” ์ž๋ฆฌ์—์„œ ์šด์˜
100
+ * ๋Œ€ํ™”๊ฐ€ ์žกํžŒ๋‹ค. figure ์—์„œ ๊ทธ๊ฒƒ์ด ํ™”๋ฉด์œผ๋กœ ๋‚˜์™”๋‹ค โ€” ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟจ๋Š”๋ฐ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์ƒˆ ๋ง์ด
101
+ * ์•ž ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).
102
+ *
103
+ * `kind` ๋ฅผ ์•ˆ ์ฃผ๋ฉด ์•ต์ปค์˜ ๋Œ€ํ™”๋ฅผ ๋ชจ๋‘ ๋ณธ๋‹ค โ€” ๋ชฉ๋ก์ฒ˜๋Ÿผ ๋Œ€ํ™”๋ฅผ ๊ฐ€๋ฆฌ์ง€ ์•Š๊ณ  ํ›‘๋Š” ์ž๋ฆฌ๋‹ค.
104
+ */
105
+ function anchorWhere(domainId, anchorType, anchorId, kind) {
97
106
  const type = assertAnchorType(anchorType, 'anchorWhere');
98
- if (type === 'board')
99
- return { boardId: anchorId, domain: { id: domainId } };
100
- return { anchorType: type, anchorId, domain: { id: domainId } };
107
+ const where = type === 'board' ? { boardId: anchorId, domain: { id: domainId } } : { anchorType: type, anchorId, domain: { id: domainId } };
108
+ return kind ? { ...where, kind } : where;
101
109
  }
102
110
  //# sourceMappingURL=session-anchor.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"session-anchor.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-anchor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;;;AAiBH,kDAEC;AAiBD,sCAeC;AASD,kCAIC;AA9DD;;;;;;;;GAQG;AACH,2EAA2E;AAC3E,6EAA6E;AAChE,QAAA,oBAAoB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAA;AAItH,SAAgB,mBAAmB,CAAC,KAAc;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,4BAA0C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACjG,CAAC;AAED,SAAS,gBAAgB,CAAC,UAA8B,EAAE,KAAa;IACrE,MAAM,IAAI,GAAG,UAAU,IAAI,MAAM,CAAA;IACjC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,6BAA6B,KAAK,MAAM,IAAI,2BAA2B;YACrE,YAAY,4BAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;YAChD,gGAAgG;YAChG,2EAA2E;YAC3E,4EAA4E,CAC/E,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,oEAAoE;AACpE,SAAgB,aAAa,CAC3B,UAA8B,EAC9B,QAA4B;IAE5B,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;IAC1D,MAAM,EAAE,GAAG,QAAQ,IAAI,SAAS,CAAA;IAChC,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,qBAAqB;YAC3E,+FAA+F;YAC/F,gGAAgG;YAChG,+EAA+E,CAClF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;AACvF,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,WAAW,CAAC,QAAgB,EAAE,UAAkB,EAAE,QAAgB;IAChF,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;IACxD,IAAI,IAAI,KAAK,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAA;IAC5E,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAA;AACjE,CAAC","sourcesContent":["/*\n * ์„ธ์…˜ ์•ต์ปค ์ˆœ์ˆ˜ ๋กœ์ง โ€” ๋ฆฌ์กธ๋ฒ„์™€ ๋ถ„๋ฆฌ.\n *\n * chat-session-resolver ๋Š” type-graphql ์˜ **๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ**(@Arg)๋ฅผ ์“ฐ๋ฏ€๋กœ jest ์˜ babel\n * ํŒŒ์„œ๊ฐ€ ์ฝ์ง€ ๋ชปํ•œ๋‹ค. ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์—ฌ๊ธฐ ๋‘๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค(chat-echo-dedup ยท\n * chat-input-builder ์™€ ๊ฐ™์€ ๊ด€๋ก€).\n *\n * **์•ต์ปค๋ž€**: ๋Œ€ํ™”๊ฐ€ ๋ฌด์—‡์— ๋งค๋‹ฌ๋ ค ์žˆ๋Š”์ง€. ์ €์ž‘ ๋Œ€ํ™”๋Š” ๋ณด๋“œ์—, ์šด์˜ ๋Œ€ํ™”๋Š” ๊ณต๊ฐ„(์‚ฌ์ดํŠธ์™€ ๊ทธ ๊ณต๊ฐ„์—์„œ\n * ๋„๋Š” ํŠธ์œˆ๋“ค)์— ๋งค๋‹ฌ๋ฆฐ๋‹ค. ์•ต์ปค๊ฐ€ ๋Œ€ํ™”์˜ ์„ฑ๊ฒฉ๊ณผ **์ด๋ ฅ ๊ฒฝ๊ณ„**๋ฅผ ์ •ํ•œ๋‹ค โ€” ๋ณด๋“œ ํŽธ์ง‘ ์ด๋ ฅ์— ์šด์˜\n * ๋ฌธ๋‹ต์ด ๋ผ์ง€ ์•Š์•„์•ผ ํ•˜๊ณ , ๊ทธ ๋ฐ˜๋Œ€๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋‹ค.\n *\n * **ํ•˜์œ„ํ˜ธํ™˜**: ๊ธฐ์กด ํ–‰์€ ์•ต์ปค ์ปฌ๋Ÿผ์ด ๋น„์–ด ์žˆ๊ณ  boardId ๋งŒ ์žˆ๋‹ค. ๋ฐฑํ•„ํ•˜์ง€ ์•Š๊ณ  **์ฝ์„ ๋•Œ** board\n * ์•ต์ปค๋กœ ๊ฐ„์ฃผํ•œ๋‹ค(์ปฌ๋Ÿผ ์ถ”๊ฐ€๋งŒ์œผ๋กœ ๋๋‚ด๊ธฐ ์œ„ํ•จ). ๋ณด๋“œ ์•ต์ปค ์„ธ์…˜์€ anchorId ์™€ boardId ๋ฅผ ํ•จ๊ป˜\n * ์“ด๋‹ค(dual-write) โ€” ๊ธฐ์กด ์กฐํšŒยท์ธ๋ฑ์Šคยท์†Œ๋น„์ž(operato-board ๋“ฑ)๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค.\n *\n * โ”€โ”€ Why this file validates instead of casting (2026-09-09) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * `anchorColumns` used to do `(anchorType || 'none') as SessionAnchorType`. That is a cast, not a\n * check: the type existed and nothing enforced it, so any string reached a nullable varchar and\n * was stored. The plant lane was about to add a product and stopped to ask which value to write,\n * which is the only reason this was noticed โ€” the column would have accepted whatever it sent.\n *\n * A wrong value here is not a wrong value in one row. The anchor decides which conversations are\n * listed together and which history a reply may see, so a typo silently creates a second,\n * invisible bucket: the session is written, the inbox query for the real anchor does not match\n * it, and the conversation is simply gone from the list with no error anywhere.\n *\n * Two rules follow, and both are refusals rather than defaults:\n *\n * ยท An unknown anchor type throws. A default of 'none' would be worse than a typo, because it\n * turns \"you named something that does not exist\" into \"this conversation is about nothing\".\n * ยท Every anchor except 'none' requires an id. An anchor is a pointer; `('space', '')` points\n * nowhere, and `anchorWhere` would then look for an empty string while `anchorColumns` stored\n * NULL โ€” the lookup misses its own row and every call creates another session.\n *\n * 'none' therefore means exactly one thing: **not addressable by anchor.** It is the UI state for\n * \"nothing picked yet\" (twin's dock refuses to create a session while it holds 'none'), not a\n * value a product writes because it has nothing narrower to point at. A product whose subject is\n * the whole tenant anchors to 'domain' and holds the domain id โ€” that conversation is listable and\n * has a history boundary, and \"unanchored\" has neither.\n *\n * Adding a value to this set is an architect's call, not a lane's. A new anchor type means a new\n * kind of subject a conversation can be about, and it is stored โ€” once rows carry it, renaming it\n * orphans them.\n */\n\n/**\n * ๋Œ€ํ™”๊ฐ€ ๋งค๋‹ฌ๋ฆด ์ˆ˜ ์žˆ๋Š” ๊ฒƒ. **์ €์žฅ๋˜๋Š” ๊ฐ’์ด๋ฏ€๋กœ ๋Š˜๋ฆฌ๋Š” ๊ฒƒ์€ ์•„ํ‚คํ…ํŠธ ํŒ๋‹จ์ด๋‹ค.**\n *\n * board ๋ณด๋“œ ์ €์ž‘ ๋Œ€ํ™” โ€” ๊ทธ ๋ณด๋“œ์˜ ํŽธ์ง‘ ์ด๋ ฅ\n * space ๊ณต๊ฐ„ ์šด์˜ ๋Œ€ํ™” โ€” ์‚ฌ์ดํŠธ์™€ ๊ทธ ๊ณต๊ฐ„์—์„œ ๋„๋Š” ํŠธ์œˆ๋“ค\n * instance ํŠธ์œˆ ํ•˜๋‚˜์— ๋Œ€ํ•œ ๋Œ€ํ™”\n * domain ํ…Œ๋„ŒํŠธ ์ „์ฒด๊ฐ€ ์ฃผ์ œ์ธ ๋Œ€ํ™” โ€” ๊ณต์žฅ ์ „์ฒด ๋ฌธ๋‹ต์ฒ˜๋Ÿผ ๋” ์ข๊ฒŒ ๊ฐ€๋ฆฌํ‚ฌ ๊ฒƒ์ด ์—†์„ ๋•Œ\n * none ์•ต์ปค๋กœ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๋Œ€ํ™”. ใ€Œ์•„์ง ์•„๋ฌด๊ฒƒ๋„ ๊ณ ๋ฅด์ง€ ์•Š์•˜๋‹คใ€๋Š” ํ™”๋ฉด ์ƒํƒœ๋‹ค\n */\n// Figure-specific history requested explicitly: reuse the existing varchar\n// anchor columns, without writing Figure ids into the legacy boardId column.\nexport const SESSION_ANCHOR_TYPES = ['board', 'figure', 'application', 'space', 'instance', 'domain', 'none'] as const\n\nexport type SessionAnchorType = (typeof SESSION_ANCHOR_TYPES)[number]\n\nexport function isSessionAnchorType(value: unknown): value is SessionAnchorType {\n return typeof value === 'string' && (SESSION_ANCHOR_TYPES as readonly string[]).includes(value)\n}\n\nfunction assertAnchorType(anchorType: string | undefined, where: string): SessionAnchorType {\n const type = anchorType || 'none'\n if (!isSessionAnchorType(type)) {\n throw new Error(\n `[board-ai/session-anchor] ${where}: '${type}' is not an anchor type. ` +\n `Allowed: ${SESSION_ANCHOR_TYPES.join(' ยท ')}. ` +\n 'The anchor decides which conversations are listed together and which history a reply may see, ' +\n 'so an unrecognised value would write a session the inbox can never find. ' +\n 'If a new kind of subject is needed, raise it โ€” this set is stored in rows.'\n )\n }\n return type\n}\n\n/** ์•ต์ปค โ†’ ์ €์žฅํ•  ์ปฌ๋Ÿผ ๊ฐ’. ๋ณด๋“œ ์•ต์ปค๋Š” boardId ์—๋„ ๊ฐ™์€ ๊ฐ’์„ ๋„ฃ์–ด ๊ธฐ์กด ์กฐํšŒ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•œ๋‹ค. */\nexport function anchorColumns(\n anchorType: string | undefined,\n anchorId: string | undefined\n): { anchorType: SessionAnchorType; anchorId?: string; boardId?: string } {\n const type = assertAnchorType(anchorType, 'anchorColumns')\n const id = anchorId || undefined\n if (type !== 'none' && !id) {\n throw new Error(\n `[board-ai/session-anchor] anchorColumns: anchor '${type}' was given no id. ` +\n 'An anchor is a pointer, so this would store NULL while the lookup searches for the id it was ' +\n 'given โ€” the session would not be found by its own anchor and every call would create another. ' +\n \"Use 'none' for a conversation that is deliberately not addressable by anchor.\"\n )\n }\n return { anchorType: type, anchorId: id, boardId: type === 'board' ? id : undefined }\n}\n\n/**\n * ์•ต์ปค ์กฐํšŒ ์กฐ๊ฑด. ๋ณด๋“œ ์•ต์ปค๋Š” **์•ต์ปค ์ปฌ๋Ÿผ์ด ๋นˆ ์˜› ํ–‰๋„ ์žก์•„์•ผ** ํ•˜๋ฏ€๋กœ boardId ๋กœ ์ฐพ๋Š”๋‹ค\n * (dual-write ๋•๋ถ„์— ์‹ ๊ทœ ํ–‰๋„ ๊ฐ™์€ ์กฐ๊ฑด์— ๊ฑธ๋ฆฐ๋‹ค). ๊ทธ ๋ฐ– ์•ต์ปค๋Š” ์•ต์ปค ์ปฌ๋Ÿผ์œผ๋กœ ์ฐพ๋Š”๋‹ค.\n *\n * ์—ฌ๊ธฐ์„œ๋„ ์•ต์ปค ์ด๋ฆ„์„ ๊ฒ€์‚ฌํ•œ๋‹ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ์—์„œ ํ‹€๋ฆฐ ์ด๋ฆ„์€ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ **0 ํ–‰**์œผ๋กœ ๋‚˜์˜ค๊ณ ,\n * ํ™”๋ฉด์€ ใ€Œ๋Œ€ํ™”๊ฐ€ ์—†๋‹คใ€๋กœ ๊ทธ๋ฆฐ๋‹ค. ๊ทธ ์ฆ์ƒ์œผ๋กœ๋Š” ์›์ธ์„ ๋ชป ์ฐพ๋Š”๋‹ค.\n */\nexport function anchorWhere(domainId: string, anchorType: string, anchorId: string): any {\n const type = assertAnchorType(anchorType, 'anchorWhere')\n if (type === 'board') return { boardId: anchorId, domain: { id: domainId } }\n return { anchorType: type, anchorId, domain: { id: domainId } }\n}\n"]}
1
+ {"version":3,"file":"session-anchor.js","sourceRoot":"","sources":["../../../server/service/chat-session/session-anchor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;;;AAiBH,kDAEC;AAiBD,sCAeC;AAkBD,kCAIC;AAvED;;;;;;;;GAQG;AACH,2EAA2E;AAC3E,6EAA6E;AAChE,QAAA,oBAAoB,GAAG,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAU,CAAA;AAItH,SAAgB,mBAAmB,CAAC,KAAc;IAChD,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,4BAA0C,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAA;AACjG,CAAC;AAED,SAAS,gBAAgB,CAAC,UAA8B,EAAE,KAAa;IACrE,MAAM,IAAI,GAAG,UAAU,IAAI,MAAM,CAAA;IACjC,IAAI,CAAC,mBAAmB,CAAC,IAAI,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,6BAA6B,KAAK,MAAM,IAAI,2BAA2B;YACrE,YAAY,4BAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI;YAChD,gGAAgG;YAChG,2EAA2E;YAC3E,4EAA4E,CAC/E,CAAA;IACH,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED,oEAAoE;AACpE,SAAgB,aAAa,CAC3B,UAA8B,EAC9B,QAA4B;IAE5B,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,EAAE,eAAe,CAAC,CAAA;IAC1D,MAAM,EAAE,GAAG,QAAQ,IAAI,SAAS,CAAA;IAChC,IAAI,IAAI,KAAK,MAAM,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACb,oDAAoD,IAAI,qBAAqB;YAC3E,+FAA+F;YAC/F,gGAAgG;YAChG,+EAA+E,CAClF,CAAA;IACH,CAAC;IACD,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,OAAO,EAAE,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,SAAS,EAAE,CAAA;AACvF,CAAC;AAED;;;;;;GAMG;AACH;;;;;;;;GAQG;AACH,SAAgB,WAAW,CAAC,QAAgB,EAAE,UAAkB,EAAE,QAAgB,EAAE,IAAa;IAC/F,MAAM,IAAI,GAAG,gBAAgB,CAAC,UAAU,EAAE,aAAa,CAAC,CAAA;IACxD,MAAM,KAAK,GAAG,IAAI,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAA;IAC3I,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,KAAK,CAAA;AAC1C,CAAC","sourcesContent":["/*\n * ์„ธ์…˜ ์•ต์ปค ์ˆœ์ˆ˜ ๋กœ์ง โ€” ๋ฆฌ์กธ๋ฒ„์™€ ๋ถ„๋ฆฌ.\n *\n * chat-session-resolver ๋Š” type-graphql ์˜ **๋งค๊ฐœ๋ณ€์ˆ˜ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ**(@Arg)๋ฅผ ์“ฐ๋ฏ€๋กœ jest ์˜ babel\n * ํŒŒ์„œ๊ฐ€ ์ฝ์ง€ ๋ชปํ•œ๋‹ค. ์ˆœ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์—ฌ๊ธฐ ๋‘๋ฉด ๋‹จ์œ„ ํ…Œ์ŠคํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค(chat-echo-dedup ยท\n * chat-input-builder ์™€ ๊ฐ™์€ ๊ด€๋ก€).\n *\n * **์•ต์ปค๋ž€**: ๋Œ€ํ™”๊ฐ€ ๋ฌด์—‡์— ๋งค๋‹ฌ๋ ค ์žˆ๋Š”์ง€. ์ €์ž‘ ๋Œ€ํ™”๋Š” ๋ณด๋“œ์—, ์šด์˜ ๋Œ€ํ™”๋Š” ๊ณต๊ฐ„(์‚ฌ์ดํŠธ์™€ ๊ทธ ๊ณต๊ฐ„์—์„œ\n * ๋„๋Š” ํŠธ์œˆ๋“ค)์— ๋งค๋‹ฌ๋ฆฐ๋‹ค. ์•ต์ปค๊ฐ€ ๋Œ€ํ™”์˜ ์„ฑ๊ฒฉ๊ณผ **์ด๋ ฅ ๊ฒฝ๊ณ„**๋ฅผ ์ •ํ•œ๋‹ค โ€” ๋ณด๋“œ ํŽธ์ง‘ ์ด๋ ฅ์— ์šด์˜\n * ๋ฌธ๋‹ต์ด ๋ผ์ง€ ์•Š์•„์•ผ ํ•˜๊ณ , ๊ทธ ๋ฐ˜๋Œ€๋„ ๋งˆ์ฐฌ๊ฐ€์ง€๋‹ค.\n *\n * **ํ•˜์œ„ํ˜ธํ™˜**: ๊ธฐ์กด ํ–‰์€ ์•ต์ปค ์ปฌ๋Ÿผ์ด ๋น„์–ด ์žˆ๊ณ  boardId ๋งŒ ์žˆ๋‹ค. ๋ฐฑํ•„ํ•˜์ง€ ์•Š๊ณ  **์ฝ์„ ๋•Œ** board\n * ์•ต์ปค๋กœ ๊ฐ„์ฃผํ•œ๋‹ค(์ปฌ๋Ÿผ ์ถ”๊ฐ€๋งŒ์œผ๋กœ ๋๋‚ด๊ธฐ ์œ„ํ•จ). ๋ณด๋“œ ์•ต์ปค ์„ธ์…˜์€ anchorId ์™€ boardId ๋ฅผ ํ•จ๊ป˜\n * ์“ด๋‹ค(dual-write) โ€” ๊ธฐ์กด ์กฐํšŒยท์ธ๋ฑ์Šคยท์†Œ๋น„์ž(operato-board ๋“ฑ)๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•œ๋‹ค.\n *\n * โ”€โ”€ Why this file validates instead of casting (2026-09-09) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€\n * `anchorColumns` used to do `(anchorType || 'none') as SessionAnchorType`. That is a cast, not a\n * check: the type existed and nothing enforced it, so any string reached a nullable varchar and\n * was stored. The plant lane was about to add a product and stopped to ask which value to write,\n * which is the only reason this was noticed โ€” the column would have accepted whatever it sent.\n *\n * A wrong value here is not a wrong value in one row. The anchor decides which conversations are\n * listed together and which history a reply may see, so a typo silently creates a second,\n * invisible bucket: the session is written, the inbox query for the real anchor does not match\n * it, and the conversation is simply gone from the list with no error anywhere.\n *\n * Two rules follow, and both are refusals rather than defaults:\n *\n * ยท An unknown anchor type throws. A default of 'none' would be worse than a typo, because it\n * turns \"you named something that does not exist\" into \"this conversation is about nothing\".\n * ยท Every anchor except 'none' requires an id. An anchor is a pointer; `('space', '')` points\n * nowhere, and `anchorWhere` would then look for an empty string while `anchorColumns` stored\n * NULL โ€” the lookup misses its own row and every call creates another session.\n *\n * 'none' therefore means exactly one thing: **not addressable by anchor.** It is the UI state for\n * \"nothing picked yet\" (twin's dock refuses to create a session while it holds 'none'), not a\n * value a product writes because it has nothing narrower to point at. A product whose subject is\n * the whole tenant anchors to 'domain' and holds the domain id โ€” that conversation is listable and\n * has a history boundary, and \"unanchored\" has neither.\n *\n * Adding a value to this set is an architect's call, not a lane's. A new anchor type means a new\n * kind of subject a conversation can be about, and it is stored โ€” once rows carry it, renaming it\n * orphans them.\n */\n\n/**\n * ๋Œ€ํ™”๊ฐ€ ๋งค๋‹ฌ๋ฆด ์ˆ˜ ์žˆ๋Š” ๊ฒƒ. **์ €์žฅ๋˜๋Š” ๊ฐ’์ด๋ฏ€๋กœ ๋Š˜๋ฆฌ๋Š” ๊ฒƒ์€ ์•„ํ‚คํ…ํŠธ ํŒ๋‹จ์ด๋‹ค.**\n *\n * board ๋ณด๋“œ ์ €์ž‘ ๋Œ€ํ™” โ€” ๊ทธ ๋ณด๋“œ์˜ ํŽธ์ง‘ ์ด๋ ฅ\n * space ๊ณต๊ฐ„ ์šด์˜ ๋Œ€ํ™” โ€” ์‚ฌ์ดํŠธ์™€ ๊ทธ ๊ณต๊ฐ„์—์„œ ๋„๋Š” ํŠธ์œˆ๋“ค\n * instance ํŠธ์œˆ ํ•˜๋‚˜์— ๋Œ€ํ•œ ๋Œ€ํ™”\n * domain ํ…Œ๋„ŒํŠธ ์ „์ฒด๊ฐ€ ์ฃผ์ œ์ธ ๋Œ€ํ™” โ€” ๊ณต์žฅ ์ „์ฒด ๋ฌธ๋‹ต์ฒ˜๋Ÿผ ๋” ์ข๊ฒŒ ๊ฐ€๋ฆฌํ‚ฌ ๊ฒƒ์ด ์—†์„ ๋•Œ\n * none ์•ต์ปค๋กœ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๋Œ€ํ™”. ใ€Œ์•„์ง ์•„๋ฌด๊ฒƒ๋„ ๊ณ ๋ฅด์ง€ ์•Š์•˜๋‹คใ€๋Š” ํ™”๋ฉด ์ƒํƒœ๋‹ค\n */\n// Figure-specific history requested explicitly: reuse the existing varchar\n// anchor columns, without writing Figure ids into the legacy boardId column.\nexport const SESSION_ANCHOR_TYPES = ['board', 'figure', 'application', 'space', 'instance', 'domain', 'none'] as const\n\nexport type SessionAnchorType = (typeof SESSION_ANCHOR_TYPES)[number]\n\nexport function isSessionAnchorType(value: unknown): value is SessionAnchorType {\n return typeof value === 'string' && (SESSION_ANCHOR_TYPES as readonly string[]).includes(value)\n}\n\nfunction assertAnchorType(anchorType: string | undefined, where: string): SessionAnchorType {\n const type = anchorType || 'none'\n if (!isSessionAnchorType(type)) {\n throw new Error(\n `[board-ai/session-anchor] ${where}: '${type}' is not an anchor type. ` +\n `Allowed: ${SESSION_ANCHOR_TYPES.join(' ยท ')}. ` +\n 'The anchor decides which conversations are listed together and which history a reply may see, ' +\n 'so an unrecognised value would write a session the inbox can never find. ' +\n 'If a new kind of subject is needed, raise it โ€” this set is stored in rows.'\n )\n }\n return type\n}\n\n/** ์•ต์ปค โ†’ ์ €์žฅํ•  ์ปฌ๋Ÿผ ๊ฐ’. ๋ณด๋“œ ์•ต์ปค๋Š” boardId ์—๋„ ๊ฐ™์€ ๊ฐ’์„ ๋„ฃ์–ด ๊ธฐ์กด ์กฐํšŒ๊ฐ€ ๊ทธ๋Œ€๋กœ ๋™์ž‘ํ•˜๊ฒŒ ํ•œ๋‹ค. */\nexport function anchorColumns(\n anchorType: string | undefined,\n anchorId: string | undefined\n): { anchorType: SessionAnchorType; anchorId?: string; boardId?: string } {\n const type = assertAnchorType(anchorType, 'anchorColumns')\n const id = anchorId || undefined\n if (type !== 'none' && !id) {\n throw new Error(\n `[board-ai/session-anchor] anchorColumns: anchor '${type}' was given no id. ` +\n 'An anchor is a pointer, so this would store NULL while the lookup searches for the id it was ' +\n 'given โ€” the session would not be found by its own anchor and every call would create another. ' +\n \"Use 'none' for a conversation that is deliberately not addressable by anchor.\"\n )\n }\n return { anchorType: type, anchorId: id, boardId: type === 'board' ? id : undefined }\n}\n\n/**\n * ์•ต์ปค ์กฐํšŒ ์กฐ๊ฑด. ๋ณด๋“œ ์•ต์ปค๋Š” **์•ต์ปค ์ปฌ๋Ÿผ์ด ๋นˆ ์˜› ํ–‰๋„ ์žก์•„์•ผ** ํ•˜๋ฏ€๋กœ boardId ๋กœ ์ฐพ๋Š”๋‹ค\n * (dual-write ๋•๋ถ„์— ์‹ ๊ทœ ํ–‰๋„ ๊ฐ™์€ ์กฐ๊ฑด์— ๊ฑธ๋ฆฐ๋‹ค). ๊ทธ ๋ฐ– ์•ต์ปค๋Š” ์•ต์ปค ์ปฌ๋Ÿผ์œผ๋กœ ์ฐพ๋Š”๋‹ค.\n *\n * ์—ฌ๊ธฐ์„œ๋„ ์•ต์ปค ์ด๋ฆ„์„ ๊ฒ€์‚ฌํ•œ๋‹ค โ€” ์ฝ๊ธฐ ๊ฒฝ๋กœ์—์„œ ํ‹€๋ฆฐ ์ด๋ฆ„์€ ์˜ค๋ฅ˜๊ฐ€ ์•„๋‹ˆ๋ผ **0 ํ–‰**์œผ๋กœ ๋‚˜์˜ค๊ณ ,\n * ํ™”๋ฉด์€ ใ€Œ๋Œ€ํ™”๊ฐ€ ์—†๋‹คใ€๋กœ ๊ทธ๋ฆฐ๋‹ค. ๊ทธ ์ฆ์ƒ์œผ๋กœ๋Š” ์›์ธ์„ ๋ชป ์ฐพ๋Š”๋‹ค.\n */\n/**\n * ์ด ์•ต์ปค์˜ ์„ธ์…˜์„ ์ฐพ๋Š” ์กฐ๊ฑด โ€” `kind` ๋ฅผ ์ฃผ๋ฉด **๊ทธ ๋Œ€ํ™”์˜** ์„ธ์…˜๋งŒ ๋ณธ๋‹ค(ADR-0072 ๊ฒฐ์ • 3).\n *\n * ํ•œ ๋Œ€์ƒ ์œ„์— ๋Œ€ํ™”๊ฐ€ ๋‘˜ ์„ค ์ˆ˜ ์žˆ๊ฒŒ ๋˜๋ฉด์„œ(A17), ์•ต์ปค๋งŒ์œผ๋กœ ์ฐพ์œผ๋ฉด ์ €์ž‘ ๋Œ€ํ™”๋ฅผ ์ฐพ๋Š” ์ž๋ฆฌ์—์„œ ์šด์˜\n * ๋Œ€ํ™”๊ฐ€ ์žกํžŒ๋‹ค. figure ์—์„œ ๊ทธ๊ฒƒ์ด ํ™”๋ฉด์œผ๋กœ ๋‚˜์™”๋‹ค โ€” ๋ชจ๋“œ๋ฅผ ๋ฐ”๊ฟจ๋Š”๋ฐ ๊ฐ™์€ ์„ธ์…˜์ด ๊ทธ๋Œ€๋กœ ๋‚จ์•„ ์ƒˆ ๋ง์ด\n * ์•ž ๋Œ€ํ™”์— ๋ถ™์—ˆ๋‹ค(2026-09-18, Figure ๋ ˆ์ธ :3300).\n *\n * `kind` ๋ฅผ ์•ˆ ์ฃผ๋ฉด ์•ต์ปค์˜ ๋Œ€ํ™”๋ฅผ ๋ชจ๋‘ ๋ณธ๋‹ค โ€” ๋ชฉ๋ก์ฒ˜๋Ÿผ ๋Œ€ํ™”๋ฅผ ๊ฐ€๋ฆฌ์ง€ ์•Š๊ณ  ํ›‘๋Š” ์ž๋ฆฌ๋‹ค.\n */\nexport function anchorWhere(domainId: string, anchorType: string, anchorId: string, kind?: string): any {\n const type = assertAnchorType(anchorType, 'anchorWhere')\n const where = type === 'board' ? { boardId: anchorId, domain: { id: domainId } } : { anchorType: type, anchorId, domain: { id: domainId } }\n return kind ? { ...where, kind } : where\n}\n"]}
@@ -2,6 +2,8 @@
2
2
  export interface InboxSessionRow {
3
3
  id?: string;
4
4
  name?: string;
5
+ /** ์–ด๋А ๋Œ€ํ™”์ธ๊ฐ€ โ€” ๋ชฉ๋ก์ด ๋Œ€ํ™”๋ฅผ ๊ฐ€๋ฆฌ์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ณ ๋ฅธ ๋’ค ํ™”๋ฉด์ด ๋”ฐ๋ผ๊ฐˆ ์ˆ˜ ์žˆ๊ฒŒ ํ•จ๊ป˜ ๋‚ธ๋‹ค. */
6
+ kind?: string;
5
7
  anchorType?: string;
6
8
  anchorId?: string;
7
9
  boardId?: string;