autosnippet 3.1.5 → 3.1.6

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.
@@ -16,6 +16,7 @@
16
16
 
17
17
  import { TierScheduler } from './pipeline/tier-scheduler.js';
18
18
  import { SUBMISSION_SCHEMA, EXAMPLE_TEMPLATES } from './shared/dimension-text.js';
19
+ import { getDimensionSOP, sopToCompactText, PRE_SUBMIT_CHECKLIST } from './shared/dimension-sop.js';
19
20
 
20
21
  // ── 常量 ────────────────────────────────────────────────────
21
22
 
@@ -42,25 +43,45 @@ const SIZE_THRESHOLDS = {
42
43
  * @returns {object} — Mission Briefing 维度任务对象
43
44
  */
44
45
  function enrichDimensionTask(dim, skills, tier) {
45
- // ── analysisGuide: dim.guide + skill 内容增强 ──
46
- let analysisGuide = `分析项目的${dim.label}。\n\n重点关注:\n${dim.guide}\n\n分析要求:\n1. 在具体文件/类中验证发现(引用 ≥3 个文件路径)\n2. 说明具体实现方式和代码特征\n3. 解释设计意图\n4. 提供统计数据(数量、占比)\n5. 每个知识点独立描述,目标 3-5 个发现`;
46
+ // ── analysisGuide: SOP 优先使用维度专属 SOP,否则回退通用指引 ──
47
+ const sop = getDimensionSOP(dim.id);
48
+ let analysisGuide;
49
+
50
+ if (sop) {
51
+ // SOP 结构化模式: steps + timeEstimate + commonMistakes
52
+ analysisGuide = {
53
+ goal: `分析项目的${dim.label}`,
54
+ focus: dim.guide,
55
+ steps: sop.steps,
56
+ timeEstimate: sop.timeEstimate || '10-15 min',
57
+ commonMistakes: sop.commonMistakes || [],
58
+ };
59
+ } else {
60
+ // 无 SOP 的维度: 保留原始文本指引(兼容新增维度未定义 SOP 的情况)
61
+ analysisGuide = `分析项目的${dim.label}。\n\n重点关注:\n${dim.guide}\n\n分析要求:\n1. 在具体文件/类中验证发现(引用 ≥3 个文件路径)\n2. 说明具体实现方式和代码特征\n3. 解释设计意图\n4. 提供统计数据(数量、占比)\n5. 每个知识点独立描述,目标 3-5 个发现`;
62
+ }
47
63
 
48
64
  // 如果有相关 skill,注入到 analysisGuide 中
49
65
  const relatedSkill = skills?.find(
50
66
  (s) => s.relatedDimension === dim.id || s.name === dim.skillMeta?.name
51
67
  );
52
68
  if (relatedSkill) {
53
- analysisGuide += `\n\n参考已有 Skill (${relatedSkill.name}):\n${relatedSkill.content?.substring(0, 500) || ''}`;
69
+ const skillHint = `参考已有 Skill (${relatedSkill.name}):\n${relatedSkill.content?.substring(0, 500) || ''}`;
70
+ if (typeof analysisGuide === 'string') {
71
+ analysisGuide += `\n\n${skillHint}`;
72
+ } else {
73
+ analysisGuide.referenceSkill = skillHint;
74
+ }
54
75
  }
55
76
 
56
- // ── submissionSpec ──
77
+ // ── submissionSpec: 嵌入 Quality Checklist ──
57
78
  const submissionSpec = {
58
79
  knowledgeTypes: dim.knowledgeTypes || [],
59
80
  targetCandidateCount: '3-5',
60
81
  contentStyle:
61
82
  '融合基本用法与项目特征的「项目特写」。\n四大核心内容:\n1. 项目选择了什么 — 采用了哪种写法/模式/约定\n2. 为什么这样选 — 统计分布、占比、历史决策\n3. 项目禁止什么 — 反模式、已废弃写法\n4. 新代码怎么写 — 可直接复制的代码模板 + 来源标注',
62
83
  contentQuality:
63
- 'content.markdown 必须 ≥200 字符,包含: (1) ## 标题 (2) 正文说明 (3) 至少一个 ```代码块``` (4) 来源标注「(来源: FileName.ext:行号)」。短于 200 字符的提交会被拒绝。',
84
+ 'content.markdown 必须 ≥200 字符,包含: (1) ## 标题 (2) 正文说明 (3) 至少一个 ```代码块``` (4) 来源标注「(来源: FileName.ext:行号)」。短于 200 字符的提交会被拒绝。\n【禁止】标题和正文中不得出现 "Agent" 字样 — 所有候选必须以项目规范/开发规范的视角撰写,描述的是项目规则而非 AI Agent 指南。',
64
85
  cursorFields: {
65
86
  trigger: '@前缀-kebab-case(每个候选唯一)',
66
87
  kind: 'rule=强制约束 | pattern=实现模式 | fact=项目事实',
@@ -71,6 +92,7 @@ function enrichDimensionTask(dim, skills, tier) {
71
92
  },
72
93
  dimensionCompleteGuide:
73
94
  '调用 dimension_complete 时必须传递: referencedFiles=[本维度分析过的全部文件路径], keyFindings=[3-5条关键发现摘要], analysisText=详细分析报告(≥500字符,含##标题+列表+代码块)',
95
+ preSubmitChecklist: PRE_SUBMIT_CHECKLIST,
74
96
  };
75
97
 
76
98
  // ── skillMeta ──
@@ -533,6 +555,15 @@ export function buildMissionBriefing({
533
555
  for (const dim of briefing.dimensions) {
534
556
  delete dim.evidenceStarters;
535
557
  }
558
+ // Level 5: SOP 降级为紧凑文本 + 移除 FAIL_EXAMPLES
559
+ for (const dim of briefing.dimensions) {
560
+ if (dim.analysisGuide && typeof dim.analysisGuide === 'object') {
561
+ dim.analysisGuide = sopToCompactText(dim.analysisGuide);
562
+ }
563
+ if (dim.submissionSpec?.preSubmitChecklist?.FAIL_EXAMPLES) {
564
+ delete dim.submissionSpec.preSubmitChecklist.FAIL_EXAMPLES;
565
+ }
566
+ }
536
567
  // 更新 meta
537
568
  const newSize = JSON.stringify(briefing).length;
538
569
  briefing.meta.responseSizeKB = Math.round(newSize / 1024);
@@ -90,10 +90,10 @@ export const baseDimensions = [
90
90
  'Project tech stack, module structure, third-party dependencies, base extensions/classes, event hooks, infrastructure services, and runtime/interop features (auto-generated by bootstrap)',
91
91
  },
92
92
  },
93
- // ⑦ Agent 开发注意事项(Skill)
93
+ // ⑦ 项目开发强制规范(Skill)
94
94
  {
95
95
  id: 'agent-guidelines',
96
- label: 'Agent开发注意事项',
96
+ label: '项目开发强制规范',
97
97
  guide:
98
98
  '三大核心原则(严谨性/深度特征挖掘/完整性)、命名强制、线程安全、内存约束、已废弃 API 标记、架构约束注释、TODO/FIXME',
99
99
  knowledgeTypes: ['boundary-constraint', 'code-standard'],
@@ -101,7 +101,7 @@ export const baseDimensions = [
101
101
  skillMeta: {
102
102
  name: 'project-agent-guidelines',
103
103
  description:
104
- 'Mandatory coding rules, deprecated APIs and agent constraints for this project (auto-generated by bootstrap)',
104
+ 'Mandatory coding rules, deprecated APIs and project constraints (auto-generated by bootstrap)',
105
105
  },
106
106
  },
107
107
 
@@ -112,7 +112,7 @@ export const baseDimensions = [
112
112
  id: 'objc-deep-scan',
113
113
  label: '深度扫描(常量/Hook)',
114
114
  guide:
115
- '全量扫描 #define 值宏/函数宏、extern/static 常量、Method Swizzling hook 对(Agent 必须使用项目常量,修改被 hook 方法前必须查阅 hook 清单)',
115
+ '全量扫描 #define 值宏/函数宏、extern/static 常量、Method Swizzling hook 对(开发者必须使用项目常量,修改被 hook 方法前必须查阅 hook 清单)',
116
116
  knowledgeTypes: ['code-standard', 'code-pattern'],
117
117
  conditions: { languages: ['objectivec', 'swift'] },
118
118
  skillWorthy: true,
@@ -128,7 +128,7 @@ export const baseDimensions = [
128
128
  id: 'category-scan',
129
129
  label: '基础类分类方法扫描',
130
130
  guide:
131
- 'Foundation/UIKit Category/Extension 逐方法清单(含完整实现代码与项目使用频次),仅扫描基础类分类、不含业务代码(Agent 遇到同等功能必须使用项目已有分类方法,禁止重复实现)',
131
+ 'Foundation/UIKit Category/Extension 逐方法清单(含完整实现代码与项目使用频次),仅扫描基础类分类、不含业务代码(遇到同等功能必须使用项目已有分类方法,禁止重复实现)',
132
132
  knowledgeTypes: ['code-standard', 'code-pattern'],
133
133
  conditions: { languages: ['objectivec', 'swift'] },
134
134
  skillWorthy: true,
@@ -95,7 +95,7 @@ export class EpisodicMemory {
95
95
  /** @type {object} 项目上下文 (不变信息) */
96
96
  #projectContext;
97
97
 
98
- /** @type {import('../../../../../lib/infrastructure/logging/Logger.js').default} */
98
+ /** @type {import('../../../../../infrastructure/logging/Logger.js').default} */
99
99
  #logger;
100
100
 
101
101
  /**
@@ -32,7 +32,7 @@ export class ToolResultCache {
32
32
  /** @type {Map<string, {content: string, cachedAt: number, hitCount: number}>} */
33
33
  #fileCache = new Map();
34
34
 
35
- /** @type {import('../../../../../lib/infrastructure/logging/Logger.js').default} */
35
+ /** @type {import('../../../../../infrastructure/logging/Logger.js').default} */
36
36
  #logger;
37
37
 
38
38
  /** @type {{hits: number, misses: number, evictions: number}} */
@@ -103,8 +103,8 @@ export const DIMENSION_CONFIGS_V3 = {
103
103
  allowedKnowledgeTypes: ['best-practice'],
104
104
  },
105
105
  'agent-guidelines': {
106
- label: 'Agent 开发注意事项',
107
- guide: '总结 Agent 在此项目开发时必须遵守的规则和约束。',
106
+ label: '项目开发强制规范',
107
+ guide: '总结在此项目开发时必须遵守的强制规则和约束。',
108
108
  focusAreas: [
109
109
  '命名强制规则和前缀约定',
110
110
  '线程安全约束',
@@ -121,7 +121,7 @@ export async function runNoAiFallback(fillContext) {
121
121
  if (guidelines) {
122
122
  candidates.push(guidelines);
123
123
  skills.push(
124
- _wrapAsSkill('agent-guidelines', 'Agent 开发注意事项', guidelines.content.markdown)
124
+ _wrapAsSkill('agent-guidelines', '项目开发强制规范', guidelines.content.markdown)
125
125
  );
126
126
  report.candidatesCreated++;
127
127
  report.skillsCreated++;
@@ -0,0 +1,670 @@
1
+ /**
2
+ * dimension-sop.js — 维度级 SOP (Standard Operating Procedure)
3
+ *
4
+ * 为每个维度定义结构化分析步骤,替代 enrichDimensionTask() 中
5
+ * 原有的通用 analysisGuide 字符串。
6
+ *
7
+ * 参考:
8
+ * - MetaGPT SOP 驱动模式 (docs/design/external-agent-quality-gap.md §4.2)
9
+ * - 内部 Agent 的 AnalystAgent + ProducerAgent 双阶段流程
10
+ *
11
+ * 设计原则:
12
+ * - 每个维度 4 个阶段: 扫描 → 验证 → 异常检测 → 提交
13
+ * - steps[].tools 仅为建议,外部 Agent 可用自身原生能力替代
14
+ * - commonMistakes 来自实际 cold start 观察到的低质量模式
15
+ *
16
+ * @module bootstrap/shared/dimension-sop
17
+ */
18
+
19
+ // ═══════════════════════════════════════════════════════════
20
+ // 维度 SOP 注册表
21
+ // ═══════════════════════════════════════════════════════════
22
+
23
+ export const DIMENSION_SOP = {
24
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
25
+ // 通用维度
26
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
27
+
28
+ 'code-standard': {
29
+ steps: [
30
+ {
31
+ phase: '1. 全局扫描',
32
+ action: '搜索项目中出现频率最高的类名前缀/后缀、方法命名模式',
33
+ expectedOutput: '识别 3-5 个主要命名模式及其出现频率统计',
34
+ tools: ['grep_search 搜索类定义/接口定义', '浏览核心目录下的文件列表'],
35
+ },
36
+ {
37
+ phase: '2. 深度验证',
38
+ action: '阅读 5+ 个核心类文件,验证命名模式、注释风格、文件组织方式是否一致',
39
+ expectedOutput: '每个模式至少有 3 个文件证据,含具体行号',
40
+ tools: ['read_file 逐个阅读核心类/模块'],
41
+ },
42
+ {
43
+ phase: '3. 异常检测',
44
+ action: '搜索不符合主流命名模式的例外,确认是否为已废弃写法或特殊例外',
45
+ expectedOutput: '识别例外模式及其原因(历史遗留/第三方要求等)',
46
+ },
47
+ {
48
+ phase: '4. 提交',
49
+ action: '按项目特写格式提交 3-5 个知识候选,每个候选聚焦一种具体代码规范',
50
+ qualityChecklist: [
51
+ '每个 content ≥200 字符',
52
+ '每个候选引用 ≥3 个文件路径',
53
+ 'doClause 用英文祈使句,具体到命名模式',
54
+ 'coreCode 提供可复制的完整代码骨架',
55
+ ],
56
+ },
57
+ ],
58
+ timeEstimate: '10-15 min',
59
+ commonMistakes: [
60
+ '不要只扫描 1 个文件就提交 — 至少读 5+ 个文件验证模式一致性',
61
+ '不要把"类名前缀"和"方法签名风格"合并成一个候选 — 每种规范应独立成条',
62
+ '不要写空泛的规范如"use camelCase" — 必须写明项目特定的前缀/后缀/风格',
63
+ 'content 中必须有 (来源: FileName.ext:行号) 标注具体出处',
64
+ ],
65
+ },
66
+
67
+ 'code-pattern': {
68
+ steps: [
69
+ {
70
+ phase: '1. 模式识别',
71
+ action: '搜索常见设计模式关键词: Singleton/shared/default, Factory/create/build, Delegate/protocol, Observer/notify',
72
+ expectedOutput: '列出项目中使用的设计模式类型及代表性实现',
73
+ tools: ['grep_search 搜索单例、工厂、委托、观察者等关键词'],
74
+ },
75
+ {
76
+ phase: '2. 实现验证',
77
+ action: '阅读每种模式的 2-3 个实现,记录实现方式是否统一',
78
+ expectedOutput: '每种模式的标准实现写法 + 文件证据 + 变体说明',
79
+ tools: ['read_file 阅读模式实现类'],
80
+ },
81
+ {
82
+ phase: '3. 继承与组合分析',
83
+ action: '追踪核心基类的继承链,分析"基类-子类"和"接口-实现"关系',
84
+ expectedOutput: '核心继承图、关键基类的扩展约束',
85
+ },
86
+ {
87
+ phase: '4. 提交',
88
+ action: '每种设计模式单独提交一个候选,包含标准实现代码和使用约束',
89
+ qualityChecklist: [
90
+ '每个候选只聚焦一种设计模式',
91
+ 'content 包含 ✅ 正确写法 和 ❌ 禁止写法',
92
+ 'coreCode 是可复制的模式骨架代码',
93
+ '说明何时应使用此模式(whenClause)',
94
+ ],
95
+ },
96
+ ],
97
+ timeEstimate: '10-15 min',
98
+ commonMistakes: [
99
+ '不要只列出模式名称而不分析实现 — 必须展示项目的具体实现代码',
100
+ '不要把 Singleton 和 Factory 合并成一个候选 — 每种模式独立提交',
101
+ '不要忽略项目中模式的变体 — 如果项目的 Singleton 不用标准 dispatch_once,要说明其特殊写法',
102
+ ],
103
+ },
104
+
105
+ architecture: {
106
+ steps: [
107
+ {
108
+ phase: '1. 目录结构分析',
109
+ action: '浏览项目根目录和核心子目录,识别分层结构(Controller/View/Model/Service 等)',
110
+ expectedOutput: '项目的分层架构图和模块划分',
111
+ tools: ['list_dir 浏览项目目录结构'],
112
+ },
113
+ {
114
+ phase: '2. 依赖关系分析',
115
+ action: '阅读核心模块的 import/include,追踪模块间依赖方向',
116
+ expectedOutput: '模块间通信方式清单(Protocol/Delegate/Notification/DI 等)',
117
+ tools: ['grep_search 搜索 import/include/require 语句'],
118
+ },
119
+ {
120
+ phase: '3. 边界约束验证',
121
+ action: '确认是否存在分层约束(如 View 不直接访问 Model),搜索违反约束的例外',
122
+ expectedOutput: '架构约束规则 + 违规例外及原因',
123
+ },
124
+ {
125
+ phase: '4. 提交',
126
+ action: '分别提交分层架构、模块通信、依赖管理等知识候选',
127
+ qualityChecklist: [
128
+ 'content 包含架构层次图或文字描述',
129
+ '引用具体目录路径和核心文件',
130
+ 'doClause 表达架构约束规则',
131
+ 'dontClause 表达禁止的跨层调用',
132
+ ],
133
+ },
134
+ ],
135
+ timeEstimate: '10-15 min',
136
+ commonMistakes: [
137
+ '不要只描述目录名 — 要分析每层的职责和通信方式',
138
+ '不要忽略依赖方向 — 是 Controller→Service 还是双向依赖?',
139
+ '不要把整个架构写进一个候选 — 分层结构、通信方式、依赖管理应分别提交',
140
+ ],
141
+ },
142
+
143
+ 'best-practice': {
144
+ steps: [
145
+ {
146
+ phase: '1. 错误处理扫描',
147
+ action: '搜索 try/catch/throw、Error/Exception、错误码定义,分析错误处理策略',
148
+ expectedOutput: '错误处理模式分类 + 统计分布',
149
+ tools: ['grep_search 搜索错误处理关键词'],
150
+ },
151
+ {
152
+ phase: '2. 并发与安全分析',
153
+ action: '搜索锁/队列/线程相关代码(dispatch_queue/mutex/synchronized/async-await),分析并发安全策略',
154
+ expectedOutput: '并发模式 + 线程安全约束',
155
+ tools: ['grep_search 搜索并发相关关键词'],
156
+ },
157
+ {
158
+ phase: '3. 日志与调试',
159
+ action: '搜索日志框架使用(NSLog/Logger/print/console.log),分析日志规范',
160
+ expectedOutput: '日志级别使用惯例 + 调试基础设施',
161
+ },
162
+ {
163
+ phase: '4. 提交',
164
+ action: '每种最佳实践独立提交,包含正反面代码示例',
165
+ qualityChecklist: [
166
+ '每个候选聚焦一种实践(如"错误处理"或"并发安全")',
167
+ 'content 包含 ✅ 推荐写法 和 ❌ 反模式',
168
+ '提供具体的统计数据(如"项目中 80% 的错误处理使用 Result 类型")',
169
+ 'coreCode 展示推荐的代码模板',
170
+ ],
171
+ },
172
+ ],
173
+ timeEstimate: '10-15 min',
174
+ commonMistakes: [
175
+ '不要笼统写"项目有错误处理" — 必须说明具体的处理策略(Result/throw/错误码)',
176
+ '不要忽略反模式 — dontClause 要具体说明禁止的做法',
177
+ '不要遗漏并发安全 — 这是代码补全时最容易出错的地方',
178
+ ],
179
+ },
180
+
181
+ 'event-and-data-flow': {
182
+ steps: [
183
+ {
184
+ phase: '1. 事件机制扫描',
185
+ action: '搜索 Delegate/Protocol、Notification、Callback/Closure/Block、EventEmitter 等事件传播机制',
186
+ expectedOutput: '事件传播机制清单 + 各机制使用频率统计',
187
+ tools: ['grep_search 搜索事件相关关键词'],
188
+ },
189
+ {
190
+ phase: '2. 数据流追踪',
191
+ action: '选取 2-3 个核心业务流程,追踪数据从输入到持久化的完整路径',
192
+ expectedOutput: '数据流转路径图 + 状态管理方式',
193
+ tools: ['read_file 阅读核心业务流程入口'],
194
+ },
195
+ {
196
+ phase: '3. 持久化方案',
197
+ action: '搜索数据库/缓存/文件存储相关代码,分析数据持久化方案',
198
+ expectedOutput: '持久化技术栈 + 数据访问模式',
199
+ },
200
+ {
201
+ phase: '4. 提交',
202
+ action: '事件传播和数据流分别提交候选',
203
+ qualityChecklist: [
204
+ '每个候选聚焦一种事件/数据流模式',
205
+ 'content 描述具体的事件传播链路(从触发到响应)',
206
+ 'whenClause 描述何时使用此事件/数据模式',
207
+ '引用具体的文件路径和代码行',
208
+ ],
209
+ },
210
+ ],
211
+ timeEstimate: '10-15 min',
212
+ commonMistakes: [
213
+ '不要只列出"项目使用 Notification" — 要说明具体的通知名、发送者、接收者',
214
+ '不要混淆事件传播和数据流 — Delegate 是事件机制,CoreData 是数据流',
215
+ '应关注跨模块的事件链路 — 单模块内的方法调用不算事件传播',
216
+ ],
217
+ },
218
+
219
+ 'project-profile': {
220
+ steps: [
221
+ {
222
+ phase: '1. 项目结构概览',
223
+ action: '浏览根目录和核心子目录,识别模块划分和技术栈',
224
+ expectedOutput: '技术栈清单、目录结构图、模块列表',
225
+ tools: ['list_dir 浏览目录', 'read_file 阅读配置文件(Package.swift/Podfile/package.json 等)'],
226
+ },
227
+ {
228
+ phase: '2. 依赖与基础设施',
229
+ action: '阅读依赖配置文件,识别三方库及其用途;搜索基础设施服务(网络/存储/日志)',
230
+ expectedOutput: '三方依赖清单 + 基础设施服务注册表',
231
+ tools: ['read_file 阅读依赖配置', 'grep_search 搜索服务注册/初始化代码'],
232
+ },
233
+ {
234
+ phase: '3. 入口与生命周期',
235
+ action: '找到应用入口点,分析启动流程和生命周期 hook',
236
+ expectedOutput: '启动流程链路 + 核心生命周期回调',
237
+ tools: ['grep_search 搜索 main/AppDelegate/Application 等入口关键词'],
238
+ },
239
+ {
240
+ phase: '4. 提交',
241
+ action: '分模块提交项目特征候选(技术栈、依赖、入口点、基础设施等)',
242
+ qualityChecklist: [
243
+ 'content 包含具体的技术栈版本和依赖列表',
244
+ '引用配置文件和入口文件路径',
245
+ '每个候选只关注一个方面(如"三方依赖"或"启动流程")',
246
+ 'coreCode 展示关键的配置或初始化代码',
247
+ ],
248
+ },
249
+ ],
250
+ timeEstimate: '10-15 min',
251
+ commonMistakes: [
252
+ '不要直接复制依赖列表 — 要说明每个关键依赖的用途和版本',
253
+ '不要只写"项目使用 MVC" — 要说明具体的分层职责和文件组织方式',
254
+ '不要遗漏项目的自定义基类和全局定义 — 这些是开发时最容易忽略的',
255
+ ],
256
+ },
257
+
258
+ 'agent-guidelines': {
259
+ steps: [
260
+ {
261
+ phase: '1. 综合前序维度发现',
262
+ action: '回顾之前所有维度分析的结果,提取项目开发中最重要的强制约束规则',
263
+ expectedOutput: '关键约束规则清单(命名、线程、内存、已废弃 API)',
264
+ },
265
+ {
266
+ phase: '2. 搜索显式约束',
267
+ action: '搜索 TODO/FIXME/DEPRECATED/WARNING 注释,以及 lint 配置中的强制规则',
268
+ expectedOutput: '项目显式标注的约束和已废弃 API 列表',
269
+ tools: ['grep_search 搜索 TODO/FIXME/DEPRECATED/WARNING'],
270
+ },
271
+ {
272
+ phase: '3. 推导隐式约束',
273
+ action: '从代码模式中推导隐式约束(如"所有 Manager 必须是单例"、"网络请求必须通过 BaseRequest")',
274
+ expectedOutput: '隐式约束规则 + 代码证据',
275
+ },
276
+ {
277
+ phase: '4. 提交',
278
+ action: '每条开发约束单独提交,确保 doClause 表达清晰的强制规则',
279
+ qualityChecklist: [
280
+ '每个候选是一条明确的项目开发规则',
281
+ 'doClause 以动词开头,表达强制性要求',
282
+ 'dontClause 明确禁止的做法',
283
+ 'content 说明规则来源和违反后果',
284
+ '【禁止】标题和内容中不要出现 "Agent" 字样 — 应写为项目规范/开发规范',
285
+ ],
286
+ },
287
+ ],
288
+ timeEstimate: '8-12 min',
289
+ commonMistakes: [
290
+ '【重要】不要在标题或内容中使用 "Agent" 字样 — 这是项目编码规范,不是 Agent 说明书',
291
+ '不要只写通用编程建议 — 必须是此项目特有的约束',
292
+ '不要遗漏线程安全约束 — 这是代码补全时最容易犯的错误',
293
+ '本维度应在最后分析 — 需要综合前序维度的发现',
294
+ '不要把所有规则写进一个候选 — 每条约束独立成条',
295
+ ],
296
+ },
297
+
298
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
299
+ // 语言条件维度
300
+ // ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
301
+
302
+ 'objc-deep-scan': {
303
+ steps: [
304
+ {
305
+ phase: '1. 宏定义扫描',
306
+ action: '搜索 #define 值宏和函数宏,分类统计常量宏 vs 功能宏',
307
+ expectedOutput: '宏定义分类清单 + 使用频率统计',
308
+ tools: ['grep_search 搜索 #define'],
309
+ },
310
+ {
311
+ phase: '2. 常量定义扫描',
312
+ action: '搜索 extern/static 常量定义、NS_ENUM/NS_OPTIONS 类型定义',
313
+ expectedOutput: '常量定义清单 + 命名约定分析',
314
+ tools: ['grep_search 搜索 extern/static const'],
315
+ },
316
+ {
317
+ phase: '3. Method Swizzling 扫描',
318
+ action: '搜索 method_exchangeImplementations、class_addMethod、+load、+initialize',
319
+ expectedOutput: 'Hook 清单(原方法→替换方法映射)+ 执行时机',
320
+ tools: ['grep_search 搜索 swizzl/method_exchange/class_addMethod'],
321
+ },
322
+ {
323
+ phase: '4. 提交',
324
+ action: '宏定义、常量、Hook 分别提交候选,确保包含完整实现代码',
325
+ qualityChecklist: [
326
+ 'coreCode 包含完整的宏/常量/Hook 实现',
327
+ 'content 包含使用频率和场景说明',
328
+ 'Hook 候选必须说明原方法和替换方法的对应关系',
329
+ '常量候选必须包含 extern 声明和值定义',
330
+ ],
331
+ },
332
+ ],
333
+ timeEstimate: '10-15 min',
334
+ commonMistakes: [
335
+ '不要只列出宏名 — 必须包含宏定义的完整值和使用场景',
336
+ 'Hook 必须说明 hook 的目的和潜在风险',
337
+ '常量必须说明值和使用位置 — Agent 需要知道用哪个常量替代硬编码',
338
+ ],
339
+ },
340
+
341
+ 'category-scan': {
342
+ steps: [
343
+ {
344
+ phase: '1. Category 文件定位',
345
+ action: '搜索 Foundation/UIKit 基础类的 Category 文件(NSString+/UIView+/NSDictionary+ 等)',
346
+ expectedOutput: 'Category 文件列表 + 基类分类统计',
347
+ tools: ['file_search 搜索 +Extension/+Category 文件', 'grep_search 搜索 @interface.*Category'],
348
+ },
349
+ {
350
+ phase: '2. 逐方法分析',
351
+ action: '阅读每个 Category,记录方法签名、实现代码和使用场景',
352
+ expectedOutput: '每个 Category 的方法清单 + 核心方法实现代码',
353
+ tools: ['read_file 逐个阅读 Category 文件'],
354
+ },
355
+ {
356
+ phase: '3. 使用频率验证',
357
+ action: '搜索核心 Category 方法在项目中的调用频率',
358
+ expectedOutput: '高频使用的 Category 方法排行',
359
+ tools: ['grep_search 搜索 Category 方法名'],
360
+ },
361
+ {
362
+ phase: '4. 提交',
363
+ action: '按基类分组提交候选(NSString Category 一条、UIView Category 一条等)',
364
+ qualityChecklist: [
365
+ 'content 包含完整的方法签名列表',
366
+ 'coreCode 包含最常用方法的实现代码',
367
+ 'doClause 强制要求使用已有 Category 方法',
368
+ 'dontClause 禁止重复实现同功能方法',
369
+ ],
370
+ },
371
+ ],
372
+ timeEstimate: '10-15 min',
373
+ commonMistakes: [
374
+ '不要遗漏任何 Category 方法 — 要做到全量扫描',
375
+ '不要只列出方法名不含实现 — Agent 需要实现代码才能正确使用',
376
+ '不要把业务代码 Category 混入基础类 Category',
377
+ ],
378
+ },
379
+
380
+ 'module-export-scan': {
381
+ steps: [
382
+ {
383
+ phase: '1. barrel export 扫描',
384
+ action: '搜索 index.ts/index.js 文件,分析 re-export 结构',
385
+ expectedOutput: 'barrel export 结构图 + public API surface',
386
+ tools: ['file_search 搜索 index.ts/index.js', 'grep_search 搜索 export.*from'],
387
+ },
388
+ {
389
+ phase: '2. 导出模式分析',
390
+ action: '统计 named export vs default export 使用比例,分析导出命名约定',
391
+ expectedOutput: '导出模式统计 + 命名约定',
392
+ tools: ['grep_search 搜索 export default/export const/export function'],
393
+ },
394
+ {
395
+ phase: '3. 循环依赖检测',
396
+ action: '追踪 import 链路,检查模块间是否存在循环依赖',
397
+ expectedOutput: '循环依赖列表(如有)+ 模块依赖方向约束',
398
+ },
399
+ {
400
+ phase: '4. 提交',
401
+ action: '分别提交 barrel export 结构、导出约定、依赖约束等候选',
402
+ qualityChecklist: [
403
+ 'content 包含具体的 import/export 代码示例',
404
+ '引用 index.ts 和核心模块文件路径',
405
+ 'doClause 表达导出约定规则',
406
+ 'coreCode 展示标准的 export 写法',
407
+ ],
408
+ },
409
+ ],
410
+ timeEstimate: '10-15 min',
411
+ commonMistakes: [
412
+ '不要只说"项目使用 named export" — 要展示具体的 export 模式和约定',
413
+ '不要忽略 re-export 链路 — barrel export 结构对项目维护很重要',
414
+ '如果项目有 tree-shaking 相关配置,需要特别说明',
415
+ ],
416
+ },
417
+
418
+ 'framework-convention-scan': {
419
+ steps: [
420
+ {
421
+ phase: '1. 组件结构分析',
422
+ action: '浏览组件目录,分析组件文件组织方式(单文件/目录组件/atoms-molecules 等)',
423
+ expectedOutput: '组件目录结构约定 + 命名约定',
424
+ tools: ['list_dir 浏览组件目录', 'read_file 阅读代表性组件'],
425
+ },
426
+ {
427
+ phase: '2. 状态管理分析',
428
+ action: '搜索状态管理相关代码(store/reducer/action/mutation/atom 等)',
429
+ expectedOutput: '状态管理方案 + 使用模式 + 约定',
430
+ tools: ['grep_search 搜索 store/useStore/createSlice/defineStore 等'],
431
+ },
432
+ {
433
+ phase: '3. 路由与数据获取',
434
+ action: '分析路由配置和数据获取模式(SSR/CSR/ISR 等)',
435
+ expectedOutput: '路由约定 + 数据获取模式',
436
+ tools: ['grep_search 搜索 router/route/fetch/loader/getServerSideProps'],
437
+ },
438
+ {
439
+ phase: '4. 提交',
440
+ action: '组件约定、状态管理、路由模式分别提交候选',
441
+ qualityChecklist: [
442
+ 'content 包含组件/状态/路由的代码示例',
443
+ '引用具体框架版本和配置文件',
444
+ 'coreCode 是可复制的标准写法模板',
445
+ 'whenClause 描述何时使用此约定',
446
+ ],
447
+ },
448
+ ],
449
+ timeEstimate: '12-18 min',
450
+ commonMistakes: [
451
+ '不要假设框架版本 — 从 package.json 确认具体版本再分析',
452
+ '不要只分析组件结构忽略状态管理 — 状态管理模式对项目开发至关重要',
453
+ '不要混淆 SSR 和 CSR 数据获取模式',
454
+ ],
455
+ },
456
+
457
+ 'python-package-scan': {
458
+ steps: [
459
+ {
460
+ phase: '1. 包结构分析',
461
+ action: '浏览包目录,分析 __init__.py 内容和 __all__ 定义',
462
+ expectedOutput: '__init__.py 导出策略 + 包层级结构',
463
+ tools: ['file_search 搜索 __init__.py', 'read_file 阅读 __init__.py 文件'],
464
+ },
465
+ {
466
+ phase: '2. 导入风格分析',
467
+ action: '统计相对导入 vs 绝对导入使用比例,分析导入约定',
468
+ expectedOutput: '导入风格统计 + 项目约定',
469
+ tools: ['grep_search 搜索 from . import/from .. import/import xxx'],
470
+ },
471
+ {
472
+ phase: '3. 类型标注与装饰器',
473
+ action: '分析 type hints 覆盖率、Protocol 使用、decorator 模式',
474
+ expectedOutput: '类型标注覆盖率 + 常用装饰器清单',
475
+ tools: ['grep_search 搜索 -> /: str/: int/Protocol/@decorator'],
476
+ },
477
+ {
478
+ phase: '4. 提交',
479
+ action: '包结构、导入约定、类型标注分别提交候选',
480
+ qualityChecklist: [
481
+ 'content 包含具体的 import/type hint 代码示例',
482
+ '引用 __init__.py 和核心模块路径',
483
+ 'coreCode 展示标准的导入和类型标注写法',
484
+ 'doClause 表达导入和类型标注约定',
485
+ ],
486
+ },
487
+ ],
488
+ timeEstimate: '10-15 min',
489
+ commonMistakes: [
490
+ '不要忽略 __all__ 定义 — 它决定了 from pkg import * 的行为',
491
+ '不要假设所有包都有 type hints — 先统计覆盖率再分析',
492
+ '不要遗漏 decorator 模式 — 自定义 decorator 是项目的重要约定',
493
+ ],
494
+ },
495
+
496
+ 'jvm-annotation-scan': {
497
+ steps: [
498
+ {
499
+ phase: '1. DI 注解扫描',
500
+ action: '搜索 @Inject/@Autowired/@Component/@Service/@Repository 等 DI 注解',
501
+ expectedOutput: 'DI 注解使用模式 + 统计',
502
+ tools: ['grep_search 搜索 @Inject/@Autowired/@Component'],
503
+ },
504
+ {
505
+ phase: '2. ORM 注解扫描',
506
+ action: '搜索 @Entity/@Table/@Column/@ManyToOne 等 ORM 注解',
507
+ expectedOutput: 'ORM 映射模式 + 实体关系图',
508
+ tools: ['grep_search 搜索 @Entity/@Table/@Column'],
509
+ },
510
+ {
511
+ phase: '3. API 与自定义注解',
512
+ action: '搜索 @RestController/@RequestMapping/@GetMapping + 自定义注解定义',
513
+ expectedOutput: 'API 路由约定 + 自定义注解清单及用途',
514
+ tools: ['grep_search 搜索 @RestController/@interface (annotation 定义)'],
515
+ },
516
+ {
517
+ phase: '4. 提交',
518
+ action: 'DI、ORM、API、自定义注解分别提交候选',
519
+ qualityChecklist: [
520
+ 'content 包含注解的完整使用示例',
521
+ '引用具体的类文件和配置',
522
+ 'coreCode 展示标准的注解使用骨架',
523
+ 'doClause 表达注解使用的强制规则',
524
+ ],
525
+ },
526
+ ],
527
+ timeEstimate: '10-15 min',
528
+ commonMistakes: [
529
+ '不要只扫描 Spring 注解 — 项目可能使用 Guice/Dagger/Hilt',
530
+ '不要忽略自定义注解 — 它们编码了项目特有的规则',
531
+ '区分 field injection 和 constructor injection — 项目通常有明确偏好',
532
+ ],
533
+ },
534
+
535
+ 'go-module-scan': {
536
+ steps: [
537
+ {
538
+ phase: '1. 模块结构分析',
539
+ action: '阅读 go.mod,浏览 cmd/ 目录和 internal/ 目录',
540
+ expectedOutput: 'go.mod 依赖图 + 入口点枚举 + internal 包边界',
541
+ tools: ['read_file 阅读 go.mod', 'list_dir 浏览 cmd/ 和 internal/'],
542
+ },
543
+ {
544
+ phase: '2. 接口分布分析',
545
+ action: '搜索 interface 定义和实现,分析接口设计模式',
546
+ expectedOutput: '核心接口清单 + 实现关系',
547
+ tools: ['grep_search 搜索 type.*interface'],
548
+ },
549
+ {
550
+ phase: '3. 初始化与构建',
551
+ action: '搜索 init() 函数、build tags,分析初始化链路',
552
+ expectedOutput: 'init() 执行链路 + build tags 约束',
553
+ tools: ['grep_search 搜索 func init()/go:build'],
554
+ },
555
+ {
556
+ phase: '4. 提交',
557
+ action: '模块结构、接口约定、初始化模式分别提交候选',
558
+ qualityChecklist: [
559
+ 'content 包含模块依赖关系和接口设计',
560
+ '引用 go.mod 和核心包路径',
561
+ 'coreCode 展示标准的接口定义和使用',
562
+ 'doClause 表达模块边界和导入约束',
563
+ ],
564
+ },
565
+ ],
566
+ timeEstimate: '10-15 min',
567
+ commonMistakes: [
568
+ '不要忽略 internal/ 包的隔离边界 — 这是 Go 的强制约束',
569
+ '不要只列出依赖不说明用途',
570
+ '注意区分项目自有模块和第三方模块',
571
+ ],
572
+ },
573
+ };
574
+
575
+ // ═══════════════════════════════════════════════════════════
576
+ // Quality Checklist (提交前自检清单)
577
+ // ═══════════════════════════════════════════════════════════
578
+
579
+ export const PRE_SUBMIT_CHECKLIST = {
580
+ MUST: [
581
+ 'content.markdown ≥ 200 字符(含 ## 标题 + 正文 + 代码块 + 来源标注)',
582
+ '引用 ≥ 3 个项目真实文件路径(不得编造文件路径)',
583
+ '至少 1 个 ```代码块``` + 来源标注「(来源: FileName.ext:行号)」',
584
+ 'doClause 英文祈使句 + 以动词开头 + ≤60 tokens + 包含项目特定信息',
585
+ 'dontClause 英文反向约束 + 描述具体的禁止做法',
586
+ 'whenClause 英文触发场景 + 描述具体的适用条件',
587
+ 'trigger 唯一 + @kebab-case 格式',
588
+ 'coreCode 3-8 行可复制纯代码骨架(语法完整、括号配对)',
589
+ 'kind 正确分类: rule=强制约束 | pattern=实现模式 | fact=项目事实',
590
+ '标题和正文禁止出现 "Agent" 字样 — 所有候选必须以项目规范/开发规范视角撰写',
591
+ ],
592
+ SHOULD: [
593
+ '每个候选聚焦单一知识点 — 不要把多种模式合并成一个候选',
594
+ 'content 包含统计数据(数量、占比、频率)',
595
+ 'content 包含 ✅ 正确写法 和 ❌ 禁止写法的对比',
596
+ 'reasoning.whyStandard 解释为什么这是项目标准(含统计证据)',
597
+ 'reasoning.sources 列出 ≥2 个文件路径作为证据来源',
598
+ 'reasoning.confidence ≥ 0.8',
599
+ ],
600
+ FAIL_EXAMPLES: [
601
+ {
602
+ bad: {
603
+ content: '项目使用 Swift 语言开发',
604
+ doClause: 'use swift',
605
+ coreCode: '// swift code',
606
+ },
607
+ why: 'content 太短且无项目特征 — 没有展示任何具体的代码模式或约定; doClause 不是祈使句且无具体规则; coreCode 不是可复制的代码骨架',
608
+ good: {
609
+ content:
610
+ '## BD 前缀单例管理类\n\n项目中所有 Manager 单例类使用 BD 前缀 + sharedInstance 模式...\n\n### 项目选择了什么\n18 个 Manager 类中 16 个使用此模式...\n\n```objc\n@interface BDVideoManager : NSObject\n+ (instancetype)sharedInstance;\n@end\n```\n(来源: BDVideoManager.h:12)',
611
+ doClause:
612
+ 'Use BD prefix and sharedInstance class method for all singleton Manager classes',
613
+ coreCode:
614
+ '+ (instancetype)sharedInstance {\n static id instance;\n static dispatch_once_t onceToken;\n dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; });\n return instance;\n}',
615
+ },
616
+ },
617
+ {
618
+ bad: {
619
+ content: '## 网络请求\n\n项目有网络请求功能。使用了 URLSession。',
620
+ doClause: 'Make network requests using URLSession',
621
+ coreCode: 'URLSession.shared.dataTask(url)',
622
+ },
623
+ why: 'content 虽有标题但只有两句话 — 缺少项目特有的封装/约定/统计; doClause 是 iOS 通用知识非项目特有; coreCode 不是项目封装的调用方式',
624
+ good: {
625
+ content:
626
+ '## BDBaseRequest 网络请求封装\n\n项目所有网络请求必须通过 BDBaseRequest 子类发起...\n\n### 项目选择了什么\n47 个 API 请求全部继承自 BDBaseRequest,使用 BDNetworkManager 调度...\n\n```objc\n@interface BDUserInfoRequest : BDBaseRequest\n- (void)startWithSuccess:(BDRequestSuccess)success failure:(BDRequestFailure)failure;\n@end\n```\n(来源: BDUserInfoRequest.h:8)\n\n### 新代码怎么写\n继承 BDBaseRequest,覆写 requestUrl 和 requestMethod...',
627
+ doClause:
628
+ 'Subclass BDBaseRequest for all network requests and use BDNetworkManager for dispatch',
629
+ coreCode:
630
+ '@interface MyRequest : BDBaseRequest\n- (NSString *)requestUrl { return @"/api/v2/xxx"; }\n- (BDRequestMethod)requestMethod { return BDRequestMethodGET; }\n@end',
631
+ },
632
+ },
633
+ ],
634
+ };
635
+
636
+ // ═══════════════════════════════════════════════════════════
637
+ // SOP 查询辅助函数
638
+ // ═══════════════════════════════════════════════════════════
639
+
640
+ /**
641
+ * 获取维度的 SOP 配置
642
+ * @param {string} dimId — 维度 ID
643
+ * @returns {object|null} — SOP 配置,未定义时返回 null
644
+ */
645
+ export function getDimensionSOP(dimId) {
646
+ return DIMENSION_SOP[dimId] || null;
647
+ }
648
+
649
+ /**
650
+ * 将 SOP 步骤序列化为紧凑文本(用于体积紧张时的降级)
651
+ * @param {object} sop — SOP 配置
652
+ * @returns {string} — 紧凑的文本表示
653
+ */
654
+ export function sopToCompactText(sop) {
655
+ if (!sop?.steps) return '';
656
+ const lines = [];
657
+ for (const step of sop.steps) {
658
+ lines.push(`${step.phase}: ${step.action}`);
659
+ if (step.expectedOutput) {
660
+ lines.push(` → 预期产出: ${step.expectedOutput}`);
661
+ }
662
+ }
663
+ if (sop.commonMistakes?.length > 0) {
664
+ lines.push('\n⚠ 常见错误:');
665
+ for (const m of sop.commonMistakes) {
666
+ lines.push(` - ${m}`);
667
+ }
668
+ }
669
+ return lines.join('\n');
670
+ }
@@ -20,24 +20,79 @@ const logger = Logger.getInstance();
20
20
  /** Skill 分析文本最低字符数 */
21
21
  const MIN_ANALYSIS_LENGTH = 100;
22
22
 
23
- /** 重复率阈值(低于此值视为重复内容) */
24
- const MAX_DUPLICATE_RATIO = 0.3;
23
+ /** 重复率硬拒绝阈值(全局唯一率低于此值 → 尝试去重挽救) */
24
+ const HARD_REJECT_RATIO = 0.10;
25
+
26
+ /** 连续重复块阈值(连续相同行 ≥ 此值 → AI 循环信号) */
27
+ const CONSECUTIVE_DUPE_THRESHOLD = 8;
25
28
 
26
29
  /** 短文本结构化豁免阈值(低于此字符数的文本必须有结构化内容) */
27
30
  const STRUCTURE_CHECK_THRESHOLD = 500;
28
31
 
29
32
  // ── 质量门控 ────────────────────────────────────────────────
30
33
 
34
+ /**
35
+ * 规范化行文本 — 用于去重比较
36
+ * 去除列表标记、编号、代码围栏、引用标记等结构性前缀,
37
+ * 避免 category-scan 等维度中大量结构相似但内容不同的行被误判为重复。
38
+ */
39
+ function normalizeLine(line) {
40
+ return line
41
+ .trim()
42
+ .replace(/^[-*•]\s+/, '') // strip list markers
43
+ .replace(/^\d+\.\s+/, '') // strip numbered list
44
+ .replace(/^[`>]+\s*/, '') // strip code/quote markers
45
+ .replace(/^#{1,3}\s+/, '') // strip Markdown headings
46
+ .replace(/\(来源[::].*?\)/g, '') // strip source annotations
47
+ .replace(/\s+/g, ' ') // collapse whitespace
48
+ .trim();
49
+ }
50
+
51
+ /**
52
+ * 检测最长连续重复块长度 — AI 循环的核心特征
53
+ */
54
+ function maxConsecutiveDuplicates(lines) {
55
+ let max = 0;
56
+ let current = 0;
57
+ for (let i = 1; i < lines.length; i++) {
58
+ if (lines[i] === lines[i - 1] && lines[i].length > 0) {
59
+ current++;
60
+ if (current > max) max = current;
61
+ } else {
62
+ current = 0;
63
+ }
64
+ }
65
+ return max;
66
+ }
67
+
68
+ /**
69
+ * 去除连续重复行 — 将连续 N 行相同内容压缩为 1 行
70
+ */
71
+ function deduplicateConsecutive(text) {
72
+ const lines = text.split('\n');
73
+ const result = [lines[0]];
74
+ for (let i = 1; i < lines.length; i++) {
75
+ if (lines[i].trim() !== lines[i - 1].trim() || lines[i].trim().length === 0) {
76
+ result.push(lines[i]);
77
+ }
78
+ }
79
+ return result.join('\n');
80
+ }
81
+
31
82
  /**
32
83
  * 验证分析文本是否满足 Skill 生成的质量门控
33
84
  *
34
85
  * 门控规则:
35
86
  * 1. 文本长度 ≥ 100 字符
36
- * 2. 重复行比率 < 0.3(防止 AI 循环输出)
87
+ * 2. 重复检测 (两层):
88
+ * a. 连续重复块 ≥ 8 行 → AI 循环信号 → 去重后挽救
89
+ * b. 规范化后全局唯一率 < 0.10 → 极端重复 → 去重后挽救
37
90
  * 3. 短文本(<500 字符)必须包含结构化内容(标题/列表/代码块)
38
91
  *
92
+ * 挽救机制: 当检测到重复时,先去除连续重复行,如果去重后文本 ≥ 100 字符则放行。
93
+ *
39
94
  * @param {string} analysisText — Analyst 或外部 Agent 的分析文本
40
- * @returns {{ pass: boolean, reason: string|null }}
95
+ * @returns {{ pass: boolean, reason: string|null, deduplicatedText?: string }}
41
96
  */
42
97
  function validateSkillQuality(analysisText) {
43
98
  // 1. 文本过短
@@ -48,14 +103,32 @@ function validateSkillQuality(analysisText) {
48
103
  };
49
104
  }
50
105
 
51
- // 2. 重复行检测(AI 陷入循环输出工具提示等)
106
+ // 2. 重复检测 — 规范化后比较,避免结构性前缀导致误判
52
107
  const textLines = analysisText.split('\n').filter((l) => l.trim().length > 0);
53
- const uniqueLines = new Set(textLines.map((l) => l.trim()));
54
- const uniqueRatio = textLines.length > 0 ? uniqueLines.size / textLines.length : 1;
55
- if (textLines.length > 20 && uniqueRatio < MAX_DUPLICATE_RATIO) {
108
+ const normalizedLines = textLines.map(normalizeLine).filter((l) => l.length > 0);
109
+ const uniqueNormalized = new Set(normalizedLines);
110
+ const uniqueRatio = normalizedLines.length > 0 ? uniqueNormalized.size / normalizedLines.length : 1;
111
+
112
+ // 连续重复块检测(AI 循环的核心特征)
113
+ const maxConsDupes = maxConsecutiveDuplicates(normalizedLines);
114
+
115
+ const isRepetitive = (normalizedLines.length > 30 && uniqueRatio < HARD_REJECT_RATIO)
116
+ || maxConsDupes >= CONSECUTIVE_DUPE_THRESHOLD;
117
+
118
+ if (isRepetitive) {
119
+ // ── 挽救: 去除连续重复后重新评估 ──
120
+ const cleaned = deduplicateConsecutive(analysisText);
121
+ if (cleaned.trim().length >= MIN_ANALYSIS_LENGTH) {
122
+ logger.info(
123
+ `[SkillGenerator] Repetition detected (${uniqueNormalized.size}/${normalizedLines.length} unique, ` +
124
+ `ratio ${uniqueRatio.toFixed(2)}, maxConsec ${maxConsDupes}), salvaged via dedup ` +
125
+ `(${analysisText.length} → ${cleaned.length} chars)`
126
+ );
127
+ return { pass: true, reason: null, deduplicatedText: cleaned };
128
+ }
56
129
  return {
57
130
  pass: false,
58
- reason: `repetitive content detected (${uniqueLines.size}/${textLines.length} unique, ratio ${uniqueRatio.toFixed(2)})`,
131
+ reason: `repetitive content detected (${uniqueNormalized.size}/${normalizedLines.length} unique, ratio ${uniqueRatio.toFixed(2)}, maxConsec ${maxConsDupes}) — dedup salvage also too short (${cleaned.trim().length} chars)`,
59
132
  };
60
133
  }
61
134
 
@@ -156,8 +229,11 @@ export async function generateSkill(ctx, dim, analysisText, referencedFiles = []
156
229
  return { success: false, skillName, error: validation.reason };
157
230
  }
158
231
 
232
+ // 1.5. 如果触发了去重挽救,使用清理后的文本
233
+ const effectiveText = validation.deduplicatedText || analysisText;
234
+
159
235
  // 2. 内容构建
160
- const skillContent = buildSkillContent(dim, analysisText, referencedFiles, keyFindings, source);
236
+ const skillContent = buildSkillContent(dim, effectiveText, referencedFiles, keyFindings, source);
161
237
 
162
238
  // 3. 创建 Skill
163
239
  try {
@@ -101,7 +101,7 @@ const _SYSTEM_FRAMEWORKS = new Set([
101
101
  * 3. Xcode osascript 自动插入,失败则文件写入回退
102
102
  * 4. 附加 AutoSnippet 注释后缀
103
103
  *
104
- * @param {import('./FileWatcher.js').FileWatcher} watcher
104
+ * @param {import('../../../service/automation/FileWatcher.js').FileWatcher} watcher
105
105
  * @param {string} fullPath 目标文件绝对路径
106
106
  * @param {string[]} headers 待插入的 import 行数组
107
107
  * @param {object} [opts]
@@ -296,7 +296,7 @@ export async function insertHeaders(watcher, fullPath, headers, opts = {}) {
296
296
  * 7. Jump 到粘贴行 → 选中行内容 → Cmd+V 粘贴替换
297
297
  * 8. 任一步失败 → 降级到纯文件写入
298
298
  *
299
- * @param {import('./FileWatcher.js').FileWatcher} watcher
299
+ * @param {import('../../../service/automation/FileWatcher.js').FileWatcher} watcher
300
300
  */
301
301
  export async function insertCodeToXcode(watcher, fullPath, selected, triggerLine) {
302
302
  const XA = await import('./XcodeAutomation.js');
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CandidateAggregator — 候选条目去重聚合
3
+ *
4
+ * 对候选列表按 title 进行模糊去重,保留最优条目。
5
+ * 被 knowledge handler 的 submitKnowledgeBatch 使用。
6
+ *
7
+ * @module service/candidate/CandidateAggregator
8
+ */
9
+
10
+ import { jaccardSimilarity, tokenizeForSimilarity } from '../../shared/similarity.js';
11
+
12
+ /** title 相似度阈值,超过此值视为重复 */
13
+ const TITLE_SIMILARITY_THRESHOLD = 0.85;
14
+
15
+ /**
16
+ * 对候选条目列表进行去重聚合
17
+ *
18
+ * @param {Array<{title: string, code?: string, [key: string]: any}>} items
19
+ * @param {object} [opts]
20
+ * @param {number} [opts.threshold] 自定义相似度阈值 (0-1)
21
+ * @returns {{ items: Array, duplicates: Array<{item: any, duplicateOf: string}> }}
22
+ */
23
+ export function aggregateCandidates(items, opts = {}) {
24
+ if (!Array.isArray(items) || items.length === 0) {
25
+ return { items: [], duplicates: [] };
26
+ }
27
+
28
+ const threshold = opts.threshold ?? TITLE_SIMILARITY_THRESHOLD;
29
+ const kept = [];
30
+ const duplicates = [];
31
+
32
+ for (const item of items) {
33
+ const titleTokens = tokenizeForSimilarity(item.title || '');
34
+ let isDuplicate = false;
35
+
36
+ for (const existing of kept) {
37
+ const existingTokens = tokenizeForSimilarity(existing.title || '');
38
+ const sim = jaccardSimilarity(titleTokens, existingTokens);
39
+ if (sim >= threshold) {
40
+ duplicates.push({ item, duplicateOf: existing.title });
41
+ isDuplicate = true;
42
+ break;
43
+ }
44
+ }
45
+
46
+ if (!isDuplicate) {
47
+ kept.push(item);
48
+ }
49
+ }
50
+
51
+ return { items: kept, duplicates };
52
+ }
@@ -60,7 +60,7 @@ export const extractRecipes = {
60
60
  // 加载语言参考 Skill(如有),注入到 AI 提取 prompt
61
61
  let skillReference = null;
62
62
  try {
63
- const { loadBootstrapSkills } = await import('../../external/mcp/handlers/bootstrap-internal.js');
63
+ const { loadBootstrapSkills } = await import('../../../external/mcp/handlers/bootstrap-internal.js');
64
64
  const langProfile = ctx.aiProvider._detectLanguageProfile?.(files);
65
65
  const primaryLang = langProfile?.primaryLanguage;
66
66
  if (primaryLang) {
@@ -240,7 +240,7 @@ export const enrichCandidate = {
240
240
  return { error: 'AI provider not available' };
241
241
  }
242
242
  // V3: 使用 MCP handler enrichCandidates 的逻辑
243
- const { enrichCandidates: enrichFn } = await import('../../external/mcp/handlers/candidate.js');
243
+ const { enrichCandidates: enrichFn } = await import('../../../external/mcp/handlers/candidate.js');
244
244
  const result = await enrichFn(ctx, { candidateIds: params.candidateIds });
245
245
  return result?.data || result;
246
246
  },
@@ -273,7 +273,7 @@ export const refineBootstrapCandidates = {
273
273
  return { error: 'AI provider not available' };
274
274
  }
275
275
  // V3: 委托给 bootstrap handler 的 refine 逻辑
276
- const { bootstrapRefine } = await import('../../external/mcp/handlers/bootstrap-internal.js');
276
+ const { bootstrapRefine } = await import('../../../external/mcp/handlers/bootstrap-internal.js');
277
277
  const result = await bootstrapRefine(ctx, {
278
278
  candidateIds: params.candidateIds,
279
279
  userPrompt: params.userPrompt,
@@ -20,7 +20,7 @@
20
20
  /**
21
21
  * 辅助: 安全获取 ProjectGraph 实例
22
22
  * @param {object} ctx
23
- * @returns {import('../../core/ast/ProjectGraph.js').default|null}
23
+ * @returns {import('../../../core/ast/ProjectGraph.js').default|null}
24
24
  */
25
25
  function _getProjectGraph(ctx) {
26
26
  try {
@@ -558,7 +558,7 @@ export const queryCodeGraph = {
558
558
  },
559
559
  handler: async (params, ctx) => {
560
560
  try {
561
- const { CodeEntityGraph } = await import('./../../service/knowledge/CodeEntityGraph.js');
561
+ const { CodeEntityGraph } = await import('../../knowledge/CodeEntityGraph.js');
562
562
  const db = ctx?.container?.get('database');
563
563
  if (!db) {
564
564
  return '代码实体图谱不可用: 数据库未初始化';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autosnippet",
3
- "version": "3.1.5",
3
+ "version": "3.1.6",
4
4
  "description": "Extract code patterns into a knowledge base for AI coding assistants",
5
5
  "type": "module",
6
6
  "main": "lib/bootstrap.js",
@@ -27,8 +27,11 @@ const noBackup = args.includes('--no-backup');
27
27
  const projectRoot = args.find((a) => !a.startsWith('--')) || process.cwd();
28
28
 
29
29
  // ── 动态导入(兼容 ESM) ──
30
- const { parseRecipeMarkdown } = await import('../lib/service/recipe/RecipeFileWriter.js');
31
- const { parseCandidateMarkdown } = await import('../lib/service/candidate/CandidateFileWriter.js');
30
+ // NOTE: 旧的 RecipeFileWriter / CandidateFileWriter 已统一为 KnowledgeFileWriter
31
+ // parseKnowledgeMarkdown 兼容解析旧格式
32
+ const { parseKnowledgeMarkdown } = await import('../lib/service/knowledge/KnowledgeFileWriter.js');
33
+ const parseRecipeMarkdown = parseKnowledgeMarkdown;
34
+ const parseCandidateMarkdown = parseKnowledgeMarkdown;
32
35
  const { KnowledgeEntry } = await import('../lib/domain/knowledge/KnowledgeEntry.js');
33
36
  const { KnowledgeFileWriter } = await import('../lib/service/knowledge/KnowledgeFileWriter.js');
34
37
  const { Lifecycle } = await import('../lib/domain/knowledge/Lifecycle.js');