autosnippet 2.9.0 → 2.11.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 (115) hide show
  1. package/README.md +12 -12
  2. package/bin/cli.js +53 -40
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
  5. package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
  6. package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +35 -36
  9. package/lib/cli/KnowledgeSyncService.js +345 -0
  10. package/lib/cli/SetupService.js +8 -26
  11. package/lib/cli/UpgradeService.js +28 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +289 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +99 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +128 -0
  19. package/lib/domain/knowledge/values/Content.js +69 -0
  20. package/lib/domain/knowledge/values/Quality.js +81 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +70 -0
  22. package/lib/domain/knowledge/values/Relations.js +142 -0
  23. package/lib/domain/knowledge/values/Stats.js +72 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +85 -11
  26. package/lib/external/mcp/McpServer.js +7 -5
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
  28. package/lib/external/mcp/handlers/bootstrap.js +116 -11
  29. package/lib/external/mcp/handlers/browse.js +76 -73
  30. package/lib/external/mcp/handlers/candidate.js +26 -275
  31. package/lib/external/mcp/handlers/guard.js +2 -0
  32. package/lib/external/mcp/handlers/knowledge.js +267 -0
  33. package/lib/external/mcp/handlers/structure.js +25 -23
  34. package/lib/external/mcp/handlers/system.js +10 -12
  35. package/lib/external/mcp/tools.js +134 -140
  36. package/lib/http/HttpServer.js +14 -8
  37. package/lib/http/routes/ai.js +4 -3
  38. package/lib/http/routes/extract.js +48 -4
  39. package/lib/http/routes/knowledge.js +246 -0
  40. package/lib/http/routes/search.js +12 -17
  41. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  42. package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
  43. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  44. package/lib/injection/ServiceContainer.js +69 -60
  45. package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
  46. package/lib/service/automation/DirectiveDetector.js +2 -3
  47. package/lib/service/automation/FileWatcher.js +59 -28
  48. package/lib/service/automation/XcodeIntegration.js +931 -156
  49. package/lib/service/automation/handlers/AlinkHandler.js +5 -4
  50. package/lib/service/automation/handlers/CreateHandler.js +53 -19
  51. package/lib/service/automation/handlers/DraftHandler.js +1 -1
  52. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  53. package/lib/service/automation/handlers/SearchHandler.js +25 -22
  54. package/lib/service/candidate/SimilarityService.js +2 -2
  55. package/lib/service/chat/AnalystAgent.js +9 -0
  56. package/lib/service/chat/CandidateGuardrail.js +22 -11
  57. package/lib/service/chat/ChatAgent.js +132 -54
  58. package/lib/service/chat/ContextWindow.js +5 -5
  59. package/lib/service/chat/HandoffProtocol.js +1 -0
  60. package/lib/service/chat/ProducerAgent.js +40 -13
  61. package/lib/service/chat/ReasoningLayer.js +854 -0
  62. package/lib/service/chat/ReasoningTrace.js +329 -0
  63. package/lib/service/chat/tools.js +308 -205
  64. package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
  65. package/lib/service/cursor/KnowledgeCompressor.js +87 -0
  66. package/lib/service/cursor/RulesGenerator.js +168 -0
  67. package/lib/service/cursor/SkillsSyncer.js +268 -0
  68. package/lib/service/cursor/TokenBudget.js +58 -0
  69. package/lib/service/cursor/TopicClassifier.js +141 -0
  70. package/lib/service/guard/GuardCheckEngine.js +99 -10
  71. package/lib/service/guard/GuardService.js +57 -46
  72. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  73. package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
  74. package/lib/service/knowledge/KnowledgeService.js +802 -0
  75. package/lib/service/recipe/RecipeParser.js +3 -12
  76. package/lib/service/search/SearchEngine.js +67 -22
  77. package/lib/service/skills/SignalCollector.js +14 -9
  78. package/lib/service/skills/SkillAdvisor.js +13 -11
  79. package/lib/service/snippet/SnippetFactory.js +5 -5
  80. package/lib/service/spm/SpmService.js +15 -48
  81. package/lib/shared/RecipeReadinessChecker.js +6 -11
  82. package/package.json +1 -1
  83. package/scripts/install-cursor-skill.js +0 -6
  84. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  85. package/skills/autosnippet-analysis/SKILL.md +15 -7
  86. package/skills/autosnippet-candidates/SKILL.md +8 -8
  87. package/skills/autosnippet-coldstart/SKILL.md +8 -4
  88. package/skills/autosnippet-concepts/SKILL.md +7 -6
  89. package/skills/autosnippet-create/SKILL.md +13 -13
  90. package/skills/autosnippet-intent/SKILL.md +3 -2
  91. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  92. package/skills/autosnippet-recipes/SKILL.md +18 -6
  93. package/templates/constitution.yaml +1 -1
  94. package/templates/copilot-instructions.md +6 -6
  95. package/templates/recipes-setup/README.md +3 -3
  96. package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
  97. package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
  98. package/lib/cli/CandidateSyncService.js +0 -261
  99. package/lib/cli/SyncService.js +0 -356
  100. package/lib/domain/candidate/Candidate.js +0 -196
  101. package/lib/domain/candidate/CandidateRepository.js +0 -107
  102. package/lib/domain/candidate/Reasoning.js +0 -52
  103. package/lib/domain/recipe/Recipe.js +0 -421
  104. package/lib/domain/recipe/RecipeRepository.js +0 -54
  105. package/lib/domain/types/CandidateStatus.js +0 -52
  106. package/lib/http/routes/candidates.js +0 -559
  107. package/lib/http/routes/recipes.js +0 -397
  108. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  109. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  110. package/lib/service/candidate/CandidateAggregator.js +0 -52
  111. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  112. package/lib/service/candidate/CandidateService.js +0 -1001
  113. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  114. package/lib/service/recipe/RecipeService.js +0 -786
  115. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -30,9 +30,10 @@ export async function handleAlink(alinkLine) {
30
30
 
31
31
  let recipeId = null;
32
32
  if (db) {
33
+ const rawDb = typeof db.getDb === 'function' ? db.getDb() : db;
33
34
  try {
34
- const row = db.prepare(
35
- 'SELECT id FROM recipes WHERE trigger = ? LIMIT 1',
35
+ const row = rawDb.prepare(
36
+ 'SELECT id FROM knowledge_entries WHERE trigger = ? AND lifecycle = \'active\' LIMIT 1',
36
37
  ).get(completionKey);
37
38
  if (row) recipeId = row.id;
38
39
  } catch {
@@ -43,8 +44,8 @@ export async function handleAlink(alinkLine) {
43
44
  if (!recipeId) {
44
45
  try {
45
46
  const escaped = completionKey.replace(/[%_\\]/g, ch => `\\${ch}`);
46
- const row = db.prepare(
47
- "SELECT id FROM recipes WHERE trigger LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\' LIMIT 1",
47
+ const row = rawDb.prepare(
48
+ "SELECT id FROM knowledge_entries WHERE (trigger LIKE ? ESCAPE '\\' OR title LIKE ? ESCAPE '\\') AND lifecycle = 'active' LIMIT 1",
48
49
  ).get(`%${escaped}%`, `%${escaped}%`);
49
50
  if (row) recipeId = row.id;
50
51
  } catch { /* silent */ }
@@ -86,7 +86,7 @@ async function silentCreateCandidate(watcher, text, relativePath) {
86
86
  const normalize = (arr) =>
87
87
  arr.map((r) => ({
88
88
  title: r.title,
89
- summary: r.summary || r.summary_cn || '',
89
+ summary: r.summary || r.description || '',
90
90
  trigger: r.trigger,
91
91
  category: r.category || 'Utility',
92
92
  language: r.language === 'swift' ? 'swift' : 'objc',
@@ -95,16 +95,17 @@ async function silentCreateCandidate(watcher, text, relativePath) {
95
95
  headers: r.headers || [],
96
96
  }));
97
97
 
98
- // 先尝试批量解析
98
+ // 先尝试批量解析(仅对 Recipe Markdown 格式有效)
99
99
  const allRecipes = parser.parseAll(text);
100
- if (allRecipes.length > 0) {
101
- const items = normalize(allRecipes);
100
+ const validRecipes = allRecipes.filter(r => r.title && r.title.trim());
101
+ if (validRecipes.length > 0) {
102
+ const items = normalize(validRecipes);
102
103
  await watcher._resolveHeadersIfNeeded(items[0], relativePath, text);
103
104
  await watcher._appendCandidates(items, 'watch-create');
104
105
  const msg =
105
- allRecipes.length === 1
106
- ? `已创建候选「${allRecipes[0].title}」,请在 Dashboard Candidates 页审核`
107
- : `已创建 ${allRecipes.length} 条候选,请在 Dashboard Candidates 页审核`;
106
+ validRecipes.length === 1
107
+ ? `已创建候选「${validRecipes[0].title}」,请在 Dashboard Candidates 页审核`
108
+ : `已创建 ${validRecipes.length} 条候选,请在 Dashboard Candidates 页审核`;
108
109
  console.log(`✅ [as:create] ${msg}`);
109
110
  watcher._notify(msg);
110
111
  return;
@@ -113,7 +114,7 @@ async function silentCreateCandidate(watcher, text, relativePath) {
113
114
  // 尝试单条解析
114
115
  if (parser.isCompleteRecipe(text)) {
115
116
  const one = parser.parse(text);
116
- if (one) {
117
+ if (one && one.title && one.title.trim()) {
117
118
  const item = normalize([one])[0];
118
119
  await watcher._resolveHeadersIfNeeded(item, relativePath, text);
119
120
  await watcher._appendCandidates([item], 'watch-create');
@@ -123,22 +124,55 @@ async function silentCreateCandidate(watcher, text, relativePath) {
123
124
  }
124
125
  }
125
126
 
126
- // 最后用 AI 摘要(通过 ChatAgent 统一入口)
127
+ // -c 模式:剪贴板内容整体作为一条候选(不拆分)
128
+ // 先用 AI 生成标题和摘要,code 保持剪贴板原文
129
+ const lang = relativePath && /\.swift$/i.test(relativePath) ? 'swift' : 'objc';
130
+ const fileName = relativePath
131
+ ? relativePath.split('/').pop()
132
+ : `clipboard.${lang === 'swift' ? 'swift' : 'm'}`;
133
+
134
+ let title = fileName.replace(/\.\w+$/, '') || 'Clipboard Snippet';
135
+ let summary = '';
136
+ let usageGuide = '';
137
+ let category = 'Utility';
138
+ let headers = [];
139
+ let tags = [];
140
+ let trigger = '';
141
+
142
+ // 尝试用 AI 生成摘要信息(但 code 始终保持原文)
127
143
  try {
128
144
  const { getServiceContainer } = await import('../../../injection/ServiceContainer.js');
129
145
  const container = getServiceContainer();
130
146
  const chatAgent = container.get('chatAgent');
131
- const lang = relativePath && /\.swift$/i.test(relativePath) ? 'swift' : 'objc';
132
- const result = await chatAgent.executeTool('summarize_code', { code: text, language: lang });
133
- if (result && !result.error && result.title && result.code) {
134
- await watcher._appendCandidates([result], 'watch-create');
135
- console.log(`✅ [as:create] 已静默创建候选「${result.title}」`);
136
- watcher._notify(`已创建候选「${result.title}」,请在 Candidates 页审核`);
137
- return;
147
+ const aiResult = await chatAgent.executeTool('summarize_code', { code: text, language: lang });
148
+ if (aiResult && !aiResult.error) {
149
+ title = aiResult.title || title;
150
+ summary = aiResult.summary || '';
151
+ usageGuide = aiResult.usageGuide || '';
152
+ category = aiResult.category || category;
153
+ headers = aiResult.headers || [];
154
+ tags = aiResult.tags || [];
155
+ trigger = aiResult.trigger || '';
138
156
  }
139
- } catch { /* ChatAgent 不可用 */ }
140
-
141
- throw new Error('无法从剪贴板内容解析出候选');
157
+ } catch { /* AI 不可用,使用默认值 */ }
158
+
159
+ const item = {
160
+ title,
161
+ summary,
162
+ description: summary,
163
+ trigger,
164
+ category,
165
+ language: lang,
166
+ code: text, // 剪贴板原文整体
167
+ usageGuide,
168
+ headers,
169
+ tags,
170
+ };
171
+
172
+ await watcher._resolveHeadersIfNeeded(item, relativePath, text);
173
+ await watcher._appendCandidates([item], 'watch-create');
174
+ console.log(`✅ [as:create -c] 已创建候选「${title}」,请在 Candidates 页审核`);
175
+ watcher._notify(`已创建候选「${title}」,请在 Candidates 页审核`);
142
176
  }
143
177
 
144
178
  /**
@@ -18,7 +18,7 @@ export async function handleDraft(watcher, fullPath, relativePath, content) {
18
18
  const normalize = (arr) =>
19
19
  arr.map((r) => ({
20
20
  title: r.title,
21
- summary: r.summary || r.summary_cn || '',
21
+ summary: r.summary || r.description || '',
22
22
  trigger: r.trigger,
23
23
  category: r.category || 'Utility',
24
24
  language: r.language === 'swift' ? 'swift' : 'objc',
@@ -1,42 +1,183 @@
1
1
  /**
2
2
  * GuardHandler — 处理 // as:a (audit/guard/lint) 指令
3
+ *
4
+ * 用法:
5
+ * // as:a — 检查当前文件 (scope=file)
6
+ * // as:a file — 同上,显式 file scope
7
+ * // as:a target — 检查当前文件所在目录树 (scope=target)
8
+ * // as:a project — 检查整个项目所有源文件 (scope=project)
9
+ * // as:a <keyword> — 检查当前文件 + 搜索相关规范
3
10
  */
4
11
 
5
- import { basename } from 'node:path';
12
+ import { basename, join, extname, dirname } from 'node:path';
13
+ import { readFile } from 'node:fs/promises';
14
+
15
+ /** 已知的 scope 关键词 */
16
+ const SCOPE_KEYWORDS = new Set(['file', 'target', 'project', 'all']);
17
+
18
+ /** 支持审计的源文件扩展名 */
19
+ const SOURCE_EXTS = new Set([
20
+ '.m', '.mm', '.h', '.swift',
21
+ '.c', '.cpp', '.cc', '.cxx', '.hpp',
22
+ '.js', '.ts', '.jsx', '.tsx',
23
+ '.java', '.kt', '.py', '.rb', '.go', '.rs',
24
+ ]);
6
25
 
7
26
  /**
8
- * @param {string} fullPath
9
- * @param {string} code
10
- * @param {string} guardLine
27
+ * 递归收集目录下所有源文件路径
11
28
  */
12
- export async function handleGuard(fullPath, code, guardLine) {
29
+ async function collectSourceFiles(dir) {
30
+ const { readdir } = await import('node:fs/promises');
31
+ const files = [];
32
+
33
+ // 跳过的目录
34
+ const SKIP_DIRS = new Set([
35
+ 'node_modules', '.git', 'build', 'DerivedData',
36
+ 'Pods', '.build', 'vendor', 'dist', '.next',
37
+ 'Carthage', 'xcuserdata', '__pycache__',
38
+ ]);
39
+
40
+ async function walk(currentDir) {
41
+ let entries;
42
+ try {
43
+ entries = await readdir(currentDir, { withFileTypes: true });
44
+ } catch {
45
+ return; // 权限不足等情况跳过
46
+ }
47
+ for (const entry of entries) {
48
+ if (entry.name.startsWith('.') && entry.name !== '.') continue;
49
+ const fullPath = join(currentDir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ if (!SKIP_DIRS.has(entry.name)) {
52
+ await walk(fullPath);
53
+ }
54
+ } else if (entry.isFile() && SOURCE_EXTS.has(extname(entry.name).toLowerCase())) {
55
+ files.push(fullPath);
56
+ }
57
+ }
58
+ }
59
+
60
+ await walk(dir);
61
+ return files;
62
+ }
63
+
64
+ /**
65
+ * @param {import('../FileWatcher').default} watcher FileWatcher 实例
66
+ * @param {string} fullPath 当前文件绝对路径
67
+ * @param {string} code 当前文件内容
68
+ * @param {string} guardLine 触发行原文
69
+ */
70
+ export async function handleGuard(watcher, fullPath, code, guardLine) {
13
71
  const rest = guardLine.replace(/^\/\/\s*as:(?:audit|a|lint|l|guard|g)\s*/, '').trim();
14
- console.log(`\n🛡️ [Guard] 正在检查文件: ${basename(fullPath)}`);
72
+ const scopeArg = rest.toLowerCase();
73
+ const isScope = SCOPE_KEYWORDS.has(scopeArg);
74
+ // 确定 scope:无参数或 'file' → file;'target' → target;'project'/'all' → project
75
+ const scope = !rest || scopeArg === 'file' ? 'file'
76
+ : scopeArg === 'target' ? 'target'
77
+ : (scopeArg === 'project' || scopeArg === 'all') ? 'project'
78
+ : 'file'; // 非 scope 关键词回退到 file
15
79
 
16
80
  try {
17
81
  const { detectLanguage } = await import('../../guard/GuardCheckEngine.js');
18
82
  const { ServiceContainer } = await import('../../../injection/ServiceContainer.js');
19
83
  const container = ServiceContainer.getInstance();
20
84
  const engine = container.get('guardCheckEngine');
21
- const language = detectLanguage(fullPath);
22
- const violations = engine.checkCode(code, language);
23
85
 
24
- if (violations.length === 0) {
25
- console.log(` ✅ 无违规`);
26
- } else {
27
- const errors = violations.filter((v) => v.severity === 'error');
28
- const warnings = violations.filter((v) => v.severity === 'warning');
29
- console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
30
- for (const v of errors) {
31
- console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
86
+ /* ── 多文件审计 (target / project) ── */
87
+ if (scope === 'project' || scope === 'target') {
88
+ let scanRoot;
89
+ if (scope === 'project') {
90
+ scanRoot = watcher?.projectRoot;
91
+ } else {
92
+ // target: 扫描当前文件所在目录树
93
+ scanRoot = dirname(fullPath);
94
+ }
95
+
96
+ if (!scanRoot) {
97
+ console.warn(' ⚠️ 无法确定扫描根目录,回退到单文件检查');
98
+ return _auditSingleFile(engine, fullPath, code, detectLanguage, 'file');
99
+ }
100
+
101
+ const scopeLabel = scope === 'project' ? '整个项目' : '当前目录';
102
+ console.log(`\n🛡️ [Guard] 正在扫描${scopeLabel}: ${scanRoot}`);
103
+ const sourcePaths = await collectSourceFiles(scanRoot);
104
+ console.log(` 📁 找到 ${sourcePaths.length} 个源文件`);
105
+
106
+ if (sourcePaths.length === 0) {
107
+ console.log(' ✅ 未找到源文件');
108
+ return;
109
+ }
110
+
111
+ // 读取所有文件内容
112
+ const fileEntries = [];
113
+ let readErrors = 0;
114
+ for (const p of sourcePaths) {
115
+ try {
116
+ const content = await readFile(p, 'utf-8');
117
+ fileEntries.push({ path: p, content });
118
+ } catch {
119
+ readErrors++;
120
+ }
32
121
  }
33
- for (const v of warnings.slice(0, 5)) {
34
- console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
122
+ if (readErrors > 0) {
123
+ console.log(` ⚠️ ${readErrors} 个文件无法读取,已跳过`);
35
124
  }
125
+
126
+ // 批量审计(传递 scope 以启用对应维度规则)
127
+ const report = engine.auditFiles(fileEntries, { scope });
128
+ const { summary } = report;
129
+
130
+ if (summary.totalViolations === 0) {
131
+ console.log(` ✅ ${summary.filesChecked} 个文件全部通过,无违规`);
132
+ } else {
133
+ console.log(` 🛡️ 扫描 ${summary.filesChecked} 个文件:`);
134
+ console.log(` ${summary.totalErrors} errors, ${summary.totalViolations - summary.totalErrors} warnings`);
135
+ console.log(` ${summary.filesWithViolations} 个文件存在问题\n`);
136
+
137
+ // 按文件输出详情(限制输出前 10 个有问题的文件)
138
+ const filesWithIssues = report.files.filter(f => f.summary.total > 0);
139
+ for (const file of filesWithIssues.slice(0, 10)) {
140
+ const rel = file.filePath.replace(scanRoot + '/', '');
141
+ console.log(` 📄 ${rel} (${file.summary.errors}E / ${file.summary.warnings}W)`);
142
+ const errors = file.violations.filter(v => v.severity === 'error');
143
+ const warnings = file.violations.filter(v => v.severity === 'warning');
144
+ for (const v of errors.slice(0, 5)) {
145
+ console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
146
+ }
147
+ if (errors.length > 5) console.log(` ... 还有 ${errors.length - 5} 个 errors`);
148
+ for (const v of warnings.slice(0, 3)) {
149
+ console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
150
+ }
151
+ if (warnings.length > 3) console.log(` ... 还有 ${warnings.length - 3} 个 warnings`);
152
+ }
153
+ if (filesWithIssues.length > 10) {
154
+ console.log(`\n ... 还有 ${filesWithIssues.length - 10} 个文件有问题,已省略`);
155
+ }
156
+
157
+ // 跨文件问题汇总
158
+ if (report.crossFileViolations?.length > 0) {
159
+ console.log(`\n 🔗 跨文件问题 (${report.crossFileViolations.length}):`);
160
+ for (const v of report.crossFileViolations.slice(0, 10)) {
161
+ console.log(` ⚠️ [${v.ruleId}] ${v.message}`);
162
+ if (v.locations) {
163
+ for (const loc of v.locations.slice(0, 5)) {
164
+ const relLoc = loc.filePath.replace(scanRoot + '/', '');
165
+ console.log(` 📄 ${relLoc}:L${loc.line}`);
166
+ }
167
+ if (v.locations.length > 5) console.log(` ... 还有 ${v.locations.length - 5} 处`);
168
+ }
169
+ }
170
+ }
171
+ }
172
+ return;
36
173
  }
37
174
 
38
- // 如果有关键词,也做语义搜索
39
- if (rest) {
175
+ /* ── 单文件审计 (file scope) ── */
176
+ console.log(`\n🛡️ [Guard] 正在检查文件: ${basename(fullPath)}`);
177
+ _auditSingleFile(engine, fullPath, code, detectLanguage, scope);
178
+
179
+ // 如果有非 scope 关键词,也做语义搜索
180
+ if (rest && !isScope) {
40
181
  try {
41
182
  const searchEngine = container.get('searchEngine');
42
183
  const results = await searchEngine.search(rest, { limit: 3, mode: 'keyword' });
@@ -54,3 +195,25 @@ export async function handleGuard(fullPath, code, guardLine) {
54
195
  console.warn(` ⚠️ Guard 检查失败: ${err.message}`);
55
196
  }
56
197
  }
198
+
199
+ /**
200
+ * 检查单个文件并打印结果
201
+ */
202
+ function _auditSingleFile(engine, fullPath, code, detectLanguage, scope = 'file') {
203
+ const language = detectLanguage(fullPath);
204
+ const violations = engine.checkCode(code, language, { scope });
205
+
206
+ if (violations.length === 0) {
207
+ console.log(` ✅ 无违规`);
208
+ } else {
209
+ const errors = violations.filter((v) => v.severity === 'error');
210
+ const warnings = violations.filter((v) => v.severity === 'warning');
211
+ console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
212
+ for (const v of errors) {
213
+ console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
214
+ }
215
+ for (const v of warnings.slice(0, 5)) {
216
+ console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
217
+ }
218
+ }
219
+ }
@@ -26,22 +26,35 @@ export async function handleSearch(watcher, fullPath, relativePath, searchLine)
26
26
  const container = ServiceContainer.getInstance();
27
27
  const searchEngine = container.get('searchEngine');
28
28
 
29
+ // 诊断:输出索引状态
30
+ const stats = searchEngine.getStats();
31
+ if (stats.totalDocuments === 0) {
32
+ console.log(` ⚠️ 知识库为空(索引 0 条记录),请先通过 asd setup / Dashboard 添加知识条目`);
33
+ } else {
34
+ console.log(` 📊 索引 ${stats.totalDocuments} 条知识`);
35
+ }
36
+
37
+ // BM25 → keyword 逐级降级:空结果也触发降级(中文分词不足时 BM25 可能零命中)
29
38
  try {
30
39
  results = await searchEngine.search(query, { limit: 10, mode: 'bm25' });
40
+ if (!results || (results.items || []).length === 0) {
41
+ results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
42
+ }
31
43
  } catch {
32
44
  results = await searchEngine.search(query, { limit: 10, mode: 'keyword' });
33
45
  }
34
46
  } catch (err) {
35
47
  console.warn(` ⚠️ 搜索失败: ${err.message}`);
36
- watcher._openDashboard(`/?action=search&q=${encodeURIComponent(query)}`);
48
+ console.log(` ℹ️ 未找到「${query}」的相关结果`);
49
+ watcher._notify(`搜索「${query}」失败: ${err.message}`);
37
50
  return;
38
51
  }
39
52
 
40
53
  const items = normalizeSearchResults(results);
41
54
 
42
55
  if (items.length === 0) {
43
- console.log(` ℹ️ 未找到相关结果`);
44
- watcher._openDashboard(`/?action=search&q=${encodeURIComponent(query)}&path=${encodeURIComponent(relativePath)}`);
56
+ console.log(` ℹ️ 未找到「${query}」的相关结果`);
57
+ watcher._notify(`未找到「${query}」的相关结果`);
45
58
  return;
46
59
  }
47
60
 
@@ -103,35 +116,25 @@ export function normalizeSearchResults(results) {
103
116
  }
104
117
  } catch { /* ignore */ }
105
118
  }
106
- // headers 优先从 dimensions_json 取(V2 标准存储位置)
107
- if (headers.length === 0 && r.dimensions_json) {
119
+ // V3: headers 是独立 JSON 列(字符串),优先解析
120
+ if (headers.length === 0 && r.headers) {
108
121
  try {
109
- const dims = typeof r.dimensions_json === 'string' ? JSON.parse(r.dimensions_json) : r.dimensions_json;
110
- if (Array.isArray(dims.headers) && dims.headers.length > 0) {
111
- headers = dims.headers;
122
+ const parsed = typeof r.headers === 'string' ? JSON.parse(r.headers) : r.headers;
123
+ if (Array.isArray(parsed) && parsed.length > 0) {
124
+ headers = parsed;
112
125
  }
113
126
  } catch { /* ignore */ }
114
127
  }
115
- // 兜底:顶层 r.headers
116
- if (headers.length === 0 && Array.isArray(r.headers) && r.headers.length > 0) {
117
- headers = r.headers;
118
- }
119
- // moduleName: 从 dimensions_json 或 content_json 或顶层提取
120
- let moduleName = null;
121
- if (r.dimensions_json) {
122
- try {
123
- const dims = typeof r.dimensions_json === 'string' ? JSON.parse(r.dimensions_json) : r.dimensions_json;
124
- moduleName = dims.moduleName || dims.module_name || null;
125
- } catch { /* ignore */ }
126
- }
128
+ // moduleName: 优先从独立列取
129
+ let moduleName = r.moduleName || null;
127
130
  if (!moduleName && r.content_json) {
128
131
  try {
129
132
  const content = typeof r.content_json === 'string' ? JSON.parse(r.content_json) : r.content_json;
130
- moduleName = content.moduleName || content.module_name || null;
133
+ moduleName = content.moduleName || null;
131
134
  } catch { /* ignore */ }
132
135
  }
133
136
  if (!moduleName) {
134
- moduleName = r.moduleName || r.module_name || null;
137
+ moduleName = r.moduleName || null;
135
138
  }
136
139
 
137
140
  return {
@@ -42,8 +42,8 @@ function jaccard(a, b) {
42
42
  function computeSimilarity(candidate, recipe) {
43
43
  const titleSim = jaccard(tokenize(candidate.title), tokenize(recipe.title));
44
44
  const summarySim = jaccard(
45
- tokenize(candidate.summary || candidate.summary_cn),
46
- tokenize(recipe.summary || recipe.summary_cn),
45
+ tokenize(candidate.summary || candidate.description),
46
+ tokenize(recipe.summary || recipe.description),
47
47
  );
48
48
  const codeSim = jaccard(tokenize(candidate.code, 3), tokenize(recipe.code, 3));
49
49
  // 加权: title 30%, summary 30%, code 40%
@@ -221,6 +221,15 @@ export class AnalystAgent {
221
221
  // 构建 AnalysisReport
222
222
  const report = buildAnalysisReport(result, dimId, this.#projectGraph);
223
223
 
224
+ // 附加推理链数据(如果 ChatAgent 返回了 ReasoningTrace)
225
+ if (result.reasoningTrace) {
226
+ report.reasoningStats = result.reasoningTrace.getStats();
227
+ report.thoughts = result.reasoningTrace.getThoughts();
228
+ }
229
+ if (result.reasoningQuality) {
230
+ report.reasoningQuality = result.reasoningQuality;
231
+ }
232
+
224
233
  // 质量门控 — 传入 outputType 以调整门槛
225
234
  const gate = analysisQualityGate(report, { outputType: dimConfig.outputType || 'analysis' });
226
235
  if (gate.pass) {
@@ -2,7 +2,7 @@
2
2
  * CandidateGuardrail.js — Producer 产出的候选验证链
3
3
  *
4
4
  * 三层验证:
5
- * 1. 结构验证 — 必填字段、内容长度、knowledgeType 约束
5
+ * 1. 结构验证 — 必填字段、内容长度、交付字段非空
6
6
  * 2. 去重验证 — 标题不重复
7
7
  * 3. 质量启发式 — 包含代码引用、项目特定内容
8
8
  *
@@ -27,22 +27,33 @@ export class CandidateGuardrail {
27
27
 
28
28
  /**
29
29
  * 验证候选结构
30
- * @param {object} candidate — submit_candidate 工具参数
30
+ * @param {object} candidate — submit_knowledge 工具参数
31
31
  * @returns {{ valid: boolean, error?: string }}
32
32
  */
33
33
  validateStructure(candidate) {
34
34
  // 必填字段检查
35
- const required = ['title', 'code'];
36
- for (const field of required) {
37
- if (!candidate[field] || String(candidate[field]).trim().length === 0) {
38
- return { valid: false, error: `缺少必填字段: ${field}` };
39
- }
35
+ if (!candidate.title || String(candidate.title).trim().length === 0) {
36
+ return { valid: false, error: '缺少必填字段: title' };
37
+ }
38
+ const markdown = candidate.content?.markdown || '';
39
+ if (!markdown || String(markdown).trim().length === 0) {
40
+ return { valid: false, error: '缺少必填字段: content.markdown' };
40
41
  }
41
42
 
42
43
  // 内容长度检查 — 「项目特写」需要足够的代码和描述
43
- const content = candidate.code || '';
44
- if (content.length < 200) {
45
- return { valid: false, error: `内容过短 (${content.length} 字符, 最少 200)。请包含代码片段和项目上下文描述,而非一句话概括。` };
44
+ if (markdown.length < 200) {
45
+ return { valid: false, error: `内容过短 (${markdown.length} 字符, 最少 200)。请包含代码片段和项目上下文描述,而非一句话概括。` };
46
+ }
47
+
48
+ // Cursor 交付字段必填检查
49
+ if (!candidate.trigger || String(candidate.trigger).trim().length === 0) {
50
+ return { valid: false, error: '缺少必填字段: trigger — 需要 @kebab-case 唯一标识符' };
51
+ }
52
+ if (!candidate.kind || !['rule', 'pattern', 'fact'].includes(candidate.kind)) {
53
+ return { valid: false, error: '缺少或无效字段: kind — 取值 rule/pattern/fact' };
54
+ }
55
+ if (!candidate.doClause || String(candidate.doClause).trim().length === 0) {
56
+ return { valid: false, error: '缺少必填字段: doClause — 需要英文祈使句正向指令' };
46
57
  }
47
58
 
48
59
  // knowledgeType 约束 — 自动修正为允许列表中第一个类型
@@ -75,7 +86,7 @@ export class CandidateGuardrail {
75
86
  * @returns {{ valid: boolean, error?: string, warning?: string }}
76
87
  */
77
88
  validateQuality(candidate) {
78
- const content = candidate.code || '';
89
+ const content = candidate.content?.markdown || '';
79
90
 
80
91
  // 检查是否包含代码引用或文件路径
81
92
  const hasCodeBlock = /```[\s\S]*?```/.test(content) || /\.(m|h|swift|js|ts)(:\d+)?/.test(content);