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