@yuaone/core 0.1.0

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 (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,887 @@
1
+ /**
2
+ * @module persona
3
+ * @description YUAN Persona & User Adaptation System.
4
+ *
5
+ * YUAN의 고유 페르소나(톤, 스타일, 원칙)를 정의하고,
6
+ * 유저의 코딩 스타일·커뮤니케이션 패턴·작업 습관을 학습하여
7
+ * 응답을 자동으로 맞춤 조정한다.
8
+ *
9
+ * 학습 방식:
10
+ * - 유저 메시지 분석 → 형식/기술 수준/언어 혼용 패턴 추출
11
+ * - 유저 코드 분석 → 들여쓰기/따옴표/세미콜론/네이밍 관습 추출
12
+ * - 명시적 규칙 → 유저가 직접 지정한 선호 사항
13
+ * - 추론 규칙 → 반복 관찰에서 자동 추론 (minSamples 이상)
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * const pm = new PersonaManager({ userId: "user-123", enableLearning: true });
18
+ * await pm.loadProfile();
19
+ *
20
+ * pm.analyzeUserMessage("ㅇㅇ 그거 pnpm으로 해줘 ㄱㄱ");
21
+ * pm.analyzeUserCode('const foo = "bar";\n', "src/index.ts");
22
+ * pm.updateProfile();
23
+ *
24
+ * const prompt = pm.buildPersonaPrompt();
25
+ * await pm.saveProfile();
26
+ * ```
27
+ */
28
+ import { readFile, writeFile, access, mkdir } from "node:fs/promises";
29
+ import { join, dirname, extname } from "node:path";
30
+ import { randomUUID } from "node:crypto";
31
+ import { homedir } from "node:os";
32
+ // ─── Constants ───
33
+ const DEFAULT_PROFILE_DIR = join(homedir(), ".yuan", "profiles");
34
+ const CONFIDENCE_INCREMENT = 0.1;
35
+ const MAX_CONFIDENCE = 1.0;
36
+ const MAX_EXAMPLES_PER_RULE = 5;
37
+ /** 한국어 축약어 패턴 */
38
+ const KO_ABBREVIATIONS = ["ㅇㅇ", "ㄱㄱ", "ㅋㅋ", "ㅎㅎ", "ㄴㄴ", "ㅠㅠ", "ㄷㄷ", "ㅈㅈ", "ㅇㅋ", "ㅊㅊ"];
39
+ /** 격식체 마커 (한국어) */
40
+ const FORMAL_MARKERS = ["습니다", "합니다", "입니다", "주세요", "드리", "겠습"];
41
+ /** 비격식 마커 (한국어) */
42
+ const CASUAL_MARKERS = ["해줘", "해봐", "할게", "하자", "ㄱ", "함", "셈", "임", "잇"];
43
+ /** 기술 용어 (영어) */
44
+ const TECH_TERMS = [
45
+ "async", "await", "import", "export", "api", "endpoint", "deploy",
46
+ "commit", "pr", "merge", "rebase", "pipeline", "ci/cd", "docker",
47
+ "kubernetes", "webpack", "vite", "typescript", "interface", "generic",
48
+ "middleware", "mutation", "query", "schema", "migration", "orm",
49
+ "ssr", "csr", "sse", "websocket", "graphql", "rest", "grpc",
50
+ ];
51
+ /** 이모지 패턴 */
52
+ const EMOJI_REGEX = /[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2600}-\u{26FF}\u{2700}-\u{27BF}]/u;
53
+ /** 한국어 문자 범위 */
54
+ const KOREAN_REGEX = /[\uAC00-\uD7AF\u3131-\u318E]/;
55
+ /** 영어 문자 범위 */
56
+ const ENGLISH_REGEX = /[a-zA-Z]/;
57
+ // ─── PersonaManager ───
58
+ /**
59
+ * YUAN 페르소나 & 유저 적응 관리자.
60
+ *
61
+ * YUAN의 기본 페르소나를 정의하고, 유저와의 상호작용에서
62
+ * 코딩 스타일·커뮤니케이션 패턴·작업 습관을 학습하여
63
+ * 시스템 프롬프트를 자동으로 조정한다.
64
+ */
65
+ export class PersonaManager {
66
+ config;
67
+ persona;
68
+ profile;
69
+ /** Maximum observations kept in memory */
70
+ static MAX_OBSERVATIONS = 200;
71
+ /** 코드 관찰 기록 (추론용) */
72
+ codeObservations = [];
73
+ /** 메시지 관찰 기록 (추론용) */
74
+ messageObservations = [];
75
+ constructor(config) {
76
+ // Sanitize userId to prevent path traversal in profile file paths
77
+ const safeUserId = config.userId.replace(/[^a-zA-Z0-9_\-]/g, "_");
78
+ this.config = {
79
+ userId: safeUserId,
80
+ profilePath: config.profilePath ?? join(DEFAULT_PROFILE_DIR, `${safeUserId}.json`),
81
+ enableLearning: config.enableLearning ?? true,
82
+ minSamplesForInference: config.minSamplesForInference ?? 5,
83
+ maxRules: config.maxRules ?? 50,
84
+ };
85
+ this.persona = this.createDefaultPersona();
86
+ this.profile = this.createDefaultProfile();
87
+ }
88
+ // ─── Persona ───
89
+ /** YUAN의 기본 페르소나를 반환한다. */
90
+ getPersona() {
91
+ return { ...this.persona };
92
+ }
93
+ /**
94
+ * 시스템 프롬프트에 삽입할 페르소나 + 유저 선호 프롬프트를 생성한다.
95
+ *
96
+ * @returns 시스템 프롬프트 섹션 문자열
97
+ */
98
+ buildPersonaPrompt() {
99
+ const sections = [];
100
+ // YUAN Identity
101
+ sections.push("## YUAN Identity");
102
+ sections.push(`You are ${this.persona.name}, ${this.persona.role}.`);
103
+ sections.push(`Tone: ${this.describeTone(this.persona.tone)}.`);
104
+ sections.push(`Style: ${this.describeStyle(this.persona.style)}.`);
105
+ sections.push(`Language: ${this.describeLanguage(this.persona.language, this.profile.communication.language)}.`);
106
+ sections.push("");
107
+ // Principles
108
+ if (this.persona.principles.length > 0) {
109
+ sections.push("### Principles");
110
+ for (const p of this.persona.principles) {
111
+ sections.push(`- ${p}`);
112
+ }
113
+ sections.push("");
114
+ }
115
+ // User Preferences
116
+ const cs = this.profile.codingStyle;
117
+ const cm = this.profile.communication;
118
+ const hasKnownStyle = cs.indentation !== "unknown" || cs.quotes !== "unknown";
119
+ const hasKnownComm = cm.formality > 0 || cm.techLevel > 0;
120
+ if (hasKnownStyle || hasKnownComm) {
121
+ sections.push("## User Preferences (Learned)");
122
+ if (hasKnownStyle) {
123
+ const parts = [];
124
+ if (cs.indentation !== "unknown")
125
+ parts.push(this.describeIndentation(cs.indentation));
126
+ if (cs.quotes !== "unknown")
127
+ parts.push(`${cs.quotes} quotes`);
128
+ if (cs.semicolons !== null)
129
+ parts.push(cs.semicolons ? "semicolons" : "no semicolons");
130
+ if (cs.namingConvention !== "unknown")
131
+ parts.push(cs.namingConvention);
132
+ if (cs.trailingComma !== "unknown")
133
+ parts.push(`trailing comma: ${cs.trailingComma}`);
134
+ if (parts.length > 0) {
135
+ sections.push(`- Coding: ${parts.join(", ")}`);
136
+ }
137
+ }
138
+ if (hasKnownComm) {
139
+ const commParts = [];
140
+ commParts.push(`formality: ${cm.formality.toFixed(1)}`);
141
+ commParts.push(`tech level: ${this.describeTechLevel(cm.techLevel)}`);
142
+ commParts.push(`prefers ${cm.preferredResponseLength} responses`);
143
+ sections.push(`- Communication: ${commParts.join(", ")}`);
144
+ }
145
+ sections.push("");
146
+ }
147
+ // Rules
148
+ const allRules = [...this.profile.explicitRules, ...this.profile.inferredRules]
149
+ .filter((r) => r.confidence >= 0.5)
150
+ .sort((a, b) => b.confidence - a.confidence);
151
+ if (allRules.length > 0) {
152
+ sections.push("### User Rules");
153
+ for (const r of allRules) {
154
+ const tag = r.source === "explicit" ? "" : ` (inferred, ${(r.confidence * 100).toFixed(0)}%)`;
155
+ sections.push(`- ${r.rule}${tag}`);
156
+ }
157
+ sections.push("");
158
+ }
159
+ return sections.join("\n").trim();
160
+ }
161
+ /**
162
+ * 유저 프로필 기반으로 응답 스타일 가이드라인을 생성한다.
163
+ *
164
+ * @returns 응답 가이드라인 문자열
165
+ */
166
+ getResponseGuidelines() {
167
+ const cm = this.profile.communication;
168
+ const lines = ["## Response Guidelines"];
169
+ // Language
170
+ if (cm.language === "ko") {
171
+ lines.push("- Respond in Korean.");
172
+ }
173
+ else if (cm.language === "en") {
174
+ lines.push("- Respond in English.");
175
+ }
176
+ else {
177
+ lines.push("- Mix Korean and English naturally, matching the user's pattern.");
178
+ }
179
+ // Formality
180
+ if (cm.formality < 0.3) {
181
+ lines.push("- Use casual tone. Short sentences. Skip pleasantries.");
182
+ }
183
+ else if (cm.formality > 0.7) {
184
+ lines.push("- Use formal, polite tone with complete sentences.");
185
+ }
186
+ else {
187
+ lines.push("- Use a balanced, professional but approachable tone.");
188
+ }
189
+ // Verbosity
190
+ if (cm.verbosity < 0.3) {
191
+ lines.push("- Be concise. Code over explanation. Minimal commentary.");
192
+ }
193
+ else if (cm.verbosity > 0.7) {
194
+ lines.push("- Provide detailed explanations with context and rationale.");
195
+ }
196
+ else {
197
+ lines.push("- Explain key decisions briefly. Focus on what matters.");
198
+ }
199
+ // Tech level
200
+ if (cm.techLevel > 0.7) {
201
+ lines.push("- Assume expert knowledge. Skip basic explanations.");
202
+ }
203
+ else if (cm.techLevel < 0.3) {
204
+ lines.push("- Explain technical concepts. Provide examples.");
205
+ }
206
+ // Emoji
207
+ if (!cm.usesEmoji) {
208
+ lines.push("- Do not use emojis in responses.");
209
+ }
210
+ // Abbreviations
211
+ if (cm.usesAbbreviations) {
212
+ lines.push("- Korean abbreviations (ㅇㅇ, ㄱㄱ) are acceptable.");
213
+ }
214
+ return lines.join("\n");
215
+ }
216
+ // ─── User Profile ───
217
+ /** 현재 유저 프로필을 반환한다 (복사본). */
218
+ getProfile() {
219
+ return JSON.parse(JSON.stringify(this.profile));
220
+ }
221
+ /**
222
+ * 디스크에서 유저 프로필을 로드한다.
223
+ * 파일이 없으면 기본 프로필을 반환한다.
224
+ *
225
+ * @returns 로드된 유저 프로필
226
+ */
227
+ async loadProfile() {
228
+ try {
229
+ await access(this.config.profilePath);
230
+ const raw = await readFile(this.config.profilePath, "utf-8");
231
+ const data = JSON.parse(raw);
232
+ this.profile = data;
233
+ }
234
+ catch {
235
+ // 파일 없음 — 기본 프로필 유지
236
+ this.profile = this.createDefaultProfile();
237
+ }
238
+ return this.getProfile();
239
+ }
240
+ /**
241
+ * 현재 유저 프로필을 디스크에 저장한다.
242
+ * 디렉토리가 없으면 자동 생성한다.
243
+ */
244
+ async saveProfile() {
245
+ const dir = dirname(this.config.profilePath);
246
+ try {
247
+ await access(dir);
248
+ }
249
+ catch {
250
+ await mkdir(dir, { recursive: true });
251
+ }
252
+ const json = JSON.stringify(this.profile, null, 2);
253
+ await writeFile(this.config.profilePath, json, "utf-8");
254
+ }
255
+ // ─── Learning ───
256
+ /**
257
+ * 유저 메시지를 분석하여 커뮤니케이션 패턴을 학습한다.
258
+ *
259
+ * @param message 유저 메시지
260
+ * @returns 분석 결과
261
+ */
262
+ analyzeUserMessage(message) {
263
+ if (!this.config.enableLearning) {
264
+ return this.createEmptyAnalysis(message);
265
+ }
266
+ const language = this.detectLanguageMix(message);
267
+ const usesEmoji = EMOJI_REGEX.test(message);
268
+ const usesAbbreviations = this.detectAbbreviations(message);
269
+ const formality = this.analyzeFormality([message]);
270
+ const techLevel = this.analyzeTechLevel([message]);
271
+ const verbosity = Math.min(message.length / 500, 1.0);
272
+ const detectedPatterns = [];
273
+ // Pattern detection
274
+ if (usesAbbreviations) {
275
+ const found = KO_ABBREVIATIONS.filter((ab) => message.includes(ab));
276
+ for (const ab of found) {
277
+ detectedPatterns.push(`uses "${ab}"`);
278
+ }
279
+ }
280
+ if (language === "mixed") {
281
+ detectedPatterns.push("mixes Korean and English");
282
+ }
283
+ if (usesEmoji) {
284
+ detectedPatterns.push("uses emoji");
285
+ }
286
+ const observation = {
287
+ formality,
288
+ techLevel,
289
+ verbosity,
290
+ language,
291
+ usesEmoji,
292
+ usesAbbreviations,
293
+ length: message.length,
294
+ patterns: detectedPatterns,
295
+ };
296
+ if (this.messageObservations.length >= PersonaManager.MAX_OBSERVATIONS) {
297
+ this.messageObservations = this.messageObservations.slice(-Math.floor(PersonaManager.MAX_OBSERVATIONS / 2));
298
+ }
299
+ this.messageObservations.push(observation);
300
+ // Update profile communication incrementally
301
+ this.profile.communication.formality = this.mergeAnalysis(this.profile.communication.formality, formality, 0.3);
302
+ this.profile.communication.techLevel = this.mergeAnalysis(this.profile.communication.techLevel, techLevel, 0.3);
303
+ this.profile.communication.verbosity = this.mergeAnalysis(this.profile.communication.verbosity, verbosity, 0.2);
304
+ this.profile.communication.language = language;
305
+ this.profile.communication.usesEmoji = usesEmoji || this.profile.communication.usesEmoji;
306
+ this.profile.communication.usesAbbreviations = usesAbbreviations || this.profile.communication.usesAbbreviations;
307
+ // Update response length preference
308
+ if (verbosity < 0.3) {
309
+ this.profile.communication.preferredResponseLength = "short";
310
+ }
311
+ else if (verbosity > 0.7) {
312
+ this.profile.communication.preferredResponseLength = "long";
313
+ }
314
+ else {
315
+ this.profile.communication.preferredResponseLength = "medium";
316
+ }
317
+ this.profile.totalInteractions++;
318
+ this.profile.lastInteraction = Date.now();
319
+ return {
320
+ formality,
321
+ techLevel,
322
+ verbosity,
323
+ language,
324
+ usesEmoji,
325
+ usesAbbreviations,
326
+ avgMessageLength: message.length,
327
+ detectedPatterns,
328
+ };
329
+ }
330
+ /**
331
+ * 유저의 코드를 분석하여 코딩 스타일을 학습한다.
332
+ *
333
+ * @param code 코드 문자열
334
+ * @param filePath 파일 경로 (확장자 기반 필터링용)
335
+ */
336
+ analyzeUserCode(code, filePath) {
337
+ if (!this.config.enableLearning)
338
+ return;
339
+ // Only analyze code files
340
+ const ext = extname(filePath).toLowerCase();
341
+ const codeExtensions = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".mts", ".vue", ".svelte"];
342
+ if (!codeExtensions.includes(ext))
343
+ return;
344
+ if (code.trim().length === 0)
345
+ return;
346
+ const observation = {
347
+ indentation: this.detectIndentation(code),
348
+ quotes: this.detectQuoteStyle(code),
349
+ semicolons: this.detectSemicolons(code),
350
+ namingConvention: this.detectNamingConvention(code),
351
+ trailingComma: this.detectTrailingComma(code),
352
+ lineLength: this.detectMaxLineLength(code),
353
+ };
354
+ if (this.codeObservations.length >= PersonaManager.MAX_OBSERVATIONS) {
355
+ this.codeObservations = this.codeObservations.slice(-Math.floor(PersonaManager.MAX_OBSERVATIONS / 2));
356
+ }
357
+ this.codeObservations.push(observation);
358
+ // Update profile coding style incrementally
359
+ this.profile.codingStyle.indentation = observation.indentation;
360
+ this.profile.codingStyle.quotes = observation.quotes;
361
+ this.profile.codingStyle.semicolons = observation.semicolons;
362
+ this.profile.codingStyle.namingConvention = observation.namingConvention;
363
+ this.profile.codingStyle.trailingComma = observation.trailingComma;
364
+ // Rolling average for line length
365
+ if (this.profile.codingStyle.maxLineLength === 0) {
366
+ this.profile.codingStyle.maxLineLength = observation.lineLength;
367
+ }
368
+ else {
369
+ this.profile.codingStyle.maxLineLength = Math.round(this.profile.codingStyle.maxLineLength * 0.7 + observation.lineLength * 0.3);
370
+ }
371
+ }
372
+ /**
373
+ * 유저가 명시적으로 지정한 규칙을 추가한다.
374
+ *
375
+ * @param rule 규칙 문자열 (예: "always use pnpm, never npm")
376
+ * @param examples 규칙이 언급된 메시지 예시
377
+ */
378
+ addExplicitRule(rule, examples = []) {
379
+ // Check for duplicate
380
+ const existing = this.profile.explicitRules.find((r) => r.rule.toLowerCase() === rule.toLowerCase());
381
+ if (existing) {
382
+ existing.lastConfirmedAt = Date.now();
383
+ existing.confidence = Math.min(existing.confidence + CONFIDENCE_INCREMENT, MAX_CONFIDENCE);
384
+ for (const ex of examples) {
385
+ if (!existing.examples.includes(ex) && existing.examples.length < MAX_EXAMPLES_PER_RULE) {
386
+ existing.examples.push(ex);
387
+ }
388
+ }
389
+ return;
390
+ }
391
+ const newRule = {
392
+ id: randomUUID(),
393
+ rule,
394
+ source: "explicit",
395
+ confidence: 1.0,
396
+ examples: examples.slice(0, MAX_EXAMPLES_PER_RULE),
397
+ createdAt: Date.now(),
398
+ lastConfirmedAt: Date.now(),
399
+ };
400
+ this.profile.explicitRules.push(newRule);
401
+ this.enforceMaxRules();
402
+ }
403
+ /**
404
+ * 규칙을 ID로 제거한다.
405
+ *
406
+ * @param ruleId 제거할 규칙 ID
407
+ */
408
+ removeRule(ruleId) {
409
+ this.profile.explicitRules = this.profile.explicitRules.filter((r) => r.id !== ruleId);
410
+ this.profile.inferredRules = this.profile.inferredRules.filter((r) => r.id !== ruleId);
411
+ }
412
+ /**
413
+ * 누적된 관찰 데이터를 기반으로 프로필을 업데이트하고 규칙을 추론한다.
414
+ * minSamplesForInference 이상의 관찰이 있을 때만 추론을 실행한다.
415
+ */
416
+ updateProfile() {
417
+ if (!this.config.enableLearning)
418
+ return;
419
+ // Infer rules from code observations
420
+ if (this.codeObservations.length >= this.config.minSamplesForInference) {
421
+ this.inferCodeRules();
422
+ }
423
+ // Infer rules from message observations
424
+ if (this.messageObservations.length >= this.config.minSamplesForInference) {
425
+ this.inferMessageRules();
426
+ }
427
+ this.enforceMaxRules();
428
+ }
429
+ // ─── Code Style Detection ───
430
+ /**
431
+ * 코드에서 들여쓰기 스타일을 감지한다.
432
+ *
433
+ * @param code 소스 코드
434
+ * @returns 감지된 들여쓰기 스타일
435
+ */
436
+ detectIndentation(code) {
437
+ const lines = code.split("\n").filter((l) => l.length > 0 && /^\s+/.test(l));
438
+ let tabs = 0;
439
+ let spaces2 = 0;
440
+ let spaces4 = 0;
441
+ for (const line of lines) {
442
+ const match = line.match(/^(\s+)/);
443
+ if (!match)
444
+ continue;
445
+ const ws = match[1];
446
+ if (ws.includes("\t")) {
447
+ tabs++;
448
+ }
449
+ else {
450
+ const len = ws.length;
451
+ if (len % 4 === 0)
452
+ spaces4++;
453
+ if (len % 2 === 0)
454
+ spaces2++;
455
+ }
456
+ }
457
+ if (tabs > spaces2 && tabs > spaces4)
458
+ return "tabs";
459
+ if (spaces4 >= spaces2)
460
+ return "spaces-4";
461
+ return "spaces-2";
462
+ }
463
+ /**
464
+ * 코드에서 따옴표 스타일을 감지한다.
465
+ * 템플릿 리터럴(백틱)과 import 구문 내 따옴표는 포함하되
466
+ * 실제 사용 빈도로 판단한다.
467
+ *
468
+ * @param code 소스 코드
469
+ * @returns 감지된 따옴표 스타일
470
+ */
471
+ detectQuoteStyle(code) {
472
+ // Count string literals (simple heuristic: unescaped quotes not inside template literals)
473
+ const singleCount = (code.match(/(?<!\\)'/g) || []).length;
474
+ const doubleCount = (code.match(/(?<!\\)"/g) || []).length;
475
+ return singleCount >= doubleCount ? "single" : "double";
476
+ }
477
+ /**
478
+ * 코드에서 세미콜론 사용 여부를 감지한다.
479
+ *
480
+ * @param code 소스 코드
481
+ * @returns true=세미콜론 사용, false=미사용
482
+ */
483
+ detectSemicolons(code) {
484
+ const lines = code.split("\n")
485
+ .map((l) => l.trim())
486
+ .filter((l) => l.length > 0 && !l.startsWith("//") && !l.startsWith("*") && !l.startsWith("/*"));
487
+ // Count lines ending with semicolons vs those that don't
488
+ // Exclude lines that end with { } , or are blank
489
+ const statementLines = lines.filter((l) => {
490
+ const last = l[l.length - 1];
491
+ return last !== "{" && last !== "}" && last !== "," && last !== "(" && last !== ")";
492
+ });
493
+ if (statementLines.length === 0)
494
+ return true;
495
+ const withSemicolon = statementLines.filter((l) => l.endsWith(";")).length;
496
+ return withSemicolon / statementLines.length > 0.5;
497
+ }
498
+ /**
499
+ * 코드에서 네이밍 관습을 감지한다.
500
+ *
501
+ * @param code 소스 코드
502
+ * @returns 감지된 네이밍 관습
503
+ */
504
+ detectNamingConvention(code) {
505
+ // Extract variable/function names
506
+ const camelCaseRegex = /(?:const|let|var|function)\s+([a-z][a-zA-Z0-9]*)/g;
507
+ const snakeCaseRegex = /(?:const|let|var|function)\s+([a-z][a-z0-9_]*_[a-z0-9_]*)/g;
508
+ const pascalCaseRegex = /(?:class|interface|type|enum)\s+([A-Z][a-zA-Z0-9]*)/g;
509
+ const camelMatches = (code.match(camelCaseRegex) || []).length;
510
+ const snakeMatches = (code.match(snakeCaseRegex) || []).length;
511
+ const pascalMatches = (code.match(pascalCaseRegex) || []).length;
512
+ // For variable naming, camelCase vs snake_case is most relevant
513
+ if (snakeMatches > camelMatches && snakeMatches > pascalMatches)
514
+ return "snake_case";
515
+ if (pascalMatches > camelMatches && pascalMatches > snakeMatches)
516
+ return "PascalCase";
517
+ return "camelCase";
518
+ }
519
+ /**
520
+ * 코드에서 트레일링 콤마 사용 패턴을 감지한다.
521
+ *
522
+ * @param code 소스 코드
523
+ * @returns 감지된 트레일링 콤마 스타일
524
+ */
525
+ detectTrailingComma(code) {
526
+ // Look for patterns like:
527
+ // value,\n} or value,\n]
528
+ const trailingCommaPattern = /,\s*\n\s*[}\]]/g;
529
+ const noTrailingPattern = /[^,\s]\s*\n\s*[}\]]/g;
530
+ const trailingCount = (code.match(trailingCommaPattern) || []).length;
531
+ const noTrailingCount = (code.match(noTrailingPattern) || []).length;
532
+ if (trailingCount === 0 && noTrailingCount === 0)
533
+ return "es5";
534
+ if (trailingCount === 0)
535
+ return "none";
536
+ const ratio = trailingCount / (trailingCount + noTrailingCount);
537
+ if (ratio > 0.8)
538
+ return "all";
539
+ if (ratio > 0.3)
540
+ return "es5";
541
+ return "none";
542
+ }
543
+ // ─── Speech Pattern Analysis ───
544
+ /**
545
+ * 메시지 목록에서 형식성(formality) 수준을 분석한다.
546
+ *
547
+ * @param messages 유저 메시지 목록
548
+ * @returns 0(극캐주얼) ~ 1(격식체)
549
+ */
550
+ analyzeFormality(messages) {
551
+ if (messages.length === 0)
552
+ return 0.5;
553
+ let totalFormal = 0;
554
+ let totalCasual = 0;
555
+ for (const msg of messages) {
556
+ for (const marker of FORMAL_MARKERS) {
557
+ if (msg.includes(marker))
558
+ totalFormal++;
559
+ }
560
+ for (const marker of CASUAL_MARKERS) {
561
+ if (msg.includes(marker))
562
+ totalCasual++;
563
+ }
564
+ // Abbreviations are casual
565
+ if (this.detectAbbreviations(msg))
566
+ totalCasual += 2;
567
+ }
568
+ const total = totalFormal + totalCasual;
569
+ if (total === 0)
570
+ return 0.5;
571
+ return Math.min(totalFormal / total, 1.0);
572
+ }
573
+ /**
574
+ * 메시지 목록에서 기술 수준을 분석한다.
575
+ *
576
+ * @param messages 유저 메시지 목록
577
+ * @returns 0(초보) ~ 1(전문가)
578
+ */
579
+ analyzeTechLevel(messages) {
580
+ if (messages.length === 0)
581
+ return 0.5;
582
+ let techTermCount = 0;
583
+ let totalWords = 0;
584
+ for (const msg of messages) {
585
+ const words = msg.toLowerCase().split(/\s+/);
586
+ totalWords += words.length;
587
+ for (const word of words) {
588
+ if (TECH_TERMS.includes(word.replace(/[^a-z/]/g, ""))) {
589
+ techTermCount++;
590
+ }
591
+ }
592
+ }
593
+ if (totalWords === 0)
594
+ return 0.5;
595
+ // Tech density — capped at 1.0
596
+ const density = techTermCount / totalWords;
597
+ return Math.min(density * 10, 1.0);
598
+ }
599
+ /**
600
+ * 메시지에서 한국어/영어 혼용 패턴을 감지한다.
601
+ *
602
+ * @param message 유저 메시지
603
+ * @returns 감지된 언어 종류
604
+ */
605
+ detectLanguageMix(message) {
606
+ const chars = message.replace(/\s+/g, "");
607
+ if (chars.length === 0)
608
+ return "en";
609
+ let koCount = 0;
610
+ let enCount = 0;
611
+ for (const ch of chars) {
612
+ if (KOREAN_REGEX.test(ch))
613
+ koCount++;
614
+ else if (ENGLISH_REGEX.test(ch))
615
+ enCount++;
616
+ }
617
+ const total = koCount + enCount;
618
+ if (total === 0)
619
+ return "en";
620
+ const koRatio = koCount / total;
621
+ const enRatio = enCount / total;
622
+ if (koRatio > 0.8)
623
+ return "ko";
624
+ if (enRatio > 0.8)
625
+ return "en";
626
+ return "mixed";
627
+ }
628
+ /**
629
+ * 메시지에서 한국어 축약어 사용을 감지한다.
630
+ *
631
+ * @param message 유저 메시지
632
+ * @returns 축약어 사용 여부
633
+ */
634
+ detectAbbreviations(message) {
635
+ return KO_ABBREVIATIONS.some((ab) => message.includes(ab));
636
+ }
637
+ // ─── Private ───
638
+ /** 기본 유저 프로필을 생성한다. */
639
+ createDefaultProfile() {
640
+ return {
641
+ userId: this.config.userId,
642
+ codingStyle: {
643
+ indentation: "unknown",
644
+ quotes: "unknown",
645
+ semicolons: null,
646
+ trailingComma: "unknown",
647
+ namingConvention: "unknown",
648
+ commentStyle: "unknown",
649
+ maxLineLength: 0,
650
+ preferredPatterns: [],
651
+ },
652
+ communication: {
653
+ formality: 0.5,
654
+ techLevel: 0.5,
655
+ verbosity: 0.5,
656
+ language: "mixed",
657
+ usesEmoji: false,
658
+ usesAbbreviations: false,
659
+ preferredResponseLength: "medium",
660
+ },
661
+ workPatterns: {
662
+ preferredTools: [],
663
+ commonTasks: [],
664
+ reviewStrictness: "moderate",
665
+ commitStyle: "conventional",
666
+ testingPreference: "unknown",
667
+ },
668
+ explicitRules: [],
669
+ inferredRules: [],
670
+ totalInteractions: 0,
671
+ lastInteraction: Date.now(),
672
+ createdAt: Date.now(),
673
+ };
674
+ }
675
+ /** 기본 YUAN 페르소나를 생성한다. */
676
+ createDefaultPersona() {
677
+ return {
678
+ name: "YUAN",
679
+ role: "a senior software engineer and autonomous coding agent",
680
+ tone: "professional",
681
+ style: "action-first",
682
+ language: "auto",
683
+ principles: [
684
+ "Read before writing. Understand context first.",
685
+ "Minimal, focused changes. No unnecessary refactoring.",
686
+ "Verify with build/test after every change.",
687
+ "Be transparent about what you changed and why.",
688
+ "Never expose secrets or credentials.",
689
+ "Ask before destructive operations.",
690
+ ],
691
+ };
692
+ }
693
+ /** 코드 관찰에서 규칙을 추론한다. */
694
+ inferCodeRules() {
695
+ const obs = this.codeObservations;
696
+ const total = obs.length;
697
+ // Indentation rule
698
+ const indentCounts = this.countValues(obs.map((o) => o.indentation));
699
+ const topIndent = this.topValue(indentCounts);
700
+ if (topIndent && indentCounts[topIndent] / total > 0.7) {
701
+ this.addInferredRule(`Use ${this.describeIndentation(topIndent)} for indentation`, indentCounts[topIndent] / total);
702
+ }
703
+ // Quote rule
704
+ const quoteCounts = this.countValues(obs.map((o) => o.quotes));
705
+ const topQuote = this.topValue(quoteCounts);
706
+ if (topQuote && quoteCounts[topQuote] / total > 0.7) {
707
+ this.addInferredRule(`Use ${topQuote} quotes for strings`, quoteCounts[topQuote] / total);
708
+ }
709
+ // Semicolon rule
710
+ const semiTrue = obs.filter((o) => o.semicolons).length;
711
+ const semiFalse = obs.filter((o) => !o.semicolons).length;
712
+ if (semiTrue / total > 0.7) {
713
+ this.addInferredRule("Use semicolons at end of statements", semiTrue / total);
714
+ }
715
+ else if (semiFalse / total > 0.7) {
716
+ this.addInferredRule("No semicolons (ASI style)", semiFalse / total);
717
+ }
718
+ // Naming convention rule
719
+ const namingCounts = this.countValues(obs.map((o) => o.namingConvention));
720
+ const topNaming = this.topValue(namingCounts);
721
+ if (topNaming && namingCounts[topNaming] / total > 0.7) {
722
+ this.addInferredRule(`Use ${topNaming} for variable/function naming`, namingCounts[topNaming] / total);
723
+ }
724
+ }
725
+ /** 메시지 관찰에서 규칙을 추론한다. */
726
+ inferMessageRules() {
727
+ const obs = this.messageObservations;
728
+ const total = obs.length;
729
+ // Language rule
730
+ const langCounts = this.countValues(obs.map((o) => o.language));
731
+ const topLang = this.topValue(langCounts);
732
+ if (topLang && langCounts[topLang] / total > 0.7) {
733
+ const desc = topLang === "ko" ? "Korean" : topLang === "en" ? "English" : "mixed Korean/English";
734
+ this.addInferredRule(`User prefers ${desc} communication`, langCounts[topLang] / total);
735
+ }
736
+ // Abbreviation rule
737
+ const abbrCount = obs.filter((o) => o.usesAbbreviations).length;
738
+ if (abbrCount / total > 0.5) {
739
+ this.addInferredRule("User uses Korean abbreviations — casual style OK", abbrCount / total);
740
+ }
741
+ // Verbosity rule
742
+ const avgVerbosity = obs.reduce((sum, o) => sum + o.verbosity, 0) / total;
743
+ if (avgVerbosity < 0.3) {
744
+ this.addInferredRule("User prefers concise messages — keep responses short", 0.5 + avgVerbosity);
745
+ }
746
+ else if (avgVerbosity > 0.7) {
747
+ this.addInferredRule("User writes detailed messages — detailed responses OK", avgVerbosity);
748
+ }
749
+ }
750
+ /**
751
+ * 추론 규칙을 추가하거나 기존 규칙의 confidence를 업데이트한다.
752
+ *
753
+ * @param rule 규칙 문자열
754
+ * @param confidence 초기 확신도
755
+ */
756
+ addInferredRule(rule, confidence) {
757
+ const existing = this.profile.inferredRules.find((r) => r.rule.toLowerCase() === rule.toLowerCase());
758
+ if (existing) {
759
+ existing.confidence = Math.min(existing.confidence + CONFIDENCE_INCREMENT, MAX_CONFIDENCE);
760
+ existing.lastConfirmedAt = Date.now();
761
+ return;
762
+ }
763
+ this.profile.inferredRules.push({
764
+ id: randomUUID(),
765
+ rule,
766
+ source: "inferred",
767
+ confidence: Math.max(0.5, Math.min(confidence, MAX_CONFIDENCE)),
768
+ examples: [],
769
+ createdAt: Date.now(),
770
+ lastConfirmedAt: Date.now(),
771
+ });
772
+ }
773
+ /** 최대 규칙 수를 초과하면 낮은 confidence 규칙을 제거한다. */
774
+ enforceMaxRules() {
775
+ const maxPerType = Math.floor(this.config.maxRules / 2);
776
+ if (this.profile.explicitRules.length > maxPerType) {
777
+ this.profile.explicitRules.sort((a, b) => b.confidence - a.confidence);
778
+ this.profile.explicitRules = this.profile.explicitRules.slice(0, maxPerType);
779
+ }
780
+ if (this.profile.inferredRules.length > maxPerType) {
781
+ this.profile.inferredRules.sort((a, b) => b.confidence - a.confidence);
782
+ this.profile.inferredRules = this.profile.inferredRules.slice(0, maxPerType);
783
+ }
784
+ }
785
+ /**
786
+ * 기존 값과 새 값을 가중 병합한다 (exponential moving average).
787
+ *
788
+ * @param existing 기존 값
789
+ * @param newValue 새 값
790
+ * @param weight 새 값의 가중치 (0-1)
791
+ * @returns 병합된 값
792
+ */
793
+ mergeAnalysis(existing, newValue, weight) {
794
+ return existing * (1 - weight) + newValue * weight;
795
+ }
796
+ /** 코드에서 최대 줄 길이를 감지한다. */
797
+ detectMaxLineLength(code) {
798
+ const lines = code.split("\n");
799
+ let maxLen = 0;
800
+ for (const line of lines) {
801
+ if (line.length > maxLen)
802
+ maxLen = line.length;
803
+ }
804
+ return maxLen;
805
+ }
806
+ /** 빈 분석 결과를 생성한다 (학습 비활성화 시). */
807
+ createEmptyAnalysis(message) {
808
+ return {
809
+ formality: 0.5,
810
+ techLevel: 0.5,
811
+ verbosity: 0.5,
812
+ language: "en",
813
+ usesEmoji: false,
814
+ usesAbbreviations: false,
815
+ avgMessageLength: message.length,
816
+ detectedPatterns: [],
817
+ };
818
+ }
819
+ // ─── Helpers ───
820
+ /** 배열에서 각 값의 출현 횟수를 센다. */
821
+ countValues(values) {
822
+ const counts = {};
823
+ for (const v of values) {
824
+ counts[v] = (counts[v] || 0) + 1;
825
+ }
826
+ return counts;
827
+ }
828
+ /** 가장 많은 값을 반환한다. */
829
+ topValue(counts) {
830
+ let top = null;
831
+ let max = 0;
832
+ for (const [key, count] of Object.entries(counts)) {
833
+ if (count > max) {
834
+ max = count;
835
+ top = key;
836
+ }
837
+ }
838
+ return top;
839
+ }
840
+ /** 톤 설명 문자열을 반환한다. */
841
+ describeTone(tone) {
842
+ switch (tone) {
843
+ case "professional": return "Professional but approachable";
844
+ case "casual": return "Casual and relaxed";
845
+ case "technical": return "Technical and precise";
846
+ case "friendly": return "Friendly and warm";
847
+ }
848
+ }
849
+ /** 스타일 설명 문자열을 반환한다. */
850
+ describeStyle(style) {
851
+ switch (style) {
852
+ case "concise": return "Keep it short, code speaks louder";
853
+ case "detailed": return "Explain thoroughly with context";
854
+ case "action-first": return "Lead with action, explain when asked";
855
+ case "explanation-first": return "Explain the plan, then execute";
856
+ }
857
+ }
858
+ /** 언어 설명 문자열을 반환한다. */
859
+ describeLanguage(personaLang, userLang) {
860
+ if (personaLang === "auto") {
861
+ return `Match the user's language (currently: ${userLang})`;
862
+ }
863
+ switch (personaLang) {
864
+ case "ko": return "Korean";
865
+ case "en": return "English";
866
+ case "mixed": return "Mixed Korean/English";
867
+ default: return "Auto-detect";
868
+ }
869
+ }
870
+ /** 들여쓰기 설명 문자열을 반환한다. */
871
+ describeIndentation(indent) {
872
+ switch (indent) {
873
+ case "tabs": return "tabs";
874
+ case "spaces-2": return "2-space indentation";
875
+ case "spaces-4": return "4-space indentation";
876
+ }
877
+ }
878
+ /** 기술 수준 설명 문자열을 반환한다. */
879
+ describeTechLevel(level) {
880
+ if (level > 0.7)
881
+ return "expert";
882
+ if (level > 0.4)
883
+ return "intermediate";
884
+ return "beginner";
885
+ }
886
+ }
887
+ //# sourceMappingURL=persona.js.map