evo-skills 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.eslintrc.json +36 -0
  2. package/.prettierrc +11 -0
  3. package/README.md +227 -0
  4. package/USER-GUIDE.md +382 -0
  5. package/dist/cli/commands/diff.d.ts +7 -0
  6. package/dist/cli/commands/diff.d.ts.map +1 -0
  7. package/dist/cli/commands/diff.js +126 -0
  8. package/dist/cli/commands/diff.js.map +1 -0
  9. package/dist/cli/commands/freeze.d.ts +12 -0
  10. package/dist/cli/commands/freeze.d.ts.map +1 -0
  11. package/dist/cli/commands/freeze.js +93 -0
  12. package/dist/cli/commands/freeze.js.map +1 -0
  13. package/dist/cli/commands/log.d.ts +7 -0
  14. package/dist/cli/commands/log.d.ts.map +1 -0
  15. package/dist/cli/commands/log.js +76 -0
  16. package/dist/cli/commands/log.js.map +1 -0
  17. package/dist/cli/commands/rollback.d.ts +7 -0
  18. package/dist/cli/commands/rollback.d.ts.map +1 -0
  19. package/dist/cli/commands/rollback.js +92 -0
  20. package/dist/cli/commands/rollback.js.map +1 -0
  21. package/dist/cli/commands/status.d.ts +7 -0
  22. package/dist/cli/commands/status.d.ts.map +1 -0
  23. package/dist/cli/commands/status.js +88 -0
  24. package/dist/cli/commands/status.js.map +1 -0
  25. package/dist/cli/index.d.ts +3 -0
  26. package/dist/cli/index.d.ts.map +1 -0
  27. package/dist/cli/index.js +25 -0
  28. package/dist/cli/index.js.map +1 -0
  29. package/dist/config/defaults.d.ts +6 -0
  30. package/dist/config/defaults.d.ts.map +1 -0
  31. package/dist/config/defaults.js +31 -0
  32. package/dist/config/defaults.js.map +1 -0
  33. package/dist/config/index.d.ts +68 -0
  34. package/dist/config/index.d.ts.map +1 -0
  35. package/dist/config/index.js +195 -0
  36. package/dist/config/index.js.map +1 -0
  37. package/dist/core/evaluator/base-rule.d.ts +50 -0
  38. package/dist/core/evaluator/base-rule.d.ts.map +1 -0
  39. package/dist/core/evaluator/base-rule.js +69 -0
  40. package/dist/core/evaluator/base-rule.js.map +1 -0
  41. package/dist/core/evaluator/index.d.ts +33 -0
  42. package/dist/core/evaluator/index.d.ts.map +1 -0
  43. package/dist/core/evaluator/index.js +74 -0
  44. package/dist/core/evaluator/index.js.map +1 -0
  45. package/dist/core/evaluator/rules/repeated-drift.d.ts +19 -0
  46. package/dist/core/evaluator/rules/repeated-drift.d.ts.map +1 -0
  47. package/dist/core/evaluator/rules/repeated-drift.js +82 -0
  48. package/dist/core/evaluator/rules/repeated-drift.js.map +1 -0
  49. package/dist/core/evaluator/rules/repeated-manual-fix.d.ts +23 -0
  50. package/dist/core/evaluator/rules/repeated-manual-fix.d.ts.map +1 -0
  51. package/dist/core/evaluator/rules/repeated-manual-fix.js +97 -0
  52. package/dist/core/evaluator/rules/repeated-manual-fix.js.map +1 -0
  53. package/dist/core/journal/index.d.ts +76 -0
  54. package/dist/core/journal/index.d.ts.map +1 -0
  55. package/dist/core/journal/index.js +207 -0
  56. package/dist/core/journal/index.js.map +1 -0
  57. package/dist/core/observer/base-observer.d.ts +64 -0
  58. package/dist/core/observer/base-observer.d.ts.map +1 -0
  59. package/dist/core/observer/base-observer.js +142 -0
  60. package/dist/core/observer/base-observer.js.map +1 -0
  61. package/dist/core/observer/codex-observer.d.ts +71 -0
  62. package/dist/core/observer/codex-observer.d.ts.map +1 -0
  63. package/dist/core/observer/codex-observer.js +248 -0
  64. package/dist/core/observer/codex-observer.js.map +1 -0
  65. package/dist/core/observer/index.d.ts +7 -0
  66. package/dist/core/observer/index.d.ts.map +1 -0
  67. package/dist/core/observer/index.js +7 -0
  68. package/dist/core/observer/index.js.map +1 -0
  69. package/dist/core/observer/trace-manager.d.ts +82 -0
  70. package/dist/core/observer/trace-manager.d.ts.map +1 -0
  71. package/dist/core/observer/trace-manager.js +196 -0
  72. package/dist/core/observer/trace-manager.js.map +1 -0
  73. package/dist/core/origin-registry/index.d.ts +59 -0
  74. package/dist/core/origin-registry/index.d.ts.map +1 -0
  75. package/dist/core/origin-registry/index.js +235 -0
  76. package/dist/core/origin-registry/index.js.map +1 -0
  77. package/dist/core/patch-generator/base-strategy.d.ts +35 -0
  78. package/dist/core/patch-generator/base-strategy.d.ts.map +1 -0
  79. package/dist/core/patch-generator/base-strategy.js +55 -0
  80. package/dist/core/patch-generator/base-strategy.js.map +1 -0
  81. package/dist/core/patch-generator/index.d.ts +32 -0
  82. package/dist/core/patch-generator/index.d.ts.map +1 -0
  83. package/dist/core/patch-generator/index.js +101 -0
  84. package/dist/core/patch-generator/index.js.map +1 -0
  85. package/dist/core/patch-generator/strategies/add-fallback.d.ts +11 -0
  86. package/dist/core/patch-generator/strategies/add-fallback.d.ts.map +1 -0
  87. package/dist/core/patch-generator/strategies/add-fallback.js +57 -0
  88. package/dist/core/patch-generator/strategies/add-fallback.js.map +1 -0
  89. package/dist/core/patch-generator/strategies/prune-noise.d.ts +11 -0
  90. package/dist/core/patch-generator/strategies/prune-noise.d.ts.map +1 -0
  91. package/dist/core/patch-generator/strategies/prune-noise.js +48 -0
  92. package/dist/core/patch-generator/strategies/prune-noise.js.map +1 -0
  93. package/dist/core/shadow-manager/index.d.ts +65 -0
  94. package/dist/core/shadow-manager/index.d.ts.map +1 -0
  95. package/dist/core/shadow-manager/index.js +229 -0
  96. package/dist/core/shadow-manager/index.js.map +1 -0
  97. package/dist/core/shadow-registry/index.d.ts +68 -0
  98. package/dist/core/shadow-registry/index.d.ts.map +1 -0
  99. package/dist/core/shadow-registry/index.js +173 -0
  100. package/dist/core/shadow-registry/index.js.map +1 -0
  101. package/dist/daemon/index.d.ts +43 -0
  102. package/dist/daemon/index.d.ts.map +1 -0
  103. package/dist/daemon/index.js +144 -0
  104. package/dist/daemon/index.js.map +1 -0
  105. package/dist/storage/index.d.ts +7 -0
  106. package/dist/storage/index.d.ts.map +1 -0
  107. package/dist/storage/index.js +7 -0
  108. package/dist/storage/index.js.map +1 -0
  109. package/dist/storage/markdown.d.ts +66 -0
  110. package/dist/storage/markdown.d.ts.map +1 -0
  111. package/dist/storage/markdown.js +227 -0
  112. package/dist/storage/markdown.js.map +1 -0
  113. package/dist/storage/ndjson.d.ts +118 -0
  114. package/dist/storage/ndjson.d.ts.map +1 -0
  115. package/dist/storage/ndjson.js +238 -0
  116. package/dist/storage/ndjson.js.map +1 -0
  117. package/dist/storage/sqlite.d.ts +131 -0
  118. package/dist/storage/sqlite.d.ts.map +1 -0
  119. package/dist/storage/sqlite.js +415 -0
  120. package/dist/storage/sqlite.js.map +1 -0
  121. package/dist/types/index.d.ts +158 -0
  122. package/dist/types/index.d.ts.map +1 -0
  123. package/dist/types/index.js +5 -0
  124. package/dist/types/index.js.map +1 -0
  125. package/dist/utils/diff.d.ts +20 -0
  126. package/dist/utils/diff.d.ts.map +1 -0
  127. package/dist/utils/diff.js +47 -0
  128. package/dist/utils/diff.js.map +1 -0
  129. package/dist/utils/hash.d.ts +17 -0
  130. package/dist/utils/hash.d.ts.map +1 -0
  131. package/dist/utils/hash.js +28 -0
  132. package/dist/utils/hash.js.map +1 -0
  133. package/dist/utils/index.d.ts +8 -0
  134. package/dist/utils/index.d.ts.map +1 -0
  135. package/dist/utils/index.js +8 -0
  136. package/dist/utils/index.js.map +1 -0
  137. package/dist/utils/logger.d.ts +11 -0
  138. package/dist/utils/logger.d.ts.map +1 -0
  139. package/dist/utils/logger.js +64 -0
  140. package/dist/utils/logger.js.map +1 -0
  141. package/dist/utils/path.d.ts +67 -0
  142. package/dist/utils/path.d.ts.map +1 -0
  143. package/dist/utils/path.js +115 -0
  144. package/dist/utils/path.js.map +1 -0
  145. package/docs/ENGINEERING_PLAN.md +1065 -0
  146. package/docs/PRD.md +1004 -0
  147. package/docs/PROGRESS.md +523 -0
  148. package/package.json +60 -0
  149. package/src/cli/commands/diff.ts +158 -0
  150. package/src/cli/commands/freeze.ts +108 -0
  151. package/src/cli/commands/log.ts +87 -0
  152. package/src/cli/commands/rollback.ts +99 -0
  153. package/src/cli/commands/status.ts +101 -0
  154. package/src/cli/index.ts +31 -0
  155. package/src/config/defaults.ts +32 -0
  156. package/src/config/index.ts +220 -0
  157. package/src/core/evaluator/base-rule.ts +90 -0
  158. package/src/core/evaluator/index.ts +86 -0
  159. package/src/core/evaluator/rules/repeated-drift.ts +109 -0
  160. package/src/core/evaluator/rules/repeated-manual-fix.ts +129 -0
  161. package/src/core/journal/index.ts +286 -0
  162. package/src/core/observer/base-observer.ts +200 -0
  163. package/src/core/observer/codex-observer.ts +309 -0
  164. package/src/core/observer/index.ts +7 -0
  165. package/src/core/observer/trace-manager.ts +250 -0
  166. package/src/core/origin-registry/index.ts +264 -0
  167. package/src/core/patch-generator/base-strategy.ts +67 -0
  168. package/src/core/patch-generator/index.ts +118 -0
  169. package/src/core/patch-generator/strategies/add-fallback.ts +69 -0
  170. package/src/core/patch-generator/strategies/prune-noise.ts +65 -0
  171. package/src/core/shadow-manager/index.ts +278 -0
  172. package/src/core/shadow-registry/index.ts +200 -0
  173. package/src/daemon/index.ts +174 -0
  174. package/src/storage/index.ts +7 -0
  175. package/src/storage/markdown.ts +267 -0
  176. package/src/storage/ndjson.ts +273 -0
  177. package/src/storage/sqlite.ts +555 -0
  178. package/src/types/index.ts +212 -0
  179. package/src/utils/diff.ts +60 -0
  180. package/src/utils/hash.ts +31 -0
  181. package/src/utils/index.ts +8 -0
  182. package/src/utils/logger.ts +78 -0
  183. package/src/utils/path.ts +129 -0
  184. package/tests/unit/config.test.ts +65 -0
  185. package/tests/unit/path.test.ts +89 -0
  186. package/tests/unit/utils.test.ts +45 -0
  187. package/tsconfig.json +31 -0
  188. package/vitest.config.ts +19 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * 全局类型定义入口
