@hawon/nexus 0.2.0 → 0.3.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/dist/cli/index.js +44 -71
- package/dist/index.js +15 -24
- package/dist/mcp/server.js +7 -6
- package/package.json +1 -1
- package/scripts/benchmark.ts +13 -35
- package/src/cli/index.ts +45 -84
- package/src/index.ts +17 -27
- package/src/mcp/server.ts +8 -7
- package/src/memory-engine/index.ts +4 -6
- package/src/memory-engine/semantic.ts +38 -11
- package/src/promptguard/advanced-rules.ts +122 -5
- package/src/promptguard/entropy.ts +21 -2
- package/src/promptguard/rules.ts +87 -2
- package/src/promptguard/scanner.test.ts +1 -4
- package/src/promptguard/scanner.ts +1 -1
- package/src/promptguard/semantic.ts +19 -4
- package/src/promptguard/token-analysis.ts +17 -5
- package/src/review/analyzer.ts +106 -23
- package/src/skills/index.ts +11 -27
- package/src/testing/health-check.ts +19 -2
- package/src/memory-engine/compressor.ts +0 -97
- package/src/memory-engine/context-window.ts +0 -113
- package/src/memory-engine/store.ts +0 -371
- package/src/memory-engine/types.ts +0 -32
- package/src/skills/context-engine.ts +0 -863
- package/src/skills/extractor.ts +0 -224
- package/src/skills/global-context.ts +0 -731
- package/src/skills/library.ts +0 -189
- package/src/skills/pattern-engine.ts +0 -715
- package/src/skills/render-evolved.ts +0 -160
- package/src/skills/skill-reconciler.ts +0 -699
- package/src/skills/smart-extractor.ts +0 -849
- package/src/skills/types.ts +0 -18
- package/src/skills/wisdom-extractor.ts +0 -737
- package/src/superdev-evolution/index.ts +0 -3
- package/src/superdev-evolution/skill-manager.ts +0 -266
- package/src/superdev-evolution/types.ts +0 -20
|
@@ -1,699 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Skill Reconciler — Evolving Knowledge Through Contradiction
|
|
3
|
-
*
|
|
4
|
-
* The smartest part of the system. When a new skill conflicts with
|
|
5
|
-
* an existing one, it doesn't just overwrite — it asks:
|
|
6
|
-
*
|
|
7
|
-
* "왜 그때는 이랬는데 이번에는 이랬지?"
|
|
8
|
-
*
|
|
9
|
-
* Then it:
|
|
10
|
-
* 1. Finds the DIFFERENCE in context that caused the change
|
|
11
|
-
* 2. Creates CONDITIONAL branches: "When [X], use A; when [Y], use B"
|
|
12
|
-
* 3. Keeps COMMON parts merged
|
|
13
|
-
*
|
|
14
|
-
* Three core mechanisms:
|
|
15
|
-
*
|
|
16
|
-
* A. STRICT QUALITY GATE
|
|
17
|
-
* - Won't create a skill if context is unclear
|
|
18
|
-
* - Requires minimum understanding confidence
|
|
19
|
-
* - Rejects noise, ambiguity, and too-specific patterns
|
|
20
|
-
*
|
|
21
|
-
* B. PREFERENCE LEARNING
|
|
22
|
-
* - "이 상황에서는 A보다 B가 효율적"
|
|
23
|
-
* - Tracks which approaches were chosen over alternatives
|
|
24
|
-
* - Builds preference rankings per situation type
|
|
25
|
-
*
|
|
26
|
-
* C. SKILL RECONCILIATION
|
|
27
|
-
* - Detects when new wisdom contradicts existing
|
|
28
|
-
* - Finds the contextual difference that explains the change
|
|
29
|
-
* - Splits into conditional branches or merges common ground
|
|
30
|
-
*/
|
|
31
|
-
|
|
32
|
-
import type { Wisdom, WisdomBody } from "./wisdom-extractor.js";
|
|
33
|
-
import { semanticSimilarity } from "../memory-engine/semantic.js";
|
|
34
|
-
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
35
|
-
import { join } from "node:path";
|
|
36
|
-
import { createHash } from "node:crypto";
|
|
37
|
-
|
|
38
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
39
|
-
// TYPES
|
|
40
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
41
|
-
|
|
42
|
-
/** A refined skill with conditional branches. */
|
|
43
|
-
export type RefinedSkill = {
|
|
44
|
-
id: string;
|
|
45
|
-
/** Core topic this skill covers. */
|
|
46
|
-
topic: string;
|
|
47
|
-
/** What all branches share in common. */
|
|
48
|
-
commonGround: string;
|
|
49
|
-
/** Conditional branches: different approaches for different contexts. */
|
|
50
|
-
branches: SkillBranch[];
|
|
51
|
-
/** Learned preferences: "A > B when [condition]". */
|
|
52
|
-
preferences: Preference[];
|
|
53
|
-
/** How many times this skill was validated. */
|
|
54
|
-
validations: number;
|
|
55
|
-
/** Overall confidence. */
|
|
56
|
-
confidence: number;
|
|
57
|
-
/** Domain tags. */
|
|
58
|
-
domains: string[];
|
|
59
|
-
/** Version: increments on each reconciliation. */
|
|
60
|
-
version: number;
|
|
61
|
-
createdAt: string;
|
|
62
|
-
updatedAt: string;
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
export type SkillBranch = {
|
|
66
|
-
/** When does this branch apply? */
|
|
67
|
-
condition: string;
|
|
68
|
-
/** What to do. */
|
|
69
|
-
approach: string;
|
|
70
|
-
/** Why this approach for this condition. */
|
|
71
|
-
rationale: string;
|
|
72
|
-
/** Source wisdom IDs that support this branch. */
|
|
73
|
-
sources: string[];
|
|
74
|
-
/** Success count for this branch. */
|
|
75
|
-
successCount: number;
|
|
76
|
-
/** Failure count. */
|
|
77
|
-
failureCount: number;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
export type Preference = {
|
|
81
|
-
/** The situation. */
|
|
82
|
-
situation: string;
|
|
83
|
-
/** Preferred approach. */
|
|
84
|
-
preferred: string;
|
|
85
|
-
/** Less preferred approach. */
|
|
86
|
-
overWhat: string;
|
|
87
|
-
/** Why preferred. */
|
|
88
|
-
reason: string;
|
|
89
|
-
/** How many times this preference was confirmed. */
|
|
90
|
-
confirmations: number;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
export type ReconciliationResult = {
|
|
94
|
-
/** New skills created. */
|
|
95
|
-
created: RefinedSkill[];
|
|
96
|
-
/** Existing skills updated (new branch added). */
|
|
97
|
-
updated: RefinedSkill[];
|
|
98
|
-
/** Wisdoms rejected by quality gate. */
|
|
99
|
-
rejected: { wisdom: Wisdom; reason: string }[];
|
|
100
|
-
/** Preferences learned. */
|
|
101
|
-
preferencesLearned: Preference[];
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
export type SkillLibrary = {
|
|
105
|
-
skills: RefinedSkill[];
|
|
106
|
-
version: number;
|
|
107
|
-
updatedAt: string;
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
111
|
-
// STRICT QUALITY GATE
|
|
112
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
113
|
-
|
|
114
|
-
type QualityCheck = { pass: boolean; reason: string };
|
|
115
|
-
|
|
116
|
-
function qualityGate(wisdom: Wisdom): QualityCheck {
|
|
117
|
-
const { body } = wisdom;
|
|
118
|
-
|
|
119
|
-
// 1. Situation must be meaningful
|
|
120
|
-
// If situation is empty but approach is technical and substantial, derive situation from approach
|
|
121
|
-
if (!body.situation || body.situation.length < 5) {
|
|
122
|
-
// Try to salvage: if approach is very technical and >40 chars, use first line of approach as situation
|
|
123
|
-
const approachFirstLine = (body.approach ?? "").split("\n")[0]?.trim() ?? "";
|
|
124
|
-
const hasTechContent = /overflow|bypass|injection|exploit|vulnerability|encrypt|decrypt|hash|shellcode|rop|heap|stack|deserialization|sandbox|hook|patch|fuzzing|scanner|audit|authentication|authorization|deploy|refactor|migrate|optimize|pipeline|middleware|proxy|gateway|socket|protocol|container/i.test(approachFirstLine);
|
|
125
|
-
if (hasTechContent && approachFirstLine.length > 30) {
|
|
126
|
-
body.situation = approachFirstLine.slice(0, 80);
|
|
127
|
-
// Prevent circular: if salvaged situation is same as approach, reject
|
|
128
|
-
if (body.situation === body.approach.split("\n")[0]?.trim()) {
|
|
129
|
-
return { pass: false, reason: "circular wisdom — situation derived from approach itself" };
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
return { pass: false, reason: "situation too vague — can't determine when this applies" };
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
// 2. Approach must be substantive
|
|
137
|
-
if (!body.approach || body.approach.length < 10) {
|
|
138
|
-
return { pass: false, reason: "approach too short — not enough information to be useful" };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
// 3. Reason must exist
|
|
142
|
-
if (!body.reason || body.reason.length < 5) {
|
|
143
|
-
return { pass: false, reason: "no clear reason — can't learn without understanding why" };
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// 4. Must not be system noise
|
|
147
|
-
const noisePatterns = [
|
|
148
|
-
/task-notification/i, /system-reminder/i, /<[a-z]/i,
|
|
149
|
-
/\[result-id/i, /tool loaded/i, /Using Node/i,
|
|
150
|
-
/npm warn/i, /npm notice/i, /npm error/i,
|
|
151
|
-
];
|
|
152
|
-
const allText = `${body.situation} ${body.approach} ${body.reason}`;
|
|
153
|
-
if (noisePatterns.some((p) => p.test(allText))) {
|
|
154
|
-
return { pass: false, reason: "contains system noise — not a real interaction" };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// 5. Confidence must be above threshold
|
|
158
|
-
if (wisdom.confidence < 0.5) {
|
|
159
|
-
return { pass: false, reason: `confidence too low (${(wisdom.confidence * 100).toFixed(0)}% < 50%)` };
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 6. Must have at least one domain
|
|
163
|
-
if (wisdom.domains.length === 0) {
|
|
164
|
-
return { pass: false, reason: "no domain identified — too generic to be useful" };
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// 7. Situation and approach must be different (not circular)
|
|
168
|
-
const situationWords = new Set(tokenize(body.situation));
|
|
169
|
-
const approachWords = new Set(tokenize(body.approach));
|
|
170
|
-
const overlap = [...situationWords].filter((w) => approachWords.has(w)).length;
|
|
171
|
-
const overlapRatio = overlap / Math.max(situationWords.size, 1);
|
|
172
|
-
if (overlapRatio > 0.8) {
|
|
173
|
-
return { pass: false, reason: "situation and approach are too similar — circular wisdom" };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 8. Situation must not be a raw user message (question, command, etc.)
|
|
177
|
-
const rawMessagePatterns = [
|
|
178
|
-
/^[ㄱ-ㅎㅏ-ㅣ가-힣]{1,5}$/, // Very short Korean (ㅇㅇ, 응, 해줘)
|
|
179
|
-
/^[\w\s]{1,10}\?$/, // Short question
|
|
180
|
-
/^host\d.*games|^nc\s+host|^\d+\.\d+\.\d+\.\d+/i, // Specific addresses/IPs (not the technique)
|
|
181
|
-
/^'.*\.py'|^'.*\.sage'/i, // Specific file invocations
|
|
182
|
-
/^'.*'$/, // Quoted file paths
|
|
183
|
-
];
|
|
184
|
-
if (rawMessagePatterns.some((p) => p.test(body.situation))) {
|
|
185
|
-
return { pass: false, reason: "situation is a raw command/challenge, not a generalizable pattern" };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 9. Approach must be a principle, not a specific action
|
|
189
|
-
const tooSpecificPatterns = [
|
|
190
|
-
/^(Read|Edit|Bash|Write|Grep|Agent|WebSearch)\b/, // Raw tool name
|
|
191
|
-
/^\/home\/|^\/mnt\/|^C:\\/, // File paths
|
|
192
|
-
/^\d{1,5}\.\d{1,5}\.\d{1,5}/, // IP addresses
|
|
193
|
-
];
|
|
194
|
-
if (tooSpecificPatterns.some((p) => p.test(body.approach))) {
|
|
195
|
-
return { pass: false, reason: "approach is a specific action, not a reusable principle" };
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 10. Reject casual chat / emotional expressions / not technical
|
|
199
|
-
const casualPatterns = [
|
|
200
|
-
/미친|개쩐|대박|ㅋㅋ|ㅎㅎ|ㅠㅠ|씨발|부모님|돈이잖/i,
|
|
201
|
-
/야\s|아\s.*맞다|음\s|스티븐|스테인버그/i,
|
|
202
|
-
/lol|wtf|omg|damn|shit/i,
|
|
203
|
-
];
|
|
204
|
-
if (casualPatterns.some((p) => p.test(body.situation))) {
|
|
205
|
-
return { pass: false, reason: "casual chat, not a technical skill" };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
// 10b. Reject vague/short commands that aren't descriptive
|
|
209
|
-
// BUT allow short technical phrases like "CVE 연구", "보안 감사"
|
|
210
|
-
const techShort = /보안|security|취약|exploit|코드|서버|테스트|빌드|배포|디버그|리팩토|설정|분석|스캔|감사|review|deploy|test|build|debug|refactor|analyze|audit|scan|vulnerability/i;
|
|
211
|
-
const vaguePatterns = [
|
|
212
|
-
/^이게\s*뭔/i, /^뭐야/i, /^뭔데/i,
|
|
213
|
-
/^해줘/i, /^해봐/i, /^봐봐/i, /^봐줘/i,
|
|
214
|
-
/^하나로/i, /^전부/i,
|
|
215
|
-
/^ㅇㅇ|^ㄱㄱ|^ㅇㅋ|^응$/i,
|
|
216
|
-
/^PS\s*C/i, /^C[-:]Users/i,
|
|
217
|
-
/^npm\s*cache/i,
|
|
218
|
-
];
|
|
219
|
-
// Only reject if both: matches vague pattern AND no technical content
|
|
220
|
-
const isVague = vaguePatterns.some((p) => p.test(body.situation.trim()));
|
|
221
|
-
const hasTechInSituation = techShort.test(body.situation);
|
|
222
|
-
if (isVague && !hasTechInSituation) {
|
|
223
|
-
return { pass: false, reason: "too vague or too specific command, not a generalizable situation" };
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// 11. Situation should have at least one technical/actionable keyword
|
|
227
|
-
const technicalKeywords = /코드|파일|프로젝트|서버|배포|테스트|보안|설정|빌드|에러|api|git|docker|deploy|build|test|security|config|server|database|code|function|module|component|review|refactor|debug|optimize|install|fix|create|implement|analyze|scan|vulnerability|exploit|injection|authenticate|authorize|overflow|bypass|reverse|binary|payload|shellcode|rop|heap|stack|format\s*string|race\s*condition|privilege|escalat|forensic|malware|encrypt|decrypt|hash|xss|csrf|ssrf|sqli|deserialization|sandbox|hook|intercept|patch|fuzzing|brute|crack|token|session|cookie|header|request|response|scraping|crawl|parsing|regex|algorithm|data\s*structure|complexity|cache|memory|thread|async|concurrency|pipeline|middleware|proxy|gateway|socket|websocket|protocol|packet|network|dns|ssl|tls|certificate|container|kubernetes|ci\/cd|workflow|migration|schema|query|index|replication/i;
|
|
228
|
-
if (!technicalKeywords.test(body.situation) && !technicalKeywords.test(body.approach)) {
|
|
229
|
-
return { pass: false, reason: "no technical content — not actionable as a skill" };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
// 12. Approach must be longer than 20 chars and contain a principle (not just "공통점 없음")
|
|
233
|
-
if (body.approach.length < 20 || body.approach === "공통점 없음") {
|
|
234
|
-
return { pass: false, reason: "approach is not substantial enough to be a skill" };
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// 13. Situation must be at least 15 chars (shorter OK if highly technical)
|
|
238
|
-
const isTechnical = technicalKeywords.test(body.situation);
|
|
239
|
-
if (body.situation.length < 15 && !isTechnical) {
|
|
240
|
-
return { pass: false, reason: "situation description too short to be useful" };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
// 14. Situation must NOT be a response/confirmation (Claude or user quoting)
|
|
244
|
-
const responsePatterns = [
|
|
245
|
-
/^네\s+판단|^맞습니다|^정확|^그렇습니다|^좋습니다/i, // Agreement
|
|
246
|
-
/^PS\s+[A-Z]:|^C:\\|^\/home\//i, // Terminal prompts/paths
|
|
247
|
-
/^LDPlayer|^BlueStacks|^Nox/i, // Emulator names
|
|
248
|
-
/설치.*완료|설치.*했는데|재실행/i, // Status reports not situations
|
|
249
|
-
/^근데\s+html|^근데\s+css/i, // Casual follow-ups
|
|
250
|
-
];
|
|
251
|
-
if (responsePatterns.some((p) => p.test(body.situation.trim()))) {
|
|
252
|
-
return { pass: false, reason: "situation is a response/status, not a generalizable scenario" };
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return { pass: true, reason: "passed all quality checks" };
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
259
|
-
// SIMILARITY MATCHING
|
|
260
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
261
|
-
|
|
262
|
-
function computeTopicSimilarity(a: string, b: string): number {
|
|
263
|
-
// Use full semantic similarity (synonyms + co-occurrence + Jaccard)
|
|
264
|
-
return semanticSimilarity(a, b);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function findSimilarSkill(
|
|
268
|
-
wisdom: Wisdom,
|
|
269
|
-
library: SkillLibrary,
|
|
270
|
-
threshold = 0.4, // Raised from 0.25 to prevent over-merging
|
|
271
|
-
): RefinedSkill | null {
|
|
272
|
-
let bestMatch: RefinedSkill | null = null;
|
|
273
|
-
let bestSimilarity = 0;
|
|
274
|
-
|
|
275
|
-
for (const skill of library.skills) {
|
|
276
|
-
// Compare topic similarity
|
|
277
|
-
const topicSim = computeTopicSimilarity(wisdom.body.situation, skill.topic);
|
|
278
|
-
|
|
279
|
-
// Compare domain overlap
|
|
280
|
-
const domainOverlap = wisdom.domains.filter((d) =>
|
|
281
|
-
skill.domains.includes(d),
|
|
282
|
-
).length / Math.max(wisdom.domains.length, 1);
|
|
283
|
-
|
|
284
|
-
// Combined score
|
|
285
|
-
const similarity = topicSim * 0.6 + domainOverlap * 0.4;
|
|
286
|
-
|
|
287
|
-
if (similarity > bestSimilarity && similarity >= threshold) {
|
|
288
|
-
bestSimilarity = similarity;
|
|
289
|
-
bestMatch = skill;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return bestMatch;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
297
|
-
// DIFFERENCE ANALYSIS
|
|
298
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
299
|
-
|
|
300
|
-
type DifferenceAnalysis = {
|
|
301
|
-
/** What's different in the context. */
|
|
302
|
-
contextDifference: string;
|
|
303
|
-
/** What's different in the approach. */
|
|
304
|
-
approachDifference: string;
|
|
305
|
-
/** What's the same. */
|
|
306
|
-
commonGround: string;
|
|
307
|
-
/** Is this a genuine contradiction or just a different context? */
|
|
308
|
-
type: "contradiction" | "context_dependent" | "evolution" | "complementary";
|
|
309
|
-
};
|
|
310
|
-
|
|
311
|
-
function analyzeDifference(
|
|
312
|
-
existingSkill: RefinedSkill,
|
|
313
|
-
newWisdom: Wisdom,
|
|
314
|
-
): DifferenceAnalysis {
|
|
315
|
-
const existingApproaches = existingSkill.branches.map((b) => b.approach).join(" ");
|
|
316
|
-
const newApproach = newWisdom.body.approach;
|
|
317
|
-
|
|
318
|
-
// Find common words
|
|
319
|
-
const existingWords = new Set(tokenize(existingApproaches));
|
|
320
|
-
const newWords = new Set(tokenize(newApproach));
|
|
321
|
-
const common = [...existingWords].filter((w) => newWords.has(w));
|
|
322
|
-
const onlyExisting = [...existingWords].filter((w) => !newWords.has(w));
|
|
323
|
-
const onlyNew = [...newWords].filter((w) => !existingWords.has(w));
|
|
324
|
-
|
|
325
|
-
const commonGround = common.length > 0
|
|
326
|
-
? `공통: ${common.slice(0, 5).join(", ")}`
|
|
327
|
-
: "공통점 없음";
|
|
328
|
-
|
|
329
|
-
const approachDifference = onlyNew.length > 0
|
|
330
|
-
? `새로운 접근: ${onlyNew.slice(0, 5).join(", ")}`
|
|
331
|
-
: "접근법 동일";
|
|
332
|
-
|
|
333
|
-
// Determine the context difference
|
|
334
|
-
const existingConditions = existingSkill.branches.map((b) => b.condition).join(" ");
|
|
335
|
-
const newContext = newWisdom.body.situation;
|
|
336
|
-
const contextWords = tokenize(newContext).filter(
|
|
337
|
-
(w) => !new Set(tokenize(existingConditions)).has(w),
|
|
338
|
-
);
|
|
339
|
-
const contextDifference = contextWords.length > 0
|
|
340
|
-
? contextWords.slice(0, 5).join(", ")
|
|
341
|
-
: "맥락 차이 불명확";
|
|
342
|
-
|
|
343
|
-
// Classify the type of difference
|
|
344
|
-
let type: DifferenceAnalysis["type"] = "context_dependent";
|
|
345
|
-
|
|
346
|
-
// If approaches are very different but context is similar → contradiction
|
|
347
|
-
const approachOverlap = common.length / Math.max(existingWords.size, newWords.size, 1);
|
|
348
|
-
if (approachOverlap < 0.2) {
|
|
349
|
-
const contextOverlap = computeTopicSimilarity(existingConditions, newContext);
|
|
350
|
-
if (contextOverlap > 0.5) {
|
|
351
|
-
type = "contradiction";
|
|
352
|
-
} else {
|
|
353
|
-
type = "context_dependent";
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
// If new approach is a superset of existing → evolution
|
|
358
|
-
if (onlyNew.length > 0 && onlyExisting.length === 0) {
|
|
359
|
-
type = "evolution";
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// If approaches are different but non-overlapping domains → complementary
|
|
363
|
-
if (approachOverlap < 0.3) {
|
|
364
|
-
const domainOverlap = newWisdom.domains.filter((d) =>
|
|
365
|
-
existingSkill.domains.includes(d),
|
|
366
|
-
).length;
|
|
367
|
-
if (domainOverlap === 0) type = "complementary";
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
return { contextDifference, approachDifference, commonGround, type };
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
374
|
-
// RECONCILIATION
|
|
375
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
376
|
-
|
|
377
|
-
function reconcileWithExisting(
|
|
378
|
-
existingSkill: RefinedSkill,
|
|
379
|
-
newWisdom: Wisdom,
|
|
380
|
-
): { skill: RefinedSkill; preference?: Preference } {
|
|
381
|
-
const diff = analyzeDifference(existingSkill, newWisdom);
|
|
382
|
-
const updated = { ...existingSkill };
|
|
383
|
-
let preference: Preference | undefined;
|
|
384
|
-
|
|
385
|
-
switch (diff.type) {
|
|
386
|
-
case "context_dependent": {
|
|
387
|
-
// Add a new conditional branch
|
|
388
|
-
updated.branches.push({
|
|
389
|
-
condition: newWisdom.body.situation,
|
|
390
|
-
approach: newWisdom.body.approach,
|
|
391
|
-
rationale: `${newWisdom.body.reason} (맥락 차이: ${diff.contextDifference})`,
|
|
392
|
-
sources: [newWisdom.id],
|
|
393
|
-
successCount: newWisdom.type === "principle" ? 1 : 0,
|
|
394
|
-
failureCount: newWisdom.type === "warning" ? 1 : 0,
|
|
395
|
-
});
|
|
396
|
-
updated.commonGround = diff.commonGround;
|
|
397
|
-
break;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
case "contradiction": {
|
|
401
|
-
// The new approach contradicts the old one for similar contexts
|
|
402
|
-
// → Create a preference: new > old (because it's more recent = learned from mistake)
|
|
403
|
-
const existingBranch = updated.branches[updated.branches.length - 1];
|
|
404
|
-
preference = {
|
|
405
|
-
situation: newWisdom.body.situation,
|
|
406
|
-
preferred: newWisdom.body.approach,
|
|
407
|
-
overWhat: existingBranch?.approach ?? "이전 접근법",
|
|
408
|
-
reason: `더 최신 경험에서 학습: ${newWisdom.body.reason}`,
|
|
409
|
-
confirmations: 1,
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Update existing branch or add new one
|
|
413
|
-
if (existingBranch) {
|
|
414
|
-
existingBranch.failureCount++;
|
|
415
|
-
}
|
|
416
|
-
updated.branches.push({
|
|
417
|
-
condition: `${newWisdom.body.situation} (개선된 접근)`,
|
|
418
|
-
approach: newWisdom.body.approach,
|
|
419
|
-
rationale: `이전 접근법(${existingBranch?.approach.slice(0, 30) ?? "?"})이 비효율적이어서 변경: ${newWisdom.body.reason}`,
|
|
420
|
-
sources: [newWisdom.id],
|
|
421
|
-
successCount: 1,
|
|
422
|
-
failureCount: 0,
|
|
423
|
-
});
|
|
424
|
-
updated.preferences.push(preference);
|
|
425
|
-
break;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
case "evolution": {
|
|
429
|
-
// New approach is an improvement — update the latest branch
|
|
430
|
-
const lastBranch = updated.branches[updated.branches.length - 1];
|
|
431
|
-
if (lastBranch) {
|
|
432
|
-
lastBranch.approach = `${lastBranch.approach}\n→ 개선: ${newWisdom.body.approach}`;
|
|
433
|
-
lastBranch.rationale += ` → 진화: ${newWisdom.body.reason}`;
|
|
434
|
-
lastBranch.sources.push(newWisdom.id);
|
|
435
|
-
lastBranch.successCount++;
|
|
436
|
-
}
|
|
437
|
-
break;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
case "complementary": {
|
|
441
|
-
// Different domain, just add as a new branch
|
|
442
|
-
updated.branches.push({
|
|
443
|
-
condition: newWisdom.body.situation,
|
|
444
|
-
approach: newWisdom.body.approach,
|
|
445
|
-
rationale: newWisdom.body.reason,
|
|
446
|
-
sources: [newWisdom.id],
|
|
447
|
-
successCount: 1,
|
|
448
|
-
failureCount: 0,
|
|
449
|
-
});
|
|
450
|
-
// Merge domains
|
|
451
|
-
for (const d of newWisdom.domains) {
|
|
452
|
-
if (!updated.domains.includes(d)) updated.domains.push(d);
|
|
453
|
-
}
|
|
454
|
-
break;
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
updated.version++;
|
|
459
|
-
updated.updatedAt = new Date().toISOString();
|
|
460
|
-
updated.validations++;
|
|
461
|
-
|
|
462
|
-
// Recalculate confidence
|
|
463
|
-
const totalSuccess = updated.branches.reduce((s, b) => s + b.successCount, 0);
|
|
464
|
-
const totalFailure = updated.branches.reduce((s, b) => s + b.failureCount, 0);
|
|
465
|
-
const total = totalSuccess + totalFailure;
|
|
466
|
-
updated.confidence = total > 0
|
|
467
|
-
? Math.min(0.95, 0.3 + (totalSuccess / total) * 0.5 + updated.validations * 0.05)
|
|
468
|
-
: updated.confidence;
|
|
469
|
-
|
|
470
|
-
return { skill: updated, preference };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
function abstractTopicName(wisdom: Wisdom): string {
|
|
474
|
-
// Generate a proper skill name from wisdom, not raw user message
|
|
475
|
-
const domains = wisdom.domains.slice(0, 2).join("/");
|
|
476
|
-
const type = wisdom.type === "principle" ? "원칙" : wisdom.type === "warning" ? "주의사항" : "판단기준";
|
|
477
|
-
|
|
478
|
-
// Extract the core action/concept from the situation
|
|
479
|
-
const situation = wisdom.body.situation
|
|
480
|
-
.replace(/\[파일 경로\]/g, "")
|
|
481
|
-
.replace(/\[코드\]/g, "")
|
|
482
|
-
.replace(/\[URL\]/g, "")
|
|
483
|
-
.trim();
|
|
484
|
-
|
|
485
|
-
// Take first meaningful phrase (up to 40 chars)
|
|
486
|
-
const firstPhrase = situation.split(/[.!?\n]/)[0]?.trim() ?? situation;
|
|
487
|
-
const short = firstPhrase.length > 40 ? firstPhrase.slice(0, 37) + "..." : firstPhrase;
|
|
488
|
-
|
|
489
|
-
return `[${domains}] ${short} — ${type}`;
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function createNewSkill(wisdom: Wisdom): RefinedSkill {
|
|
493
|
-
return {
|
|
494
|
-
id: createHash("sha256").update(wisdom.id + wisdom.body.situation).digest("hex").slice(0, 12),
|
|
495
|
-
topic: abstractTopicName(wisdom),
|
|
496
|
-
commonGround: wisdom.body.approach,
|
|
497
|
-
branches: [{
|
|
498
|
-
condition: wisdom.body.situation,
|
|
499
|
-
approach: wisdom.body.approach,
|
|
500
|
-
rationale: wisdom.body.reason,
|
|
501
|
-
sources: [wisdom.id],
|
|
502
|
-
successCount: wisdom.type === "principle" ? 1 : 0,
|
|
503
|
-
failureCount: wisdom.type === "warning" ? 1 : 0,
|
|
504
|
-
}],
|
|
505
|
-
preferences: [],
|
|
506
|
-
validations: 1,
|
|
507
|
-
confidence: wisdom.confidence,
|
|
508
|
-
domains: [...wisdom.domains],
|
|
509
|
-
version: 1,
|
|
510
|
-
createdAt: new Date().toISOString(),
|
|
511
|
-
updatedAt: new Date().toISOString(),
|
|
512
|
-
};
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
516
|
-
// MAIN PIPELINE
|
|
517
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
518
|
-
|
|
519
|
-
export function reconcileWisdom(
|
|
520
|
-
wisdoms: Wisdom[],
|
|
521
|
-
library: SkillLibrary,
|
|
522
|
-
): ReconciliationResult {
|
|
523
|
-
const result: ReconciliationResult = {
|
|
524
|
-
created: [],
|
|
525
|
-
updated: [],
|
|
526
|
-
rejected: [],
|
|
527
|
-
preferencesLearned: [],
|
|
528
|
-
};
|
|
529
|
-
|
|
530
|
-
for (const wisdom of wisdoms) {
|
|
531
|
-
// Step 1: Quality gate
|
|
532
|
-
const quality = qualityGate(wisdom);
|
|
533
|
-
if (!quality.pass) {
|
|
534
|
-
result.rejected.push({ wisdom, reason: quality.reason });
|
|
535
|
-
continue;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Step 2: Find similar existing skill
|
|
539
|
-
const existing = findSimilarSkill(wisdom, library);
|
|
540
|
-
|
|
541
|
-
if (existing) {
|
|
542
|
-
// Step 3a: Reconcile with existing
|
|
543
|
-
const { skill: updated, preference } = reconcileWithExisting(existing, wisdom);
|
|
544
|
-
|
|
545
|
-
// Replace in library
|
|
546
|
-
const idx = library.skills.indexOf(existing);
|
|
547
|
-
if (idx >= 0) library.skills[idx] = updated;
|
|
548
|
-
result.updated.push(updated);
|
|
549
|
-
|
|
550
|
-
if (preference) {
|
|
551
|
-
result.preferencesLearned.push(preference);
|
|
552
|
-
}
|
|
553
|
-
} else {
|
|
554
|
-
// Step 3b: Create new skill
|
|
555
|
-
const newSkill = createNewSkill(wisdom);
|
|
556
|
-
library.skills.push(newSkill);
|
|
557
|
-
result.created.push(newSkill);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
library.version++;
|
|
562
|
-
library.updatedAt = new Date().toISOString();
|
|
563
|
-
|
|
564
|
-
return result;
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
568
|
-
// PERSISTENCE
|
|
569
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
570
|
-
|
|
571
|
-
export function loadRefinedLibrary(dataDir: string): SkillLibrary {
|
|
572
|
-
const filePath = join(dataDir, "refined-skills.json");
|
|
573
|
-
if (existsSync(filePath)) {
|
|
574
|
-
return JSON.parse(readFileSync(filePath, "utf-8")) as SkillLibrary;
|
|
575
|
-
}
|
|
576
|
-
return { skills: [], version: 1, updatedAt: new Date().toISOString() };
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export function saveRefinedLibrary(library: SkillLibrary, dataDir: string): void {
|
|
580
|
-
mkdirSync(dataDir, { recursive: true });
|
|
581
|
-
writeFileSync(
|
|
582
|
-
join(dataDir, "refined-skills.json"),
|
|
583
|
-
JSON.stringify(library, null, 2),
|
|
584
|
-
"utf-8",
|
|
585
|
-
);
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
589
|
-
// OBSIDIAN RENDERER
|
|
590
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
591
|
-
|
|
592
|
-
export function renderRefinedSkillMarkdown(skill: RefinedSkill): string {
|
|
593
|
-
const lines: string[] = [];
|
|
594
|
-
|
|
595
|
-
lines.push("---");
|
|
596
|
-
lines.push(`type: refined-skill`);
|
|
597
|
-
lines.push(`topic: "${skill.topic.slice(0, 60)}"`);
|
|
598
|
-
lines.push(`confidence: ${skill.confidence.toFixed(2)}`);
|
|
599
|
-
lines.push(`version: ${skill.version}`);
|
|
600
|
-
lines.push(`branches: ${skill.branches.length}`);
|
|
601
|
-
lines.push(`validations: ${skill.validations}`);
|
|
602
|
-
lines.push(`domains: [${skill.domains.map((d) => `"${d}"`).join(", ")}]`);
|
|
603
|
-
lines.push(`tags: [claude/refined-skill]`);
|
|
604
|
-
lines.push("---");
|
|
605
|
-
lines.push("");
|
|
606
|
-
lines.push(`# ${skill.topic.slice(0, 60)}`);
|
|
607
|
-
lines.push("");
|
|
608
|
-
lines.push(`> 확신도: ${(skill.confidence * 100).toFixed(0)}% | 버전: ${skill.version} | 검증: ${skill.validations}회`);
|
|
609
|
-
lines.push("");
|
|
610
|
-
|
|
611
|
-
// Common ground
|
|
612
|
-
if (skill.commonGround) {
|
|
613
|
-
lines.push("## 공통 원칙");
|
|
614
|
-
lines.push("");
|
|
615
|
-
lines.push(skill.commonGround);
|
|
616
|
-
lines.push("");
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
// Conditional branches
|
|
620
|
-
if (skill.branches.length > 1) {
|
|
621
|
-
lines.push("## 상황별 접근법");
|
|
622
|
-
lines.push("");
|
|
623
|
-
lines.push("같은 주제라도 상황에 따라 접근법이 다릅니다:");
|
|
624
|
-
lines.push("");
|
|
625
|
-
|
|
626
|
-
for (let i = 0; i < skill.branches.length; i++) {
|
|
627
|
-
const branch = skill.branches[i];
|
|
628
|
-
const successRate = branch.successCount + branch.failureCount > 0
|
|
629
|
-
? (branch.successCount / (branch.successCount + branch.failureCount) * 100).toFixed(0)
|
|
630
|
-
: "N/A";
|
|
631
|
-
|
|
632
|
-
lines.push(`### ${i + 1}. ${branch.condition.slice(0, 60)}`);
|
|
633
|
-
lines.push("");
|
|
634
|
-
lines.push(`**접근법**: ${branch.approach}`);
|
|
635
|
-
lines.push("");
|
|
636
|
-
lines.push(`**이유**: ${branch.rationale}`);
|
|
637
|
-
lines.push("");
|
|
638
|
-
lines.push(`> 성공률: ${successRate}%`);
|
|
639
|
-
lines.push("");
|
|
640
|
-
}
|
|
641
|
-
} else if (skill.branches.length === 1) {
|
|
642
|
-
const branch = skill.branches[0];
|
|
643
|
-
lines.push("## 접근법");
|
|
644
|
-
lines.push("");
|
|
645
|
-
lines.push(branch.approach);
|
|
646
|
-
lines.push("");
|
|
647
|
-
lines.push("## 이유");
|
|
648
|
-
lines.push("");
|
|
649
|
-
lines.push(branch.rationale);
|
|
650
|
-
lines.push("");
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
// Preferences
|
|
654
|
-
if (skill.preferences.length > 0) {
|
|
655
|
-
lines.push("## 학습된 선호도");
|
|
656
|
-
lines.push("");
|
|
657
|
-
for (const pref of skill.preferences) {
|
|
658
|
-
lines.push(`- **${pref.situation.slice(0, 50)}**에서:`);
|
|
659
|
-
lines.push(` - ✅ **${pref.preferred.slice(0, 50)}** 선호`);
|
|
660
|
-
lines.push(` - ❌ ~~${pref.overWhat.slice(0, 50)}~~ 비선호`);
|
|
661
|
-
lines.push(` - 이유: ${pref.reason.slice(0, 80)}`);
|
|
662
|
-
lines.push(` - 확인: ${pref.confirmations}회`);
|
|
663
|
-
lines.push("");
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
return lines.join("\n");
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
export function exportRefinedSkills(library: SkillLibrary, vaultPath: string): string[] {
|
|
671
|
-
const dir = join(vaultPath, "Refined Skills");
|
|
672
|
-
if (!existsSync(dir)) {
|
|
673
|
-
mkdirSync(dir, { recursive: true });
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
const files: string[] = [];
|
|
677
|
-
for (const skill of library.skills) {
|
|
678
|
-
const safeName = skill.topic
|
|
679
|
-
.replace(/[<>:"/\\|?*#^[\]]/g, "-")
|
|
680
|
-
.replace(/-+/g, "-")
|
|
681
|
-
.slice(0, 60);
|
|
682
|
-
const filePath = join(dir, `${safeName}.md`);
|
|
683
|
-
writeFileSync(filePath, renderRefinedSkillMarkdown(skill), "utf-8");
|
|
684
|
-
files.push(filePath);
|
|
685
|
-
}
|
|
686
|
-
return files;
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
690
|
-
// HELPERS
|
|
691
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
692
|
-
|
|
693
|
-
function tokenize(text: string): string[] {
|
|
694
|
-
return text
|
|
695
|
-
.toLowerCase()
|
|
696
|
-
.replace(/[^a-z가-힣0-9\s]/g, " ")
|
|
697
|
-
.split(/\s+/)
|
|
698
|
-
.filter((w) => w.length > 2);
|
|
699
|
-
}
|