@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.
Files changed (37) hide show
  1. package/dist/cli/index.js +44 -71
  2. package/dist/index.js +15 -24
  3. package/dist/mcp/server.js +7 -6
  4. package/package.json +1 -1
  5. package/scripts/benchmark.ts +13 -35
  6. package/src/cli/index.ts +45 -84
  7. package/src/index.ts +17 -27
  8. package/src/mcp/server.ts +8 -7
  9. package/src/memory-engine/index.ts +4 -6
  10. package/src/memory-engine/semantic.ts +38 -11
  11. package/src/promptguard/advanced-rules.ts +122 -5
  12. package/src/promptguard/entropy.ts +21 -2
  13. package/src/promptguard/rules.ts +87 -2
  14. package/src/promptguard/scanner.test.ts +1 -4
  15. package/src/promptguard/scanner.ts +1 -1
  16. package/src/promptguard/semantic.ts +19 -4
  17. package/src/promptguard/token-analysis.ts +17 -5
  18. package/src/review/analyzer.ts +106 -23
  19. package/src/skills/index.ts +11 -27
  20. package/src/testing/health-check.ts +19 -2
  21. package/src/memory-engine/compressor.ts +0 -97
  22. package/src/memory-engine/context-window.ts +0 -113
  23. package/src/memory-engine/store.ts +0 -371
  24. package/src/memory-engine/types.ts +0 -32
  25. package/src/skills/context-engine.ts +0 -863
  26. package/src/skills/extractor.ts +0 -224
  27. package/src/skills/global-context.ts +0 -731
  28. package/src/skills/library.ts +0 -189
  29. package/src/skills/pattern-engine.ts +0 -715
  30. package/src/skills/render-evolved.ts +0 -160
  31. package/src/skills/skill-reconciler.ts +0 -699
  32. package/src/skills/smart-extractor.ts +0 -849
  33. package/src/skills/types.ts +0 -18
  34. package/src/skills/wisdom-extractor.ts +0 -737
  35. package/src/superdev-evolution/index.ts +0 -3
  36. package/src/superdev-evolution/skill-manager.ts +0 -266
  37. 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
- }