3
+ */
4
+
5
+ // Origin Skill 类型
6
+ export interface OriginSkill {
7
+ skill_id: string;
8
+ origin_path: string;
9
+ origin_version: string; // file hash
10
+ source: 'local' | 'marketplace' | 'git';
11
+ installed_at: string;
12
+ last_seen_at: string;
13
+ }
14
+
15
+ // Shadow Skill 状态
16
+ export type ShadowStatus = 'active' | 'frozen' | 'rebasing' | 'needs_attention';
17
+
18
+ // Project Skill Shadow 类型
19
+ export interface ProjectSkillShadow {
20
+ project_id: string;
21
+ skill_id: string;
22
+ shadow_id: string; // "A@repo-x"
23
+ origin_skill_id: string;
24
+ origin_version_at_fork: string;
25
+ shadow_path: string;
26
+ current_revision: number;
27
+ status: ShadowStatus;
28
+ created_at: string;
29
+ last_optimized_at: string;
30
+ }
31
+
32
+ // Shadow Skill 运行状态
33
+ export interface ShadowSkillState {
34
+ shadow_id: string;
35
+ current_content_hash: string;
36
+ current_revision: number;
37
+ last_hit_at: string;
38
+ last_optimized_at: string;
39
+ hit_count: number;
40
+ success_count: number;
41
+ manual_override_count: number;
42
+ health_score: number; // 0-100
43
+ }
44
+
45
+ // Change Type - Patch 类型
46
+ export type ChangeType =
47
+ | 'append_context'
48
+ | 'tighten_trigger'
49
+ | 'add_fallback'
50
+ | 'prune_noise'
51
+ | 'rewrite_section';
52
+
53
+ // Evolution Record 演化记录
54
+ export interface EvolutionRecord {
55
+ revision: number;
56
+ shadow_id: string;
57
+ timestamp: string;
58
+ reason: string;
59
+ source_sessions: string[];
60
+ change_type: ChangeType;
61
+ patch: string;
62
+ before_hash: string;
63
+ after_hash: string;
64
+ applied_by: 'auto' | 'manual';
65
+ }
66
+
67
+ // Snapshot 信息
68
+ export interface SnapshotInfo {
69
+ revision: number;
70
+ timestamp: string;
71
+ file_path: string;
72
+ content_hash: string;
73
+ }
74
+
75
+ // Trace 事件类型
76
+ export type TraceEventType =
77
+ | 'user_input'
78
+ | 'assistant_output'
79
+ | 'tool_call'
80
+ | 'tool_result'
81
+ | 'file_change'
82
+ | 'retry'
83
+ | 'status';
84
+
85
+ // Trace 状态
86
+ export type TraceStatus = 'success' | 'failure' | 'retry' | 'interrupted';
87
+
88
+ // Runtime 类型
89
+ export type RuntimeType = 'codex' | 'opencode' | 'claude';
90
+
91
+ // Trace 数据结构
92
+ export interface Trace {
93
+ trace_id: string;
94
+ runtime: RuntimeType;
95
+ session_id: string;
96
+ turn_id: string;
97
+ event_type: TraceEventType;
98
+ timestamp: string;
99
+ user_input?: string;
100
+ assistant_output?: string;
101
+ tool_name?: string;
102
+ tool_args?: Record<string, unknown>;
103
+ tool_result?: Record<string, unknown>;
104
+ files_changed?: string[];
105
+ status: TraceStatus;
106
+ metadata?: Record<string, unknown>;
107
+ }
108
+
109
+ // Evaluation Result 评估结果
110
+ export interface EvaluationResult {
111
+ should_patch: boolean;
112
+ change_type?: ChangeType;
113
+ reason?: string;
114
+ source_sessions: string[];
115
+ confidence: number; // 0-1
116
+ target_section?: string;
117
+ }
118
+
119
+ // Patch Result
120
+ export interface PatchResult {
121
+ success: boolean;
122
+ patch: string; // unified diff
123
+ newContent: string;
124
+ changeType: ChangeType;
125
+ error?: string;
126
+ }
127
+
128
+ // Auto Optimize Policy 自动优化策略
129
+ export interface AutoOptimizePolicy {
130
+ min_signal_count: number;
131
+ min_source_sessions: number;
132
+ min_confidence: number;
133
+ cooldown_hours: number;
134
+ max_patches_per_day: number;
135
+ pause_after_rollback_hours: number;
136
+ }
137
+
138
+ // Session 信息
139
+ export interface Session {
140
+ session_id: string;
141
+ runtime: RuntimeType;
142
+ project_id: string | null;
143
+ started_at: string;
144
+ ended_at: string | null;
145
+ trace_count: number;
146
+ }
147
+
148
+ // Snapshot 详情(包含内容)
149
+ export interface Snapshot extends SnapshotInfo {
150
+ shadow_id: string;
151
+ content: string;
152
+ }
153
+
154
+ // Journal 查询选项
155
+ export interface JournalQueryOptions {
156
+ fromRevision?: number;
157
+ toRevision?: number;
158
+ limit?: number;
159
+ changeType?: ChangeType;
160
+ }
161
+
162
+ // CLI 输出格式
163
+ export interface CliOutput {
164
+ success: boolean;
165
+ message: string;
166
+ data?: unknown;
167
+ error?: string;
168
+ }
169
+
170
+ // 配置类型
171
+ export interface EVOConfig {
172
+ origin_paths: {
173
+ paths: string[];
174
+ };
175
+ observer: {
176
+ enabled_runtimes: RuntimeType[];
177
+ trace_retention_days: number;
178
+ };
179
+ evaluator: {
180
+ min_signal_count: number;
181
+ min_source_sessions: number;
182
+ min_confidence: number;
183
+ };
184
+ patch: {
185
+ allowed_types: ChangeType[];
186
+ cooldown_hours: number;
187
+ max_patches_per_day: number;
188
+ };
189
+ journal: {
190
+ snapshot_interval: number;
191
+ max_snapshots: number;
192
+ };
193
+ daemon: {
194
+ auto_start: boolean;
195
+ log_level: 'debug' | 'info' | 'warn' | 'error';
196
+ };
197
+ }
198
+
199
+ // 项目配置
200
+ export interface ProjectConfig {
201
+ project: {
202
+ name: string;
203
+ auto_optimize: boolean;
204
+ };
205
+ skills: Record<
206
+ string,
207
+ {
208
+ auto_optimize?: boolean;
209
+ allowed_patch_types?: ChangeType[];
210
+ }
211
+ >;
212
+ }
@@ -0,0 +1,60 @@
1
+ import { diffLines, createPatch } from 'diff';
2
+
3
+ /**
4
+ * 计算两个字符串的差异
5
+ */
6
+ export function computeDiff(oldContent: string, newContent: string): string {
7
+ const differences = diffLines(oldContent, newContent);
8
+ return differences
9
+ .map((part) => {
10
+ const prefix = part.added ? '+' : part.removed ? '-' : ' ';
11
+ return part.value
12
+ .split('\n')
13
+ .filter((line) => line.length > 0)
14
+ .map((line) => `${prefix} ${line}`)
15
+ .join('\n');
16
+ })
17
+ .join('\n');
18
+ }
19
+
20
+ /**
21
+ * 生成 unified diff 格式的 patch
22
+ */
23
+ export function createUnifiedDiff(
24
+ fileName: string,
25
+ oldContent: string,
26
+ newContent: string,
27
+ oldHeader?: string,
28
+ newHeader?: string
29
+ ): string {
30
+ return createPatch(fileName, oldContent, newContent, oldHeader, newHeader);
31
+ }
32
+
33
+ /**
34
+ * 检查内容是否有变化
35
+ */
36
+ export function hasChanges(oldContent: string, newContent: string): boolean {
37
+ return oldContent !== newContent;
38
+ }
39
+
40
+ /**
41
+ * 统计差异行数
42
+ */
43
+ export function countChanges(oldContent: string, newContent: string): {
44
+ added: number;
45
+ removed: number;
46
+ } {
47
+ const differences = diffLines(oldContent, newContent);
48
+ let added = 0;
49
+ let removed = 0;
50
+
51
+ for (const part of differences) {
52
+ if (part.added) {
53
+ added += part.count ?? 0;
54
+ } else if (part.removed) {
55
+ removed += part.count ?? 0;
56
+ }
57
+ }
58
+
59
+ return { added, removed };
60
+ }
@@ -0,0 +1,31 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { readFileSync } from 'node:fs';
3
+
4
+ /**
5
+ * 计算字符串的 SHA256 哈希
6
+ */
7
+ export function hashString(content: string): string {
8
+ return createHash('sha256').update(content).digest('hex');
9
+ }
10
+
11
+ /**
12
+ * 计算文件的 SHA256 哈希
13
+ */
14
+ export function hashFile(filePath: string): string {
15
+ const content = readFileSync(filePath, 'utf-8');
16
+ return hashString(content);
17
+ }
18
+
19
+ /**
20
+ * 计算 Buffer 的 SHA256 哈希
21
+ */
22
+ export function hashBuffer(buffer: Buffer): string {
23
+ return createHash('sha256').update(buffer).digest('hex');
24
+ }
25
+
26
+ /**
27
+ * 生成短哈希(前 8 位)
28
+ */
29
+ export function shortHash(content: string): string {
30
+ return hashString(content).substring(0, 8);
31
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 工具函数库入口
3
+ */
4
+
5
+ export * from './hash.js';
6
+ export * from './diff.js';
7
+ export * from './path.js';
8
+ export * from './logger.js';
@@ -0,0 +1,78 @@
1
+ import winston from 'winston';
2
+ import { join } from 'node:path';
3
+ import { existsSync, mkdirSync } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+
6
+ const LOG_DIR = join(homedir(), '.evo', 'logs');
7
+
8
+ /**
9
+ * 创建日志目录
10
+ */
11
+ function ensureLogDir(): void {
12
+ if (!existsSync(LOG_DIR)) {
13
+ mkdirSync(LOG_DIR, { recursive: true });
14
+ }
15
+ }
16
+
17
+ /**
18
+ * 自定义日志格式
19
+ */
20
+ const logFormat = winston.format.combine(
21
+ winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
22
+ winston.format.errors({ stack: true }),
23
+ winston.format.printf(({ timestamp, level, message, stack }) => {
24
+ const baseMessage = `[${timestamp}] ${level.toUpperCase()}: ${message}`;
25
+ return stack ? `${baseMessage}\n${stack}` : baseMessage;
26
+ })
27
+ );
28
+
29
+ /**
30
+ * 创建主 logger
31
+ */
32
+ function createLogger(): winston.Logger {
33
+ ensureLogDir();
34
+
35
+ return winston.createLogger({
36
+ level: 'info',
37
+ format: logFormat,
38
+ transports: [
39
+ // 文件日志
40
+ new winston.transports.File({
41
+ filename: join(LOG_DIR, 'error.log'),
42
+ level: 'error',
43
+ maxsize: 5 * 1024 * 1024, // 5MB
44
+ maxFiles: 5,
45
+ }),
46
+ new winston.transports.File({
47
+ filename: join(LOG_DIR, 'combined.log'),
48
+ maxsize: 10 * 1024 * 1024, // 10MB
49
+ maxFiles: 10,
50
+ }),
51
+ // 控制台日志(仅在开发环境)
52
+ new winston.transports.Console({
53
+ format: winston.format.combine(
54
+ winston.format.colorize(),
55
+ logFormat
56
+ ),
57
+ silent: process.env.NODE_ENV === 'production',
58
+ }),
59
+ ],
60
+ });
61
+ }
62
+
63
+ // 导出单例 logger
64
+ export const logger = createLogger();
65
+
66
+ /**
67
+ * 创建带上下文的 logger
68
+ */
69
+ export function createChildLogger(context: string): winston.Logger {
70
+ return logger.child({ context });
71
+ }
72
+
73
+ /**
74
+ * 设置日志级别
75
+ */
76
+ export function setLogLevel(level: string): void {
77
+ logger.level = level;
78
+ }
@@ -0,0 +1,129 @@
1
+ import { join, resolve, relative, dirname, basename, extname } from 'node:path';
2
+ import { existsSync, statSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+
5
+ /**
6
+ * 展开路径中的 ~ 为用户主目录
7
+ */
8
+ export function expandHome(path: string): string {
9
+ if (path.startsWith('~/') || path === '~') {
10
+ return path.replace('~', homedir());
11
+ }
12
+ return path;
13
+ }
14
+
15
+ /**
16
+ * 获取项目的 .evo 目录路径
17
+ */
18
+ export function getEvoDir(projectRoot: string): string {
19
+ return join(projectRoot, '.evo');
20
+ }
21
+
22
+ /**
23
+ * 获取项目的 skills 目录路径
24
+ */
25
+ export function getSkillsDir(projectRoot: string): string {
26
+ return join(projectRoot, '.evo', 'skills');
27
+ }
28
+
29
+ /**
30
+ * 获取项目的 state 目录路径
31
+ */
32
+ export function getStateDir(projectRoot: string): string {
33
+ return join(projectRoot, '.evo', 'state');
34
+ }
35
+
36
+ /**
37
+ * 获取项目的 config 目录路径
38
+ */
39
+ export function getConfigDir(projectRoot: string): string {
40
+ return join(projectRoot, '.evo', 'config');
41
+ }
42
+
43
+ /**
44
+ * 获取 shadow skill 的路径
45
+ */
46
+ export function getShadowSkillPath(projectRoot: string, skillId: string): string {
47
+ return join(projectRoot, '.evo', 'skills', skillId, 'current.md');
48
+ }
49
+
50
+ /**
51
+ * 获取 shadow skill 的 meta.json 路径
52
+ */
53
+ export function getShadowMetaPath(projectRoot: string, skillId: string): string {
54
+ return join(projectRoot, '.evo', 'skills', skillId, 'meta.json');
55
+ }
56
+
57
+ /**
58
+ * 获取 shadow skill 的 journal 路径
59
+ */
60
+ export function getShadowJournalPath(projectRoot: string, skillId: string): string {
61
+ return join(projectRoot, '.evo', 'skills', skillId, 'journal.ndjson');
62
+ }
63
+
64
+ /**
65
+ * 获取 shadow skill 的 snapshots 目录路径
66
+ */
67
+ export function getSnapshotsDir(projectRoot: string, skillId: string): string {
68
+ return join(projectRoot, '.evo', 'skills', skillId, 'snapshots');
69
+ }
70
+
71
+ /**
72
+ * 检查路径是否存在
73
+ */
74
+ export function pathExists(path: string): boolean {
75
+ return existsSync(path);
76
+ }
77
+
78
+ /**
79
+ * 检查是否是目录
80
+ */
81
+ export function isDirectory(path: string): boolean {
82
+ try {
83
+ return statSync(path).isDirectory();
84
+ } catch {
85
+ return false;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * 检查是否是文件
91
+ */
92
+ export function isFile(path: string): boolean {
93
+ try {
94
+ return statSync(path).isFile();
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+
100
+ /**
101
+ * 获取相对路径
102
+ */
103
+ export function getRelativePath(from: string, to: string): string {
104
+ return relative(from, to);
105
+ }
106
+
107
+ /**
108
+ * 获取文件扩展名
109
+ */
110
+ export function getExtension(path: string): string {
111
+ return extname(path);
112
+ }
113
+
114
+ /**
115
+ * 获取文件名(不含扩展名)
116
+ */
117
+ export function getFileNameWithoutExt(path: string): string {
118
+ return basename(path, extname(path));
119
+ }
120
+
121
+ /**
122
+ * 获取目录名
123
+ */
124
+ export function getDirName(path: string): string {
125
+ return dirname(path);
126
+ }
127
+
128
+ // 重新导出常用的 path 函数
129
+ export { join, resolve, relative, dirname, basename, extname };
@@ -0,0 +1,65 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ConfigManager } from '../../src/config/index.js';
3
+ import { DEFAULT_CONFIG } from '../../src/config/defaults.js';
4
+
5
+ describe('Config Manager', () => {
6
+ let configManager: ConfigManager;
7
+
8
+ beforeEach(() => {
9
+ configManager = new ConfigManager();
10
+ });
11
+
12
+ describe('getGlobalConfig', () => {
13
+ it('should return default config initially', () => {
14
+ const config = configManager.getGlobalConfig();
15
+ expect(config).toEqual(DEFAULT_CONFIG);
16
+ });
17
+
18
+ it('should return a copy of config', () => {
19
+ const config1 = configManager.getGlobalConfig();
20
+ const config2 = configManager.getGlobalConfig();
21
+ expect(config1).not.toBe(config2);
22
+ expect(config1).toEqual(config2);
23
+ });
24
+ });
25
+
26
+ describe('getEvaluatorConfig', () => {
27
+ it('should return evaluator config', () => {
28
+ const config = configManager.getEvaluatorConfig();
29
+ expect(config).toHaveProperty('min_signal_count');
30
+ expect(config).toHaveProperty('min_source_sessions');
31
+ expect(config).toHaveProperty('min_confidence');
32
+ });
33
+ });
34
+
35
+ describe('getPatchConfig', () => {
36
+ it('should return patch config', () => {
37
+ const config = configManager.getPatchConfig();
38
+ expect(config).toHaveProperty('allowed_types');
39
+ expect(config).toHaveProperty('cooldown_hours');
40
+ expect(config).toHaveProperty('max_patches_per_day');
41
+ });
42
+ });
43
+
44
+ describe('getJournalConfig', () => {
45
+ it('should return journal config', () => {
46
+ const config = configManager.getJournalConfig();
47
+ expect(config).toHaveProperty('snapshot_interval');
48
+ expect(config).toHaveProperty('max_snapshots');
49
+ });
50
+ });
51
+
52
+ describe('isSkillFrozen', () => {
53
+ it('should return false when no project config', () => {
54
+ const result = configManager.isSkillFrozen('test-skill');
55
+ expect(result).toBe(false);
56
+ });
57
+ });
58
+
59
+ describe('getAllowedPatchTypes', () => {
60
+ it('should return default patch types when no project config', () => {
61
+ const types = configManager.getAllowedPatchTypes('test-skill');
62
+ expect(types).toEqual(DEFAULT_CONFIG.patch.allowed_types);
63
+ });
64
+ });
65
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ expandHome,
4
+ getEvoDir,
5
+ getSkillsDir,
6
+ getStateDir,
7
+ getConfigDir,
8
+ getShadowSkillPath,
9
+ getShadowMetaPath,
10
+ getShadowJournalPath,
11
+ getSnapshotsDir,
12
+ join,
13
+ } from '../../src/utils/path.js';
14
+ import { homedir } from 'node:os';
15
+
16
+ describe('Path Utils', () => {
17
+ describe('expandHome', () => {
18
+ it('should expand ~ to home directory', () => {
19
+ const result = expandHome('~/test');
20
+ expect(result).toBe(join(homedir(), 'test'));
21
+ });
22
+
23
+ it('should handle ~ alone', () => {
24
+ const result = expandHome('~');
25
+ expect(result).toBe(homedir());
26
+ });
27
+
28
+ it('should not modify paths without ~', () => {
29
+ const result = expandHome('/absolute/path');
30
+ expect(result).toBe('/absolute/path');
31
+ });
32
+ });
33
+
34
+ describe('getEvoDir', () => {
35
+ it('should return .evo directory path', () => {
36
+ const result = getEvoDir('/project');
37
+ expect(result).toBe('/project/.evo');
38
+ });
39
+ });
40
+
41
+ describe('getSkillsDir', () => {
42
+ it('should return .evo/skills directory path', () => {
43
+ const result = getSkillsDir('/project');
44
+ expect(result).toBe('/project/.evo/skills');
45
+ });
46
+ });
47
+
48
+ describe('getStateDir', () => {
49
+ it('should return .evo/state directory path', () => {
50
+ const result = getStateDir('/project');
51
+ expect(result).toBe('/project/.evo/state');
52
+ });
53
+ });
54
+
55
+ describe('getConfigDir', () => {
56
+ it('should return .evo/config directory path', () => {
57
+ const result = getConfigDir('/project');
58
+ expect(result).toBe('/project/.evo/config');
59
+ });
60
+ });
61
+
62
+ describe('getShadowSkillPath', () => {
63
+ it('should return shadow skill path', () => {
64
+ const result = getShadowSkillPath('/project', 'my-skill');
65
+ expect(result).toBe('/project/.evo/skills/my-skill/current.md');
66
+ });
67
+ });
68
+
69
+ describe('getShadowMetaPath', () => {
70
+ it('should return shadow meta path', () => {
71
+ const result = getShadowMetaPath('/project', 'my-skill');
72
+ expect(result).toBe('/project/.evo/skills/my-skill/meta.json');
73
+ });
74
+ });
75
+
76
+ describe('getShadowJournalPath', () => {
77
+ it('should return shadow journal path', () => {
78
+ const result = getShadowJournalPath('/project', 'my-skill');
79
+ expect(result).toBe('/project/.evo/skills/my-skill/journal.ndjson');
80
+ });
81
+ });
82
+
83
+ describe('getSnapshotsDir', () => {
84
+ it('should return snapshots directory path', () => {
85
+ const result = getSnapshotsDir('/project', 'my-skill');
86
+ expect(result).toBe('/project/.evo/skills/my-skill/snapshots');
87
+ });
88
+ });
89
+ });