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.
- package/README.md +12 -12
- package/bin/cli.js +53 -40
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
- package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
- package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +35 -36
- package/lib/cli/KnowledgeSyncService.js +345 -0
- package/lib/cli/SetupService.js +8 -26
- package/lib/cli/UpgradeService.js +28 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +289 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +99 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +128 -0
- package/lib/domain/knowledge/values/Content.js +69 -0
- package/lib/domain/knowledge/values/Quality.js +81 -0
- package/lib/domain/knowledge/values/Reasoning.js +70 -0
- package/lib/domain/knowledge/values/Relations.js +142 -0
- package/lib/domain/knowledge/values/Stats.js +72 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +85 -11
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
- package/lib/external/mcp/handlers/bootstrap.js +116 -11
- package/lib/external/mcp/handlers/browse.js +76 -73
- package/lib/external/mcp/handlers/candidate.js +26 -275
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +267 -0
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +134 -140
- package/lib/http/HttpServer.js +14 -8
- package/lib/http/routes/ai.js +4 -3
- package/lib/http/routes/extract.js +48 -4
- package/lib/http/routes/knowledge.js +246 -0
- package/lib/http/routes/search.js +12 -17
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/injection/ServiceContainer.js +69 -60
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +59 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +5 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -19
- package/lib/service/automation/handlers/DraftHandler.js +1 -1
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +25 -22
- package/lib/service/candidate/SimilarityService.js +2 -2
- package/lib/service/chat/AnalystAgent.js +9 -0
- package/lib/service/chat/CandidateGuardrail.js +22 -11
- package/lib/service/chat/ChatAgent.js +132 -54
- package/lib/service/chat/ContextWindow.js +5 -5
- package/lib/service/chat/HandoffProtocol.js +1 -0
- package/lib/service/chat/ProducerAgent.js +40 -13
- package/lib/service/chat/ReasoningLayer.js +854 -0
- package/lib/service/chat/ReasoningTrace.js +329 -0
- package/lib/service/chat/tools.js +308 -205
- package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
- package/lib/service/cursor/KnowledgeCompressor.js +87 -0
- package/lib/service/cursor/RulesGenerator.js +168 -0
- package/lib/service/cursor/SkillsSyncer.js +268 -0
- package/lib/service/cursor/TokenBudget.js +58 -0
- package/lib/service/cursor/TopicClassifier.js +141 -0
- package/lib/service/guard/GuardCheckEngine.js +99 -10
- package/lib/service/guard/GuardService.js +57 -46
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
- package/lib/service/knowledge/KnowledgeService.js +802 -0
- package/lib/service/recipe/RecipeParser.js +3 -12
- package/lib/service/search/SearchEngine.js +67 -22
- package/lib/service/skills/SignalCollector.js +14 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +15 -48
- package/lib/shared/RecipeReadinessChecker.js +6 -11
- package/package.json +1 -1
- package/scripts/install-cursor-skill.js +0 -6
- package/scripts/migrate-md-to-knowledge.mjs +364 -0
- package/skills/autosnippet-analysis/SKILL.md +15 -7
- package/skills/autosnippet-candidates/SKILL.md +8 -8
- package/skills/autosnippet-coldstart/SKILL.md +8 -4
- package/skills/autosnippet-concepts/SKILL.md +7 -6
- package/skills/autosnippet-create/SKILL.md +13 -13
- package/skills/autosnippet-intent/SKILL.md +3 -2
- package/skills/autosnippet-lifecycle/SKILL.md +5 -5
- package/skills/autosnippet-recipes/SKILL.md +18 -6
- package/templates/constitution.yaml +1 -1
- package/templates/copilot-instructions.md +6 -6
- package/templates/recipes-setup/README.md +3 -3
- package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
- package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
- package/lib/cli/CandidateSyncService.js +0 -261
- package/lib/cli/SyncService.js +0 -356
- package/lib/domain/candidate/Candidate.js +0 -196
- package/lib/domain/candidate/CandidateRepository.js +0 -107
- package/lib/domain/candidate/Reasoning.js +0 -52
- package/lib/domain/recipe/Recipe.js +0 -421
- package/lib/domain/recipe/RecipeRepository.js +0 -54
- package/lib/domain/types/CandidateStatus.js +0 -52
- package/lib/http/routes/candidates.js +0 -559
- package/lib/http/routes/recipes.js +0 -397
- package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
- package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
- package/lib/service/candidate/CandidateAggregator.js +0 -52
- package/lib/service/candidate/CandidateFileWriter.js +0 -383
- package/lib/service/candidate/CandidateService.js +0 -1001
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- 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 =
|
|
35
|
-
'SELECT id FROM
|
|
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 =
|
|
47
|
-
"SELECT id FROM
|
|
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.
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
106
|
-
? `已创建候选「${
|
|
107
|
-
: `已创建 ${
|
|
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
|
-
//
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 { /*
|
|
140
|
-
|
|
141
|
-
|
|
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.
|
|
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
|
-
*
|
|
9
|
-
* @param {string} code
|
|
10
|
-
* @param {string} guardLine
|
|
27
|
+
* 递归收集目录下所有源文件路径
|
|
11
28
|
*/
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
console.log(` ⚠️
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
107
|
-
if (headers.length === 0 && r.
|
|
119
|
+
// V3: headers 是独立 JSON 列(字符串),优先解析
|
|
120
|
+
if (headers.length === 0 && r.headers) {
|
|
108
121
|
try {
|
|
109
|
-
const
|
|
110
|
-
if (Array.isArray(
|
|
111
|
-
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
|
-
//
|
|
116
|
-
|
|
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 ||
|
|
133
|
+
moduleName = content.moduleName || null;
|
|
131
134
|
} catch { /* ignore */ }
|
|
132
135
|
}
|
|
133
136
|
if (!moduleName) {
|
|
134
|
-
moduleName = r.moduleName ||
|
|
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.
|
|
46
|
-
tokenize(recipe.summary || recipe.
|
|
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. 结构验证 —
|
|
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 —
|
|
30
|
+
* @param {object} candidate — submit_knowledge 工具参数
|
|
31
31
|
* @returns {{ valid: boolean, error?: string }}
|
|
32
32
|
*/
|
|
33
33
|
validateStructure(candidate) {
|
|
34
34
|
// 必填字段检查
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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.
|
|
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);
|