@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.
- package/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- package/package.json +41 -0
package/dist/persona.js
ADDED
|
@@ -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
|