autosnippet 2.8.3 → 2.10.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 +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- 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/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- 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 +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- 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 +16 -4
- 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-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- 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 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- package/lib/service/recipe/RecipeStatsTracker.js +0 -148
|
@@ -5,14 +5,14 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>AutoSnippet Dashboard</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DdmQMrJJ.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/vendor-BotF760a.js">
|
|
11
11
|
<link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
|
|
12
|
-
<link rel="modulepreload" crossorigin href="/assets/icons-
|
|
12
|
+
<link rel="modulepreload" crossorigin href="/assets/icons-BkT3XrKf.js">
|
|
13
13
|
<link rel="modulepreload" crossorigin href="/assets/syntax-highlighter-CVLHn9O5.js">
|
|
14
14
|
<link rel="modulepreload" crossorigin href="/assets/react-markdown-BA6FB2NP.js">
|
|
15
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BsB7DzW4.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/lib/cli/AiScanService.js
CHANGED
|
@@ -56,7 +56,7 @@ export class AiScanService {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
report.files = files.length;
|
|
59
|
-
const
|
|
59
|
+
const knowledgeService = this.container.get('knowledgeService');
|
|
60
60
|
|
|
61
61
|
// 3. 按文件调用 AI 提取
|
|
62
62
|
for (const file of files) {
|
|
@@ -95,29 +95,31 @@ export class AiScanService {
|
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
await
|
|
99
|
-
|
|
98
|
+
await knowledgeService.create({
|
|
99
|
+
content: {
|
|
100
|
+
pattern: recipe.code,
|
|
101
|
+
},
|
|
100
102
|
language: recipe.language || this._inferLanguage(file.name),
|
|
101
103
|
category: recipe.category || 'ai-scan',
|
|
102
104
|
source: 'ai-scan',
|
|
105
|
+
knowledge_type: recipe.knowledgeType || 'code-pattern',
|
|
106
|
+
title: recipe.title || `[AI Scan] ${file.name}`,
|
|
107
|
+
summary_cn: recipe.summary_cn || recipe.summary_en || '',
|
|
108
|
+
tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
|
|
109
|
+
scope: 'project-specific',
|
|
103
110
|
reasoning: {
|
|
104
|
-
|
|
111
|
+
why_standard: recipe.summary_cn || recipe.summary_en || recipe.title || '',
|
|
105
112
|
sources: [file.relativePath || file.name],
|
|
106
113
|
confidence: 0.7,
|
|
107
|
-
|
|
114
|
+
quality_signals: { origin: 'ai-scan', completeness: 'full' },
|
|
108
115
|
},
|
|
109
116
|
metadata: {
|
|
110
|
-
title: recipe.title || `[AI Scan] ${file.name}`,
|
|
111
|
-
description: recipe.summary_cn || recipe.summary_en || '',
|
|
112
|
-
knowledgeType: recipe.knowledgeType || 'code-pattern',
|
|
113
|
-
tags: [...(recipe.tags || []), 'ai-scan', file.targetName],
|
|
114
117
|
trigger: recipe.trigger || '',
|
|
115
|
-
scope: 'project-specific',
|
|
116
118
|
usageGuideCn: recipe.usageGuide_cn || '',
|
|
117
119
|
usageGuideEn: recipe.usageGuide_en || '',
|
|
118
120
|
headers: recipe.headers || [],
|
|
119
121
|
},
|
|
120
|
-
}
|
|
122
|
+
});
|
|
121
123
|
|
|
122
124
|
report.candidates++;
|
|
123
125
|
} catch (err) {
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeSyncService — 将 .md 文件增量同步到 SQLite DB(knowledge_entries 表)
|
|
3
|
+
*
|
|
4
|
+
* 统一替代 SyncService (Recipe) + CandidateSyncService。
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:
|
|
7
|
+
* - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
|
|
8
|
+
* - 通过 _content_hash 检测手写/手改 .md → 进入违规统计(audit_logs)
|
|
9
|
+
* - 孤儿 Entry(DB 有但 .md 不存在)→ 自动标记 deprecated
|
|
10
|
+
* - 同时扫描 AutoSnippet/candidates/ 和 AutoSnippet/recipes/ 两个目录
|
|
11
|
+
*
|
|
12
|
+
* 使用方式:
|
|
13
|
+
* - CLI: `asd sync` 委托调用
|
|
14
|
+
* - 内部: SetupService.stepDatabase() 委托调用(skipViolations = true)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
20
|
+
import { RECIPES_DIR, CANDIDATES_DIR } from '../infrastructure/config/Defaults.js';
|
|
21
|
+
import { parseKnowledgeMarkdown, computeKnowledgeHash } from '../service/knowledge/KnowledgeFileWriter.js';
|
|
22
|
+
import { KnowledgeEntry } from '../domain/knowledge/KnowledgeEntry.js';
|
|
23
|
+
import Logger from '../infrastructure/logging/Logger.js';
|
|
24
|
+
|
|
25
|
+
export class KnowledgeSyncService {
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} projectRoot
|
|
28
|
+
*/
|
|
29
|
+
constructor(projectRoot) {
|
|
30
|
+
this.projectRoot = projectRoot;
|
|
31
|
+
this.recipesDir = path.join(projectRoot, RECIPES_DIR);
|
|
32
|
+
this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
|
|
33
|
+
this.logger = Logger.getInstance();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 执行增量同步:.md → DB(knowledge_entries 表)
|
|
38
|
+
*
|
|
39
|
+
* 同时扫描 candidates/ 和 recipes/ 两个目录。
|
|
40
|
+
*
|
|
41
|
+
* @param {import('better-sqlite3').Database} db better-sqlite3 原始句柄
|
|
42
|
+
* @param {Object} [opts={}]
|
|
43
|
+
* @param {boolean} [opts.dryRun=false] 只报告不写入
|
|
44
|
+
* @param {boolean} [opts.force=false] 忽略 hash,强制覆盖
|
|
45
|
+
* @param {boolean} [opts.skipViolations=false] 跳过违规记录(setup 场景)
|
|
46
|
+
* @returns {{ synced: number, created: number, updated: number, violations: string[], orphaned: string[], skipped: number }}
|
|
47
|
+
*/
|
|
48
|
+
sync(db, opts = {}) {
|
|
49
|
+
const { dryRun = false, force = false, skipViolations = false } = opts;
|
|
50
|
+
|
|
51
|
+
const report = {
|
|
52
|
+
synced: 0,
|
|
53
|
+
created: 0,
|
|
54
|
+
updated: 0,
|
|
55
|
+
violations: [], // 手动编辑的文件列表
|
|
56
|
+
orphaned: [], // DB 有但 .md 不存在
|
|
57
|
+
skipped: 0,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// ── 1. 收集 .md 文件(两个目录) ──
|
|
61
|
+
const mdFiles = [
|
|
62
|
+
...this._collectMdFiles(this.candidatesDir, CANDIDATES_DIR),
|
|
63
|
+
...this._collectMdFiles(this.recipesDir, RECIPES_DIR),
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
if (mdFiles.length === 0) {
|
|
67
|
+
this.logger.info('KnowledgeSyncService: no .md files found');
|
|
68
|
+
return report;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── 2. 准备 upsert 语句 ──
|
|
72
|
+
const upsertStmt = dryRun ? null : this._prepareUpsert(db);
|
|
73
|
+
const auditStmt = (dryRun || skipViolations) ? null : this._prepareAuditInsert(db);
|
|
74
|
+
|
|
75
|
+
// ── 3. 逐文件同步 ──
|
|
76
|
+
const syncedIds = new Set();
|
|
77
|
+
|
|
78
|
+
for (const { absPath, relPath } of mdFiles) {
|
|
79
|
+
try {
|
|
80
|
+
const content = fs.readFileSync(absPath, 'utf8');
|
|
81
|
+
const parsed = parseKnowledgeMarkdown(content, relPath);
|
|
82
|
+
|
|
83
|
+
if (!parsed.id) {
|
|
84
|
+
this.logger.warn(`KnowledgeSyncService: skip file without id — ${relPath}`);
|
|
85
|
+
report.skipped++;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
syncedIds.add(parsed.id);
|
|
90
|
+
|
|
91
|
+
// ── 检测手动编辑 ──
|
|
92
|
+
const actualHash = computeKnowledgeHash(content);
|
|
93
|
+
const storedHash = parsed.content_hash;
|
|
94
|
+
const isManualEdit = storedHash && storedHash !== actualHash && !force;
|
|
95
|
+
|
|
96
|
+
if (isManualEdit) {
|
|
97
|
+
report.violations.push(relPath);
|
|
98
|
+
if (auditStmt) {
|
|
99
|
+
this._logViolation(auditStmt, parsed.id, relPath, storedHash, actualHash);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── upsert ──
|
|
104
|
+
if (!dryRun) {
|
|
105
|
+
const existed = this._entryExists(db, parsed.id);
|
|
106
|
+
const row = this._buildDbRow(parsed, relPath, content);
|
|
107
|
+
upsertStmt.run(...Object.values(row));
|
|
108
|
+
|
|
109
|
+
if (existed) {
|
|
110
|
+
report.updated++;
|
|
111
|
+
} else {
|
|
112
|
+
report.created++;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
report.synced++;
|
|
117
|
+
} catch (err) {
|
|
118
|
+
this.logger.error(`KnowledgeSyncService: failed to sync ${relPath}`, { error: err.message });
|
|
119
|
+
report.skipped++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ── 4. 检测孤儿 ──
|
|
124
|
+
report.orphaned = this._detectOrphans(db, syncedIds, dryRun);
|
|
125
|
+
|
|
126
|
+
this.logger.info('KnowledgeSyncService: sync complete', {
|
|
127
|
+
synced: report.synced,
|
|
128
|
+
created: report.created,
|
|
129
|
+
updated: report.updated,
|
|
130
|
+
violations: report.violations.length,
|
|
131
|
+
orphaned: report.orphaned.length,
|
|
132
|
+
skipped: report.skipped,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
return report;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ═══ 文件收集 ═══════════════════════════════════════════ */
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 递归收集指定目录下所有 .md 文件(跳过 _ 前缀模板)
|
|
142
|
+
* @param {string} dir 绝对目录路径
|
|
143
|
+
* @param {string} prefix 相对路径前缀 (e.g. 'AutoSnippet/candidates')
|
|
144
|
+
* @returns {{ absPath: string, relPath: string }[]}
|
|
145
|
+
*/
|
|
146
|
+
_collectMdFiles(dir, prefix) {
|
|
147
|
+
if (!fs.existsSync(dir)) return [];
|
|
148
|
+
|
|
149
|
+
const results = [];
|
|
150
|
+
const walk = (curDir, base) => {
|
|
151
|
+
for (const entry of fs.readdirSync(curDir, { withFileTypes: true })) {
|
|
152
|
+
const full = path.join(curDir, entry.name);
|
|
153
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
154
|
+
|
|
155
|
+
if (entry.isDirectory()) {
|
|
156
|
+
walk(full, rel);
|
|
157
|
+
} else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
|
|
158
|
+
results.push({
|
|
159
|
+
absPath: full,
|
|
160
|
+
relPath: `${prefix}/${rel}`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
walk(dir, '');
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/* ═══ DB 操作 ═══════════════════════════════════════════ */
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 从 parseKnowledgeMarkdown 的结果构建 DB row
|
|
173
|
+
* wire format → DB 列映射(与 KnowledgeRepository.impl 对齐)
|
|
174
|
+
*/
|
|
175
|
+
_buildDbRow(parsed, relPath, rawContent) {
|
|
176
|
+
const now = Math.floor(Date.now() / 1000);
|
|
177
|
+
|
|
178
|
+
// 内容 hash
|
|
179
|
+
const contentHash = computeKnowledgeHash(rawContent);
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
id: parsed.id,
|
|
183
|
+
title: parsed.title || '',
|
|
184
|
+
trigger_key: parsed.trigger || '',
|
|
185
|
+
description: parsed.description || '',
|
|
186
|
+
lifecycle: parsed.lifecycle || 'pending',
|
|
187
|
+
lifecycle_history: JSON.stringify(parsed.lifecycle_history || []),
|
|
188
|
+
probation: (parsed.auto_approvable ?? parsed.probation) ? 1 : 0,
|
|
189
|
+
language: parsed.language || 'swift',
|
|
190
|
+
category: parsed.category || 'general',
|
|
191
|
+
kind: parsed.kind || 'pattern',
|
|
192
|
+
knowledge_type: parsed.knowledge_type || 'code-pattern',
|
|
193
|
+
complexity: parsed.complexity || 'intermediate',
|
|
194
|
+
scope: parsed.scope || 'universal',
|
|
195
|
+
difficulty: parsed.difficulty || null,
|
|
196
|
+
tags: JSON.stringify(parsed.tags || []),
|
|
197
|
+
summary_cn: parsed.summary_cn || '',
|
|
198
|
+
summary_en: parsed.summary_en || '',
|
|
199
|
+
usage_guide_cn: parsed.usage_guide_cn || '',
|
|
200
|
+
usage_guide_en: parsed.usage_guide_en || '',
|
|
201
|
+
content: JSON.stringify(parsed.content || {}),
|
|
202
|
+
relations: JSON.stringify(parsed.relations || {}),
|
|
203
|
+
constraints: JSON.stringify(parsed.constraints || {}),
|
|
204
|
+
reasoning: JSON.stringify(parsed.reasoning || {}),
|
|
205
|
+
quality: JSON.stringify(parsed.quality || {}),
|
|
206
|
+
stats: JSON.stringify(parsed.stats || {}),
|
|
207
|
+
headers: JSON.stringify(parsed.headers || []),
|
|
208
|
+
header_paths: JSON.stringify(parsed.header_paths || []),
|
|
209
|
+
module_name: parsed.module_name || '',
|
|
210
|
+
include_headers: parsed.include_headers ? 1 : 0,
|
|
211
|
+
agent_notes: parsed.agent_notes ? JSON.stringify(parsed.agent_notes) : null,
|
|
212
|
+
ai_insight: parsed.ai_insight || null,
|
|
213
|
+
reviewed_by: parsed.reviewed_by || null,
|
|
214
|
+
reviewed_at: parsed.reviewed_at || null,
|
|
215
|
+
rejection_reason: parsed.rejection_reason || null,
|
|
216
|
+
source: parsed.source || 'file-sync',
|
|
217
|
+
source_file: relPath,
|
|
218
|
+
source_candidate_id: parsed.source_candidate_id || null,
|
|
219
|
+
created_by: parsed.created_by || 'file-sync',
|
|
220
|
+
created_at: parsed.created_at || now,
|
|
221
|
+
updated_at: parsed.updated_at || now,
|
|
222
|
+
published_at: parsed.published_at || null,
|
|
223
|
+
published_by: parsed.published_by || null,
|
|
224
|
+
content_hash: contentHash,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
|
|
230
|
+
*/
|
|
231
|
+
_prepareUpsert(db) {
|
|
232
|
+
const cols = [
|
|
233
|
+
'id', 'title', 'trigger_key', 'description',
|
|
234
|
+
'lifecycle', 'lifecycle_history', 'probation',
|
|
235
|
+
'language', 'category', 'kind', 'knowledge_type', 'complexity', 'scope', 'difficulty',
|
|
236
|
+
'tags', 'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
|
|
237
|
+
'content', 'relations', 'constraints', 'reasoning', 'quality', 'stats',
|
|
238
|
+
'headers', 'header_paths', 'module_name', 'include_headers',
|
|
239
|
+
'agent_notes', 'ai_insight',
|
|
240
|
+
'reviewed_by', 'reviewed_at', 'rejection_reason',
|
|
241
|
+
'source', 'source_file', 'source_candidate_id',
|
|
242
|
+
'created_by', 'created_at', 'updated_at',
|
|
243
|
+
'published_at', 'published_by',
|
|
244
|
+
'content_hash',
|
|
245
|
+
];
|
|
246
|
+
|
|
247
|
+
// ON CONFLICT 更新除 id, created_by, created_at 以外的所有列
|
|
248
|
+
const updateCols = cols.filter(c => !['id', 'created_by', 'created_at'].includes(c));
|
|
249
|
+
const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
|
|
250
|
+
|
|
251
|
+
const sql = `
|
|
252
|
+
INSERT INTO knowledge_entries (${cols.join(', ')})
|
|
253
|
+
VALUES (${cols.map(() => '?').join(', ')})
|
|
254
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
255
|
+
${setClauses}
|
|
256
|
+
`;
|
|
257
|
+
|
|
258
|
+
return db.prepare(sql);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* 检查 entry 是否已存在于 DB
|
|
263
|
+
*/
|
|
264
|
+
_entryExists(db, id) {
|
|
265
|
+
const row = db.prepare('SELECT 1 FROM knowledge_entries WHERE id = ?').get(id);
|
|
266
|
+
return !!row;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/* ═══ 违规记录 ═══════════════════════════════════════════ */
|
|
270
|
+
|
|
271
|
+
_prepareAuditInsert(db) {
|
|
272
|
+
try {
|
|
273
|
+
return db.prepare(`
|
|
274
|
+
INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
|
|
275
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
276
|
+
`);
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
_logViolation(stmt, entryId, filePath, expectedHash, actualHash) {
|
|
283
|
+
try {
|
|
284
|
+
stmt.run(
|
|
285
|
+
randomUUID(),
|
|
286
|
+
Math.floor(Date.now() / 1000),
|
|
287
|
+
'sync',
|
|
288
|
+
JSON.stringify({ source: 'cli' }),
|
|
289
|
+
'manual_knowledge_edit',
|
|
290
|
+
entryId,
|
|
291
|
+
JSON.stringify({ file: filePath, expectedHash, actualHash }),
|
|
292
|
+
'violation_detected',
|
|
293
|
+
null,
|
|
294
|
+
0,
|
|
295
|
+
);
|
|
296
|
+
} catch (err) {
|
|
297
|
+
this.logger.warn('KnowledgeSyncService: failed to log violation', {
|
|
298
|
+
entryId,
|
|
299
|
+
error: err.message,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/* ═══ 孤儿检测 ═══════════════════════════════════════════ */
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 检测 DB 中存在但 .md 已删除的 Entry → 标记 deprecated
|
|
308
|
+
* @returns {string[]} 孤儿 entry id 列表
|
|
309
|
+
*/
|
|
310
|
+
_detectOrphans(db, syncedIds, dryRun) {
|
|
311
|
+
const orphanIds = [];
|
|
312
|
+
try {
|
|
313
|
+
const rows = db.prepare(
|
|
314
|
+
`SELECT id, source_file FROM knowledge_entries
|
|
315
|
+
WHERE lifecycle NOT IN ('deprecated')
|
|
316
|
+
AND source_file IS NOT NULL`
|
|
317
|
+
).all();
|
|
318
|
+
|
|
319
|
+
for (const row of rows) {
|
|
320
|
+
if (!syncedIds.has(row.id)) {
|
|
321
|
+
orphanIds.push(row.id);
|
|
322
|
+
if (!dryRun) {
|
|
323
|
+
const now = Math.floor(Date.now() / 1000);
|
|
324
|
+
db.prepare(
|
|
325
|
+
`UPDATE knowledge_entries
|
|
326
|
+
SET lifecycle = 'deprecated',
|
|
327
|
+
rejection_reason = ?,
|
|
328
|
+
updated_at = ?
|
|
329
|
+
WHERE id = ?`
|
|
330
|
+
).run('source file deleted (orphan)', now, row.id);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
} catch (err) {
|
|
335
|
+
this.logger.warn('KnowledgeSyncService: orphan detection failed', {
|
|
336
|
+
error: err.message,
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
return orphanIds;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
export default KnowledgeSyncService;
|
package/lib/cli/SetupService.js
CHANGED
|
@@ -288,7 +288,7 @@ export class SetupService {
|
|
|
288
288
|
' requires_capability: ["git_write"]',
|
|
289
289
|
' - id: "external_agent"',
|
|
290
290
|
' name: "External Agent"',
|
|
291
|
-
' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:
|
|
291
|
+
' permissions: ["read:recipes", "read:guard_rules", "create:candidates", "submit:knowledge"]',
|
|
292
292
|
' - id: "chat_agent"',
|
|
293
293
|
' name: "ChatAgent"',
|
|
294
294
|
' permissions: ["read:recipes", "read:candidates", "create:candidates", "read:guard_rules"]',
|
|
@@ -571,40 +571,22 @@ export class SetupService {
|
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
/**
|
|
574
|
-
* @private 从 AutoSnippet/recipes/*.md 同步到 DB 缓存
|
|
575
|
-
* 委托
|
|
574
|
+
* @private 从 AutoSnippet/recipes/*.md + candidates/*.md 同步到 DB 缓存
|
|
575
|
+
* 委托 KnowledgeSyncService 执行全字段同步(setup 场景跳过违规记录)
|
|
576
576
|
*/
|
|
577
577
|
async _syncRecipesToDB(db) {
|
|
578
|
-
const {
|
|
579
|
-
const syncService = new
|
|
578
|
+
const { KnowledgeSyncService } = await import('./KnowledgeSyncService.js');
|
|
579
|
+
const syncService = new KnowledgeSyncService(this.projectRoot);
|
|
580
580
|
const report = syncService.sync(db, { skipViolations: true });
|
|
581
581
|
|
|
582
582
|
if (report.synced > 0) {
|
|
583
|
-
console.log(` ✅ 已同步 ${report.synced}
|
|
583
|
+
console.log(` ✅ 已同步 ${report.synced} 个知识文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
|
|
584
584
|
} else {
|
|
585
|
-
console.log(' ℹ️
|
|
585
|
+
console.log(' ℹ️ 暂无 .md 文件,跳过同步');
|
|
586
586
|
}
|
|
587
587
|
|
|
588
588
|
if (report.orphaned.length > 0) {
|
|
589
|
-
console.log(` ℹ️ ${report.orphaned.length}
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// ── Candidate 文件同步 ──
|
|
593
|
-
await this._syncCandidatesToDB(db);
|
|
594
|
-
}
|
|
595
|
-
|
|
596
|
-
/**
|
|
597
|
-
* @private 从 AutoSnippet/candidates/*.md 同步到 DB 缓存
|
|
598
|
-
*/
|
|
599
|
-
async _syncCandidatesToDB(db) {
|
|
600
|
-
const { CandidateSyncService } = await import('./CandidateSyncService.js');
|
|
601
|
-
const syncService = new CandidateSyncService(this.projectRoot);
|
|
602
|
-
const report = syncService.sync(db, { skipViolations: true });
|
|
603
|
-
|
|
604
|
-
if (report.synced > 0) {
|
|
605
|
-
console.log(` ✅ 已同步 ${report.synced} 个 Candidate 文件到 DB 缓存(新增 ${report.created},更新 ${report.updated})`);
|
|
606
|
-
} else {
|
|
607
|
-
console.log(' ℹ️ candidates/ 暂无 .md 文件,跳过同步');
|
|
589
|
+
console.log(` ℹ️ ${report.orphaned.length} 个孤儿条目已标记 deprecated`);
|
|
608
590
|
}
|
|
609
591
|
}
|
|
610
592
|
|
|
@@ -664,7 +646,7 @@ export class SetupService {
|
|
|
664
646
|
'# 完整配置说明见 .env.example',
|
|
665
647
|
'',
|
|
666
648
|
'ASD_AI_PROVIDER=google',
|
|
667
|
-
'ASD_AI_MODEL=gemini-
|
|
649
|
+
'ASD_AI_MODEL=gemini-3-flash-preview',
|
|
668
650
|
'# ASD_GOOGLE_API_KEY=',
|
|
669
651
|
'',
|
|
670
652
|
].join('\n'));
|
|
@@ -537,6 +537,166 @@ export default class ProjectGraph {
|
|
|
537
537
|
}
|
|
538
538
|
}
|
|
539
539
|
}
|
|
540
|
+
|
|
541
|
+
// ── 序列化 / 反序列化 ──────────────────────────────────────
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* 序列化为可 JSON.stringify 的纯对象
|
|
545
|
+
* @returns {object}
|
|
546
|
+
*/
|
|
547
|
+
toJSON() {
|
|
548
|
+
const mapToObj = (map) => Object.fromEntries(map);
|
|
549
|
+
const mapOfSetsToObj = (map) => {
|
|
550
|
+
const obj = {};
|
|
551
|
+
for (const [k, v] of map) obj[k] = [...v];
|
|
552
|
+
return obj;
|
|
553
|
+
};
|
|
554
|
+
|
|
555
|
+
return {
|
|
556
|
+
projectRoot: this.#projectRoot,
|
|
557
|
+
buildTimeMs: this.#buildTimeMs,
|
|
558
|
+
classes: mapToObj(this.#classes),
|
|
559
|
+
protocols: mapToObj(this.#protocols),
|
|
560
|
+
categories: mapToObj(this.#categories),
|
|
561
|
+
inheritance: mapToObj(this.#inheritance),
|
|
562
|
+
conformance: mapOfSetsToObj(this.#conformance),
|
|
563
|
+
files: mapToObj(this.#files),
|
|
564
|
+
methodsByClass: mapToObj(this.#methodsByClass),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* 从缓存数据恢复 ProjectGraph 实例
|
|
570
|
+
* @param {object} data toJSON() 输出的对象
|
|
571
|
+
* @returns {ProjectGraph}
|
|
572
|
+
*/
|
|
573
|
+
static fromJSON(data) {
|
|
574
|
+
const graph = new ProjectGraph();
|
|
575
|
+
graph.#projectRoot = data.projectRoot || '';
|
|
576
|
+
graph.#buildTimeMs = data.buildTimeMs || 0;
|
|
577
|
+
|
|
578
|
+
// 恢复 classes
|
|
579
|
+
for (const [name, info] of Object.entries(data.classes || {})) {
|
|
580
|
+
graph.#classes.set(name, info);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// 恢复 protocols
|
|
584
|
+
for (const [name, info] of Object.entries(data.protocols || {})) {
|
|
585
|
+
graph.#protocols.set(name, info);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// 恢复 categories
|
|
589
|
+
for (const [name, arr] of Object.entries(data.categories || {})) {
|
|
590
|
+
graph.#categories.set(name, arr);
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// 恢复 inheritance
|
|
594
|
+
for (const [child, parent] of Object.entries(data.inheritance || {})) {
|
|
595
|
+
graph.#inheritance.set(child, parent);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// 恢复 conformance (Set)
|
|
599
|
+
for (const [cls, protos] of Object.entries(data.conformance || {})) {
|
|
600
|
+
graph.#conformance.set(cls, new Set(protos));
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// 恢复 files
|
|
604
|
+
for (const [path, symbols] of Object.entries(data.files || {})) {
|
|
605
|
+
graph.#files.set(path, symbols);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 恢复 methodsByClass
|
|
609
|
+
for (const [cls, methods] of Object.entries(data.methodsByClass || {})) {
|
|
610
|
+
graph.#methodsByClass.set(cls, methods);
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
return graph;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* 增量更新:仅重新解析变更文件,合并到现有图中
|
|
618
|
+
* @param {string[]} changedPaths 变更文件的绝对路径
|
|
619
|
+
* @param {string[]} deletedPaths 删除文件的相对路径
|
|
620
|
+
* @param {object} [options]
|
|
621
|
+
* @returns {Promise<{ added: number, updated: number, deleted: number }>}
|
|
622
|
+
*/
|
|
623
|
+
async incrementalUpdate(changedPaths, deletedPaths = [], options = {}) {
|
|
624
|
+
const { analyzeFile, isAvailable } = await import('../AstAnalyzer.js');
|
|
625
|
+
if (!isAvailable()) return { added: 0, updated: 0, deleted: 0 };
|
|
626
|
+
|
|
627
|
+
const extToLang = options.extensionToLang || DEFAULTS.extensionToLang;
|
|
628
|
+
let added = 0, updated = 0, deleted = 0;
|
|
629
|
+
|
|
630
|
+
// 1. 删除已移除文件的索引
|
|
631
|
+
for (const relPath of deletedPaths) {
|
|
632
|
+
if (this.#files.has(relPath)) {
|
|
633
|
+
const symbols = this.#files.get(relPath);
|
|
634
|
+
// 清除该文件贡献的类、协议、Category
|
|
635
|
+
for (const cls of symbols.classes || []) {
|
|
636
|
+
this.#classes.delete(cls);
|
|
637
|
+
this.#inheritance.delete(cls);
|
|
638
|
+
this.#conformance.delete(cls);
|
|
639
|
+
this.#methodsByClass.delete(cls);
|
|
640
|
+
}
|
|
641
|
+
for (const proto of symbols.protocols || []) {
|
|
642
|
+
this.#protocols.delete(proto);
|
|
643
|
+
}
|
|
644
|
+
for (const catKey of symbols.categories || []) {
|
|
645
|
+
const className = catKey.split('(')[0];
|
|
646
|
+
this.#categories.delete(className);
|
|
647
|
+
}
|
|
648
|
+
this.#files.delete(relPath);
|
|
649
|
+
deleted++;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// 2. 重新解析变更文件
|
|
654
|
+
for (const filePath of changedPaths) {
|
|
655
|
+
try {
|
|
656
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
657
|
+
const ext = path.extname(filePath);
|
|
658
|
+
const lang = extToLang[ext];
|
|
659
|
+
if (!lang) continue;
|
|
660
|
+
|
|
661
|
+
const relativePath = path.relative(this.#projectRoot, filePath);
|
|
662
|
+
const isUpdate = this.#files.has(relativePath);
|
|
663
|
+
|
|
664
|
+
// 先清除旧索引(如果是更新)
|
|
665
|
+
if (isUpdate) {
|
|
666
|
+
const oldSymbols = this.#files.get(relativePath);
|
|
667
|
+
for (const cls of oldSymbols.classes || []) {
|
|
668
|
+
this.#classes.delete(cls);
|
|
669
|
+
this.#inheritance.delete(cls);
|
|
670
|
+
this.#conformance.delete(cls);
|
|
671
|
+
this.#methodsByClass.delete(cls);
|
|
672
|
+
}
|
|
673
|
+
for (const proto of oldSymbols.protocols || []) {
|
|
674
|
+
this.#protocols.delete(proto);
|
|
675
|
+
}
|
|
676
|
+
for (const catKey of oldSymbols.categories || []) {
|
|
677
|
+
const className = catKey.split('(')[0];
|
|
678
|
+
this.#categories.delete(className);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const summary = analyzeFile(content, lang);
|
|
683
|
+
if (!summary) continue;
|
|
684
|
+
|
|
685
|
+
this.#indexFileSummary(relativePath, summary);
|
|
686
|
+
isUpdate ? updated++ : added++;
|
|
687
|
+
} catch {
|
|
688
|
+
// 单文件解析失败不阻塞
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 3. 重建反向索引
|
|
693
|
+
if (added + updated + deleted > 0) {
|
|
694
|
+
this.#buildReverseIndices();
|
|
695
|
+
this.#overview = null; // 清除统计缓存
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
return { added, updated, deleted };
|
|
699
|
+
}
|
|
540
700
|
}
|
|
541
701
|
|
|
542
702
|
// ──────────────────────────────────────────────────────────────────
|