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,220 @@
1
+ import { cosmiconfig } from 'cosmiconfig';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { join, resolve } from 'node:path';
5
+ import type { EVOConfig, ProjectConfig } from '../types/index.js';
6
+ import { DEFAULT_CONFIG } from './defaults.js';
7
+
8
+ const MODULE_NAME = 'evo';
9
+ const GLOBAL_CONFIG_DIR = join(homedir(), '.evo');
10
+ const GLOBAL_CONFIG_FILE = join(GLOBAL_CONFIG_DIR, 'settings.toml');
11
+
12
+ /**
13
+ * 配置管理器
14
+ */
15
+ export class ConfigManager {
16
+ private globalConfig: EVOConfig;
17
+ private projectConfig: ProjectConfig | null = null;
18
+ private projectRoot: string | null = null;
19
+
20
+ constructor() {
21
+ this.globalConfig = { ...DEFAULT_CONFIG };
22
+ }
23
+
24
+ /**
25
+ * 初始化配置管理器
26
+ */
27
+ async init(projectRoot?: string): Promise<void> {
28
+ // 加载全局配置
29
+ await this.loadGlobalConfig();
30
+
31
+ // 如果指定了项目根目录,加载项目配置
32
+ if (projectRoot) {
33
+ this.projectRoot = projectRoot;
34
+ await this.loadProjectConfig();
35
+ }
36
+ }
37
+
38
+ /**
39
+ * 加载全局配置
40
+ */
41
+ private async loadGlobalConfig(): Promise<void> {
42
+ const explorer = cosmiconfig(MODULE_NAME, {
43
+ searchPlaces: [
44
+ 'settings.toml',
45
+ '.eorc',
46
+ '.eorc.json',
47
+ '.eorc.yaml',
48
+ '.eorc.yml',
49
+ 'package.json',
50
+ ],
51
+ });
52
+
53
+ try {
54
+ const result = await explorer.search(GLOBAL_CONFIG_DIR);
55
+ if (result && !result.isEmpty) {
56
+ this.globalConfig = this.mergeConfig(DEFAULT_CONFIG, result.config as EVOConfig);
57
+ }
58
+ } catch {
59
+ // 配置文件不存在或格式错误,使用默认配置
60
+ }
61
+ }
62
+
63
+ /**
64
+ * 加载项目配置
65
+ */
66
+ private async loadProjectConfig(): Promise<void> {
67
+ if (!this.projectRoot) return;
68
+
69
+ const projectConfigPath = join(this.projectRoot, '.evo', 'config', 'settings.toml');
70
+ const explorer = cosmiconfig(MODULE_NAME, {
71
+ searchPlaces: [projectConfigPath],
72
+ });
73
+
74
+ try {
75
+ const result = await explorer.search(this.projectRoot);
76
+ if (result && !result.isEmpty) {
77
+ this.projectConfig = result.config as ProjectConfig;
78
+ }
79
+ } catch {
80
+ // 项目配置不存在
81
+ }
82
+ }
83
+
84
+ /**
85
+ * 深度合并配置对象
86
+ */
87
+ private mergeConfig<T extends object>(base: T, override: Partial<T>): T {
88
+ const result = { ...base };
89
+
90
+ for (const key in override) {
91
+ if (Object.prototype.hasOwnProperty.call(override, key)) {
92
+ const baseValue = base[key];
93
+ const overrideValue = override[key];
94
+
95
+ if (
96
+ typeof baseValue === 'object' &&
97
+ baseValue !== null &&
98
+ !Array.isArray(baseValue) &&
99
+ typeof overrideValue === 'object' &&
100
+ overrideValue !== null &&
101
+ !Array.isArray(overrideValue)
102
+ ) {
103
+ (result as Record<string, unknown>)[key] = this.mergeConfig(
104
+ baseValue as object,
105
+ overrideValue as object
106
+ );
107
+ } else if (overrideValue !== undefined) {
108
+ (result as Record<string, unknown>)[key] = overrideValue;
109
+ }
110
+ }
111
+ }
112
+
113
+ return result;
114
+ }
115
+
116
+ /**
117
+ * 获取全局配置
118
+ */
119
+ getGlobalConfig(): EVOConfig {
120
+ return { ...this.globalConfig };
121
+ }
122
+
123
+ /**
124
+ * 获取项目配置
125
+ */
126
+ getProjectConfig(): ProjectConfig | null {
127
+ return this.projectConfig ? { ...this.projectConfig } : null;
128
+ }
129
+
130
+ /**
131
+ * 获取 origin skill 路径列表
132
+ */
133
+ getOriginPaths(): string[] {
134
+ return this.globalConfig.origin_paths.paths.map((p) =>
135
+ p.startsWith('~') ? p.replace('~', homedir()) : resolve(p)
136
+ );
137
+ }
138
+
139
+ /**
140
+ * 获取评估器配置
141
+ */
142
+ getEvaluatorConfig(): EVOConfig['evaluator'] {
143
+ return { ...this.globalConfig.evaluator };
144
+ }
145
+
146
+ /**
147
+ * 获取 patch 配置
148
+ */
149
+ getPatchConfig(): EVOConfig['patch'] {
150
+ return { ...this.globalConfig.patch };
151
+ }
152
+
153
+ /**
154
+ * 获取 journal 配置
155
+ */
156
+ getJournalConfig(): EVOConfig['journal'] {
157
+ return { ...this.globalConfig.journal };
158
+ }
159
+
160
+ /**
161
+ * 检查 skill 是否在项目中被冻结
162
+ */
163
+ isSkillFrozen(skillId: string): boolean {
164
+ if (!this.projectConfig) return false;
165
+ const skillConfig = this.projectConfig.skills[skillId];
166
+ return skillConfig?.auto_optimize === false;
167
+ }
168
+
169
+ /**
170
+ * 获取 skill 允许的 patch 类型
171
+ */
172
+ getAllowedPatchTypes(skillId: string): string[] {
173
+ if (!this.projectConfig) return this.globalConfig.patch.allowed_types;
174
+ const skillConfig = this.projectConfig.skills[skillId];
175
+ return skillConfig?.allowed_patch_types ?? this.globalConfig.patch.allowed_types;
176
+ }
177
+
178
+ /**
179
+ * 创建默认全局配置文件
180
+ */
181
+ ensureGlobalConfig(): void {
182
+ if (!existsSync(GLOBAL_CONFIG_DIR)) {
183
+ mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
184
+ }
185
+
186
+ if (!existsSync(GLOBAL_CONFIG_FILE)) {
187
+ const tomlContent = this.configToToml(DEFAULT_CONFIG);
188
+ writeFileSync(GLOBAL_CONFIG_FILE, tomlContent, 'utf-8');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * 简单的配置转 TOML(仅支持一层嵌套)
194
+ */
195
+ private configToToml(config: object, indent = 0): string {
196
+ let result = '';
197
+ const prefix = ' '.repeat(indent);
198
+
199
+ for (const [key, value] of Object.entries(config)) {
200
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
201
+ result += `${prefix}[${key}]\n`;
202
+ result += this.configToToml(value as object, indent + 1);
203
+ result += '\n';
204
+ } else if (Array.isArray(value)) {
205
+ result += `${prefix}${key} = [${value.map((v) => `"${v}"`).join(', ')}]\n`;
206
+ } else if (typeof value === 'string') {
207
+ result += `${prefix}${key} = "${value}"\n`;
208
+ } else if (typeof value === 'boolean') {
209
+ result += `${prefix}${key} = ${value}\n`;
210
+ } else if (typeof value === 'number') {
211
+ result += `${prefix}${key} = ${value}\n`;
212
+ }
213
+ }
214
+
215
+ return result;
216
+ }
217
+ }
218
+
219
+ // 导出单例实例
220
+ export const configManager = new ConfigManager();
@@ -0,0 +1,90 @@
1
+ import type { Trace, EvaluationResult } from '../../types/index.js';
2
+
3
+ /**
4
+ * 评估规则基类
5
+ */
6
+ export abstract class BaseRule {
7
+ protected name: string;
8
+ protected description: string;
9
+
10
+ constructor(name: string, description: string) {
11
+ this.name = name;
12
+ this.description = description;
13
+ }
14
+
15
+ /**
16
+ * 评估 traces,返回评估结果
17
+ */
18
+ abstract evaluate(traces: Trace[]): EvaluationResult | null;
19
+
20
+ /**
21
+ * 获取规则名称
22
+ */
23
+ getName(): string {
24
+ return this.name;
25
+ }
26
+
27
+ /**
28
+ * 获取规则描述
29
+ */
30
+ getDescription(): string {
31
+ return this.description;
32
+ }
33
+
34
+ /**
35
+ * 提取唯一的 session IDs
36
+ */
37
+ protected extractSessionIds(traces: Trace[]): string[] {
38
+ const sessionIds = new Set(traces.map((t) => t.session_id));
39
+ return Array.from(sessionIds);
40
+ }
41
+
42
+ /**
43
+ * 按事件类型过滤 traces
44
+ */
45
+ protected filterByEventType(traces: Trace[], eventType: string): Trace[] {
46
+ return traces.filter((t) => t.event_type === eventType);
47
+ }
48
+
49
+ /**
50
+ * 按状态过滤 traces
51
+ */
52
+ protected filterByStatus(traces: Trace[], status: string): Trace[] {
53
+ return traces.filter((t) => t.status === status);
54
+ }
55
+
56
+ /**
57
+ * 获取失败的 traces
58
+ */
59
+ protected getFailedTraces(traces: Trace[]): Trace[] {
60
+ return this.filterByStatus(traces, 'failure');
61
+ }
62
+
63
+ /**
64
+ * 获取重试的 traces
65
+ */
66
+ protected getRetryTraces(traces: Trace[]): Trace[] {
67
+ return this.filterByEventType(traces, 'retry');
68
+ }
69
+
70
+ /**
71
+ * 获取文件变化的 traces
72
+ */
73
+ protected getFileChangeTraces(traces: Trace[]): Trace[] {
74
+ return this.filterByEventType(traces, 'file_change');
75
+ }
76
+
77
+ /**
78
+ * 计算置信度
79
+ */
80
+ protected calculateConfidence(
81
+ signalCount: number,
82
+ sessionCount: number,
83
+ minSignals: number = 3,
84
+ minSessions: number = 2
85
+ ): number {
86
+ const signalScore = Math.min(signalCount / minSignals, 1) * 0.6;
87
+ const sessionScore = Math.min(sessionCount / minSessions, 1) * 0.4;
88
+ return Math.min(signalScore + sessionScore, 1);
89
+ }
90
+ }
@@ -0,0 +1,86 @@
1
+ import { createChildLogger } from '../../utils/logger.js';
2
+ import { configManager } from '../../config/index.js';
3
+ import { BaseRule } from './base-rule.js';
4
+ import { RepeatedManualFixRule } from './rules/repeated-manual-fix.js';
5
+ import { RepeatedDriftRule } from './rules/repeated-drift.js';
6
+ import type { Trace, EvaluationResult } from '../../types/index.js';
7
+
8
+ const logger = createChildLogger('evaluator');
9
+
10
+ /**
11
+ * Evaluator
12
+ * 负责评估 traces 并判断是否需要优化
13
+ */
14
+ export class Evaluator {
15
+ private rules: BaseRule[] = [];
16
+ private minConfidence: number;
17
+
18
+ constructor() {
19
+ this.minConfidence = configManager.getEvaluatorConfig().min_confidence;
20
+ this.registerDefaultRules();
21
+ }
22
+
23
+ /**
24
+ * 注册默认规则
25
+ */
26
+ private registerDefaultRules(): void {
27
+ this.rules.push(new RepeatedManualFixRule());
28
+ this.rules.push(new RepeatedDriftRule());
29
+ logger.info('Default evaluation rules registered');
30
+ }
31
+
32
+ /**
33
+ * 注册自定义规则
34
+ */
35
+ registerRule(rule: BaseRule): void {
36
+ this.rules.push(rule);
37
+ logger.info(`Rule registered: ${rule.getName()}`);
38
+ }
39
+
40
+ /**
41
+ * 评估 traces
42
+ */
43
+ evaluate(traces: Trace[]): EvaluationResult | null {
44
+ if (traces.length === 0) {
45
+ return null;
46
+ }
47
+
48
+ logger.debug(`Evaluating ${traces.length} traces with ${this.rules.length} rules`);
49
+
50
+ // 运行所有规则
51
+ for (const rule of this.rules) {
52
+ try {
53
+ const result = rule.evaluate(traces);
54
+
55
+ if (result && result.confidence >= this.minConfidence) {
56
+ logger.info(`Rule "${rule.getName()}" triggered`, {
57
+ change_type: result.change_type,
58
+ confidence: result.confidence,
59
+ });
60
+ return result;
61
+ }
62
+ } catch (error) {
63
+ logger.warn(`Rule "${rule.getName()}" evaluation failed`, { error });
64
+ }
65
+ }
66
+
67
+ return null;
68
+ }
69
+
70
+ /**
71
+ * 获取所有规则
72
+ */
73
+ getRules(): BaseRule[] {
74
+ return [...this.rules];
75
+ }
76
+
77
+ /**
78
+ * 设置最小置信度
79
+ */
80
+ setMinConfidence(confidence: number): void {
81
+ this.minConfidence = confidence;
82
+ }
83
+ }
84
+
85
+ // 导出单例实例
86
+ export const evaluator = new Evaluator();
@@ -0,0 +1,109 @@
1
+ import { BaseRule } from '../base-rule.js';
2
+ import type { Trace, EvaluationResult } from '../../../types/index.js';
3
+
4
+ /**
5
+ * Repeated Drift 规则
6
+ * 检测 skill 被命中但执行反复绕过某一段
7
+ */
8
+ export class RepeatedDriftRule extends BaseRule {
9
+ constructor() {
10
+ super(
11
+ 'repeated-drift',
12
+ 'Detects when skill is hit but execution repeatedly bypasses certain sections'
13
+ );
14
+ }
15
+
16
+ evaluate(traces: Trace[]): EvaluationResult | null {
17
+ // 查找工具调用
18
+ const toolCalls = this.filterByEventType(traces, 'tool_call');
19
+
20
+ if (toolCalls.length === 0) {
21
+ return null;
22
+ }
23
+
24
+ // 找出被跳过的工具或步骤
25
+ const skippedTools = this.findSkippedTools(traces);
26
+
27
+ if (skippedTools.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ // 统计跳过次数
32
+ const skipCount = this.countSkips(traces, skippedTools);
33
+
34
+ if (skipCount < 3) {
35
+ return null;
36
+ }
37
+
38
+ const sessionIds = this.extractSessionIds(traces);
39
+ const confidence = this.calculateConfidence(skipCount, sessionIds.length);
40
+
41
+ if (confidence < 0.7) {
42
+ return null;
43
+ }
44
+
45
+ return {
46
+ should_patch: true,
47
+ change_type: 'prune_noise',
48
+ reason: `Skill section "${skippedTools[0]}" was skipped ${skipCount} times across ${sessionIds.length} sessions`,
49
+ source_sessions: sessionIds,
50
+ confidence,
51
+ };
52
+ }
53
+
54
+ /**
55
+ * 找出被跳过的工具
56
+ */
57
+ private findSkippedTools(traces: Trace[]): string[] {
58
+ const skipped: string[] = [];
59
+
60
+ // 查找在工具调用后没有结果的情况
61
+ for (const trace of traces) {
62
+ if (trace.event_type === 'tool_call' && trace.tool_name) {
63
+ // 查找后续的工具结果
64
+ const subsequentResults = traces.filter(
65
+ (t) =>
66
+ t.event_type === 'tool_result' &&
67
+ t.tool_name === trace.tool_name &&
68
+ new Date(t.timestamp) > new Date(trace.timestamp) &&
69
+ new Date(t.timestamp).getTime() - new Date(trace.timestamp).getTime() < 5000
70
+ );
71
+
72
+ if (subsequentResults.length === 0) {
73
+ // 没有找到对应的结果,可能是被跳过了
74
+ if (!skipped.includes(trace.tool_name)) {
75
+ skipped.push(trace.tool_name);
76
+ }
77
+ }
78
+ }
79
+ }
80
+
81
+ return skipped;
82
+ }
83
+
84
+ /**
85
+ * 统计跳过次数
86
+ */
87
+ private countSkips(traces: Trace[], skippedTools: string[]): number {
88
+ let count = 0;
89
+
90
+ for (const trace of traces) {
91
+ if (trace.event_type === 'tool_call' && skippedTools.includes(trace.tool_name ?? '')) {
92
+ // 检查是否有对应的工具结果
93
+ const hasResult = traces.some(
94
+ (t) =>
95
+ t.event_type === 'tool_result' &&
96
+ t.tool_name === trace.tool_name &&
97
+ new Date(t.timestamp) > new Date(trace.timestamp) &&
98
+ new Date(t.timestamp).getTime() - new Date(trace.timestamp).getTime() < 5000
99
+ );
100
+
101
+ if (!hasResult) {
102
+ count++;
103
+ }
104
+ }
105
+ }
106
+
107
+ return count;
108
+ }
109
+ }
@@ -0,0 +1,129 @@
1
+ import { BaseRule } from '../base-rule.js';
2
+ import type { Trace, EvaluationResult } from '../../../types/index.js';
3
+
4
+ /**
5
+ * Repeated Manual Fix 规则
6
+ * 检测用户是否在同类任务中反复手动补充相同步骤
7
+ */
8
+ export class RepeatedManualFixRule extends BaseRule {
9
+ constructor() {
10
+ super(
11
+ 'repeated-manual-fix',
12
+ 'Detects when users repeatedly manually supplement the same steps after agent output'
13
+ );
14
+ }
15
+
16
+ evaluate(traces: Trace[]): EvaluationResult | null {
17
+ // 查找用户在助手输出后的输入
18
+ const assistantOutputs = this.filterByEventType(traces, 'assistant_output');
19
+ const userInputs = this.filterByEventType(traces, 'user_input');
20
+
21
+ if (assistantOutputs.length === 0 || userInputs.length === 0) {
22
+ return null;
23
+ }
24
+
25
+ // 分析用户输入中是否有重复的模式
26
+ const patterns = this.extractPatterns(userInputs);
27
+
28
+ // 找出出现频率最高的模式
29
+ const frequentPatterns = this.findFrequentPatterns(patterns, 3);
30
+
31
+ if (frequentPatterns.length === 0) {
32
+ return null;
33
+ }
34
+
35
+ // 检查这些模式是否出现在助手输出之后
36
+ const signals = this.countPostAssistantSignals(assistantOutputs, userInputs, frequentPatterns);
37
+
38
+ if (signals.count < 3) {
39
+ return null;
40
+ }
41
+
42
+ const sessionIds = this.extractSessionIds(traces);
43
+ const confidence = this.calculateConfidence(signals.count, sessionIds.length);
44
+
45
+ if (confidence < 0.7) {
46
+ return null;
47
+ }
48
+
49
+ return {
50
+ should_patch: true,
51
+ change_type: 'add_fallback',
52
+ reason: `User manually supplemented "${signals.pattern}" in ${signals.count} sessions after agent output`,
53
+ source_sessions: sessionIds,
54
+ confidence,
55
+ };
56
+ }
57
+
58
+ /**
59
+ * 提取用户输入的模式
60
+ */
61
+ private extractPatterns(userInputs: Trace[]): Map<string, number> {
62
+ const patterns = new Map<string, number>();
63
+
64
+ for (const input of userInputs) {
65
+ if (!input.user_input) continue;
66
+
67
+ // 简单的模式提取:取前 50 个字符作为模式
68
+ const pattern = input.user_input.substring(0, 50).toLowerCase().trim();
69
+ if (pattern.length > 10) {
70
+ patterns.set(pattern, (patterns.get(pattern) ?? 0) + 1);
71
+ }
72
+ }
73
+
74
+ return patterns;
75
+ }
76
+
77
+ /**
78
+ * 找出频繁出现的模式
79
+ */
80
+ private findFrequentPatterns(patterns: Map<string, number>, minCount: number): string[] {
81
+ const result: string[] = [];
82
+
83
+ for (const [pattern, count] of patterns.entries()) {
84
+ if (count >= minCount) {
85
+ result.push(pattern);
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ /**
93
+ * 统计助手输出后出现的信号
94
+ */
95
+ private countPostAssistantSignals(
96
+ assistantOutputs: Trace[],
97
+ userInputs: Trace[],
98
+ patterns: string[]
99
+ ): { count: number; pattern: string } {
100
+ let maxCount = 0;
101
+ let bestPattern = '';
102
+
103
+ for (const pattern of patterns) {
104
+ let count = 0;
105
+
106
+ for (const output of assistantOutputs) {
107
+ // 查找在助手输出后的用户输入
108
+ const subsequentInputs = userInputs.filter(
109
+ (input) =>
110
+ new Date(input.timestamp) > new Date(output.timestamp) &&
111
+ new Date(input.timestamp).getTime() - new Date(output.timestamp).getTime() < 60000 // 1 分钟内
112
+ );
113
+
114
+ for (const input of subsequentInputs) {
115
+ if (input.user_input?.toLowerCase().includes(pattern)) {
116
+ count++;
117
+ }
118
+ }
119
+ }
120
+
121
+ if (count > maxCount) {
122
+ maxCount = count;
123
+ bestPattern = pattern;
124
+ }
125
+ }
126
+
127
+ return { count: maxCount, pattern: bestPattern };
128
+ }
129
+ }