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
package/lib/cli/SyncService.js
DELETED
|
@@ -1,356 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SyncService — 将 AutoSnippet/recipes/*.md 增量同步到 SQLite DB
|
|
3
|
-
*
|
|
4
|
-
* 设计原则:
|
|
5
|
-
* - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
|
|
6
|
-
* - 所有 frontmatter 字段(基础 + _ 前缀机器字段)完整写入 DB
|
|
7
|
-
* - 通过 _contentHash 检测手写/手改 .md → 进入违规统计(audit_logs)
|
|
8
|
-
* - 孤儿 Recipe(DB 有但 .md 不存在)→ 自动标记 deprecated
|
|
9
|
-
*
|
|
10
|
-
* 使用方式:
|
|
11
|
-
* - CLI: `asd sync [--force] [--dry-run] [-d <dir>]`
|
|
12
|
-
* - 内部: SetupService.stepDatabase() 委托调用(skipViolations=true)
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import fs from 'node:fs';
|
|
16
|
-
import path from 'node:path';
|
|
17
|
-
import { randomUUID } from 'node:crypto';
|
|
18
|
-
import { RECIPES_DIR } from '../infrastructure/config/Defaults.js';
|
|
19
|
-
import { computeContentHash, parseRecipeMarkdown } from '../service/recipe/RecipeFileWriter.js';
|
|
20
|
-
import { inferKind } from '../domain/recipe/Recipe.js';
|
|
21
|
-
import Logger from '../infrastructure/logging/Logger.js';
|
|
22
|
-
|
|
23
|
-
export class SyncService {
|
|
24
|
-
/**
|
|
25
|
-
* @param {string} projectRoot
|
|
26
|
-
*/
|
|
27
|
-
constructor(projectRoot) {
|
|
28
|
-
this.projectRoot = projectRoot;
|
|
29
|
-
this.recipesDir = path.join(projectRoot, RECIPES_DIR);
|
|
30
|
-
this.logger = Logger.getInstance();
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 执行增量同步:.md → DB
|
|
35
|
-
* @param {import('better-sqlite3').Database} db better-sqlite3 原始句柄
|
|
36
|
-
* @param {object} [opts={}]
|
|
37
|
-
* @param {boolean} [opts.dryRun=false] 只报告不写入
|
|
38
|
-
* @param {boolean} [opts.force=false] 忽略 hash,强制覆盖
|
|
39
|
-
* @param {boolean} [opts.skipViolations=false] 跳过违规记录(setup 场景)
|
|
40
|
-
* @returns {{ synced: number, created: number, updated: number, violations: string[], orphaned: string[], skipped: number }}
|
|
41
|
-
*/
|
|
42
|
-
sync(db, opts = {}) {
|
|
43
|
-
const { dryRun = false, force = false, skipViolations = false } = opts;
|
|
44
|
-
|
|
45
|
-
const report = {
|
|
46
|
-
synced: 0,
|
|
47
|
-
created: 0,
|
|
48
|
-
updated: 0,
|
|
49
|
-
violations: [], // 手动编辑的文件列表
|
|
50
|
-
orphaned: [], // DB 有但 .md 不存在
|
|
51
|
-
skipped: 0,
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// ── 1. 收集 .md 文件 ──
|
|
55
|
-
const mdFiles = this._collectMdFiles();
|
|
56
|
-
if (mdFiles.length === 0) {
|
|
57
|
-
this.logger.info('SyncService: no .md files found in recipes/');
|
|
58
|
-
return report;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ── 2. 准备 upsert 语句 ──
|
|
62
|
-
const upsertStmt = dryRun ? null : this._prepareUpsert(db);
|
|
63
|
-
const auditStmt = (dryRun || skipViolations) ? null : this._prepareAuditInsert(db);
|
|
64
|
-
|
|
65
|
-
// ── 3. 逐文件同步 ──
|
|
66
|
-
const syncedIds = new Set();
|
|
67
|
-
|
|
68
|
-
for (const { absPath, relPath } of mdFiles) {
|
|
69
|
-
try {
|
|
70
|
-
const content = fs.readFileSync(absPath, 'utf8');
|
|
71
|
-
const parsed = parseRecipeMarkdown(content, relPath);
|
|
72
|
-
|
|
73
|
-
if (!parsed.id) {
|
|
74
|
-
this.logger.warn(`SyncService: skip file without id — ${relPath}`);
|
|
75
|
-
report.skipped++;
|
|
76
|
-
continue;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
syncedIds.add(parsed.id);
|
|
80
|
-
|
|
81
|
-
// ── 检测手动编辑 ──
|
|
82
|
-
const actualHash = computeContentHash(content);
|
|
83
|
-
const storedHash = parsed._contentHash;
|
|
84
|
-
const isManualEdit = storedHash && storedHash !== actualHash && !force;
|
|
85
|
-
|
|
86
|
-
if (isManualEdit) {
|
|
87
|
-
report.violations.push(relPath);
|
|
88
|
-
if (auditStmt) {
|
|
89
|
-
this._logViolation(auditStmt, parsed.id, relPath, storedHash, actualHash);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// ── upsert ──
|
|
94
|
-
if (!dryRun) {
|
|
95
|
-
const existed = this._recipeExists(db, parsed.id);
|
|
96
|
-
const row = this._buildDbRow(parsed, relPath, content);
|
|
97
|
-
upsertStmt.run(...Object.values(row));
|
|
98
|
-
|
|
99
|
-
if (existed) {
|
|
100
|
-
report.updated++;
|
|
101
|
-
} else {
|
|
102
|
-
report.created++;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
report.synced++;
|
|
107
|
-
} catch (err) {
|
|
108
|
-
this.logger.error(`SyncService: failed to sync ${relPath}`, { error: err.message });
|
|
109
|
-
report.skipped++;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// ── 4. 检测孤儿(DB 有但 .md 不存在)──
|
|
114
|
-
report.orphaned = this._detectOrphans(db, syncedIds, dryRun);
|
|
115
|
-
|
|
116
|
-
this.logger.info('SyncService: sync complete', {
|
|
117
|
-
synced: report.synced,
|
|
118
|
-
created: report.created,
|
|
119
|
-
updated: report.updated,
|
|
120
|
-
violations: report.violations.length,
|
|
121
|
-
orphaned: report.orphaned.length,
|
|
122
|
-
skipped: report.skipped,
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
return report;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/* ═══ 文件收集 ═══════════════════════════════════════════ */
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* 递归收集 recipes/ 下所有 .md 文件(跳过 _ 前缀模板)
|
|
132
|
-
* @returns {{ absPath: string, relPath: string }[]}
|
|
133
|
-
*/
|
|
134
|
-
_collectMdFiles() {
|
|
135
|
-
if (!fs.existsSync(this.recipesDir)) return [];
|
|
136
|
-
|
|
137
|
-
const results = [];
|
|
138
|
-
const walk = (dir, base) => {
|
|
139
|
-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
140
|
-
const full = path.join(dir, entry.name);
|
|
141
|
-
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
142
|
-
|
|
143
|
-
if (entry.isDirectory()) {
|
|
144
|
-
walk(full, rel);
|
|
145
|
-
} else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
|
|
146
|
-
results.push({ absPath: full, relPath: rel });
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
};
|
|
150
|
-
walk(this.recipesDir, '');
|
|
151
|
-
return results;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/* ═══ DB 操作 ═══════════════════════════════════════════ */
|
|
155
|
-
|
|
156
|
-
/**
|
|
157
|
-
* 构建 DB upsert 所需的行数据
|
|
158
|
-
* @param {object} parsed parseRecipeMarkdown 返回值
|
|
159
|
-
* @param {string} relPath 相对于项目根目录的 source_file
|
|
160
|
-
* @param {string} rawContent 原始 .md 全文
|
|
161
|
-
* @returns {object} 列名→值映射
|
|
162
|
-
*/
|
|
163
|
-
_buildDbRow(parsed, relPath, rawContent) {
|
|
164
|
-
// 从 body 提取代码块和结构化段落
|
|
165
|
-
const codeMatch = rawContent.match(/```\w*\s*\r?\n([\s\S]*?)```/);
|
|
166
|
-
const pattern = codeMatch ? codeMatch[1].trim() : '';
|
|
167
|
-
|
|
168
|
-
// 提取结构化段落(从 Markdown body 中按 ## 标题解析)
|
|
169
|
-
const rationale = this._extractSection(rawContent, '设计原理|Rationale|Why') || '';
|
|
170
|
-
const verification = this._extractSection(rawContent, '验证|Verification|Test') || '';
|
|
171
|
-
|
|
172
|
-
const contentJson = JSON.stringify({
|
|
173
|
-
pattern,
|
|
174
|
-
rationale,
|
|
175
|
-
verification: verification ? { method: 'section', expectedResult: verification } : null,
|
|
176
|
-
markdown: rawContent,
|
|
177
|
-
});
|
|
178
|
-
|
|
179
|
-
// usage_guide: 从 body 提取“使用指南/Usage”段落,同时兑容 frontmatter
|
|
180
|
-
const usageGuideCn = parsed.usageGuideCn || this._extractSection(rawContent, '使用指南|使用方法|如何使用') || null;
|
|
181
|
-
const usageGuideEn = parsed.usageGuideEn || this._extractSection(rawContent, 'Usage Guide|How to Use') || null;
|
|
182
|
-
|
|
183
|
-
const sourceFilePath = path.join(RECIPES_DIR, relPath).replace(/\\/g, '/');
|
|
184
|
-
const knowledgeType = parsed.knowledgeType || 'code-pattern';
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
id: parsed.id,
|
|
188
|
-
title: parsed.title || '',
|
|
189
|
-
description: parsed.summaryCn || parsed.summaryEn || '',
|
|
190
|
-
language: parsed.language || 'swift',
|
|
191
|
-
category: parsed.category || 'general',
|
|
192
|
-
summary_cn: parsed.summaryCn || null,
|
|
193
|
-
summary_en: parsed.summaryEn || null,
|
|
194
|
-
usage_guide_cn: usageGuideCn,
|
|
195
|
-
usage_guide_en: usageGuideEn,
|
|
196
|
-
knowledge_type: knowledgeType,
|
|
197
|
-
kind: parsed.kind || inferKind(knowledgeType),
|
|
198
|
-
complexity: parsed.complexity || 'intermediate',
|
|
199
|
-
scope: parsed.scope || null,
|
|
200
|
-
trigger: parsed.trigger || '',
|
|
201
|
-
source_file: sourceFilePath,
|
|
202
|
-
content_json: contentJson,
|
|
203
|
-
relations_json: JSON.stringify(parsed.relations || {}),
|
|
204
|
-
constraints_json: JSON.stringify(parsed.constraints || {}),
|
|
205
|
-
quality_code_completeness: parsed.quality?.codeCompleteness ?? 0,
|
|
206
|
-
quality_project_adaptation: parsed.quality?.projectAdaptation ?? 0,
|
|
207
|
-
quality_documentation_clarity: parsed.quality?.documentationClarity ?? 0,
|
|
208
|
-
quality_overall: parsed.quality?.overall ?? 0,
|
|
209
|
-
dimensions_json: JSON.stringify({
|
|
210
|
-
headers: parsed.headers || [],
|
|
211
|
-
authority: parsed.authority,
|
|
212
|
-
difficulty: parsed.difficulty,
|
|
213
|
-
version: parsed.version,
|
|
214
|
-
}),
|
|
215
|
-
tags_json: JSON.stringify(parsed.tags || []),
|
|
216
|
-
adoption_count: parsed.statistics?.adoptionCount ?? 0,
|
|
217
|
-
application_count: parsed.statistics?.applicationCount ?? 0,
|
|
218
|
-
guard_hit_count: parsed.statistics?.guardHitCount ?? 0,
|
|
219
|
-
view_count: parsed.statistics?.viewCount ?? 0,
|
|
220
|
-
success_count: parsed.statistics?.successCount ?? 0,
|
|
221
|
-
feedback_score: parsed.statistics?.feedbackScore ?? 0,
|
|
222
|
-
status: parsed.status || 'active',
|
|
223
|
-
created_by: parsed.createdBy || 'file-sync',
|
|
224
|
-
created_at: parsed.createdAt || Math.floor(Date.now() / 1000),
|
|
225
|
-
updated_at: parsed.updatedAt || Math.floor(Date.now() / 1000),
|
|
226
|
-
published_by: parsed.publishedBy || null,
|
|
227
|
-
published_at: parsed.publishedAt || null,
|
|
228
|
-
deprecation_reason: parsed.deprecationReason || null,
|
|
229
|
-
deprecated_at: parsed.deprecatedAt || null,
|
|
230
|
-
source_candidate_id: parsed.sourceCandidate || null,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* 从 Markdown body 中按 ## 标题匹配提取段落内容
|
|
236
|
-
* @param {string} content 原始 Markdown 全文
|
|
237
|
-
* @param {string} headingPattern 标题的正则 alternation(如 '设计原理|Rationale')
|
|
238
|
-
* @returns {string|null}
|
|
239
|
-
*/
|
|
240
|
-
_extractSection(content, headingPattern) {
|
|
241
|
-
const regex = new RegExp(`^##\\s+(${headingPattern})\\s*$`, 'im');
|
|
242
|
-
const match = content.match(regex);
|
|
243
|
-
if (!match) return null;
|
|
244
|
-
|
|
245
|
-
const startIdx = match.index + match[0].length;
|
|
246
|
-
// 查找下一个 ## 标题或文件末尾
|
|
247
|
-
const rest = content.slice(startIdx);
|
|
248
|
-
const nextHeading = rest.search(/^##\s+/m);
|
|
249
|
-
const sectionContent = (nextHeading >= 0 ? rest.slice(0, nextHeading) : rest).trim();
|
|
250
|
-
return sectionContent || null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
|
|
255
|
-
*/
|
|
256
|
-
_prepareUpsert(db) {
|
|
257
|
-
const cols = [
|
|
258
|
-
'id', 'title', 'description', 'language', 'category',
|
|
259
|
-
'summary_cn', 'summary_en', 'usage_guide_cn', 'usage_guide_en',
|
|
260
|
-
'knowledge_type', 'kind', 'complexity', 'scope', 'trigger',
|
|
261
|
-
'source_file', 'content_json', 'relations_json', 'constraints_json',
|
|
262
|
-
'quality_code_completeness', 'quality_project_adaptation',
|
|
263
|
-
'quality_documentation_clarity', 'quality_overall',
|
|
264
|
-
'dimensions_json', 'tags_json',
|
|
265
|
-
'adoption_count', 'application_count', 'guard_hit_count',
|
|
266
|
-
'view_count', 'success_count', 'feedback_score',
|
|
267
|
-
'status', 'created_by', 'created_at', 'updated_at',
|
|
268
|
-
'published_by', 'published_at',
|
|
269
|
-
'deprecation_reason', 'deprecated_at',
|
|
270
|
-
'source_candidate_id',
|
|
271
|
-
];
|
|
272
|
-
|
|
273
|
-
// ON CONFLICT 更新除 id, created_by, created_at 以外的所有列
|
|
274
|
-
const updateCols = cols.filter(c => !['id', 'created_by', 'created_at'].includes(c));
|
|
275
|
-
const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
|
|
276
|
-
|
|
277
|
-
const sql = `
|
|
278
|
-
INSERT INTO recipes (${cols.join(', ')})
|
|
279
|
-
VALUES (${cols.map(() => '?').join(', ')})
|
|
280
|
-
ON CONFLICT(id) DO UPDATE SET
|
|
281
|
-
${setClauses}
|
|
282
|
-
`;
|
|
283
|
-
|
|
284
|
-
return db.prepare(sql);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* 检查 recipe 是否已存在于 DB
|
|
289
|
-
*/
|
|
290
|
-
_recipeExists(db, id) {
|
|
291
|
-
const row = db.prepare('SELECT 1 FROM recipes WHERE id = ?').get(id);
|
|
292
|
-
return !!row;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/* ═══ 违规记录 ═══════════════════════════════════════════ */
|
|
296
|
-
|
|
297
|
-
_prepareAuditInsert(db) {
|
|
298
|
-
// 确保 audit_logs 表存在(可能在无迁移的情况下不存在)
|
|
299
|
-
try {
|
|
300
|
-
return db.prepare(`
|
|
301
|
-
INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
|
|
302
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
303
|
-
`);
|
|
304
|
-
} catch {
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
_logViolation(stmt, recipeId, filePath, expectedHash, actualHash) {
|
|
310
|
-
try {
|
|
311
|
-
stmt.run(
|
|
312
|
-
randomUUID(),
|
|
313
|
-
Math.floor(Date.now() / 1000),
|
|
314
|
-
'sync',
|
|
315
|
-
JSON.stringify({ source: 'cli' }),
|
|
316
|
-
'manual_recipe_edit',
|
|
317
|
-
recipeId,
|
|
318
|
-
JSON.stringify({ file: filePath, expectedHash, actualHash }),
|
|
319
|
-
'violation_detected',
|
|
320
|
-
null,
|
|
321
|
-
0,
|
|
322
|
-
);
|
|
323
|
-
} catch (err) {
|
|
324
|
-
this.logger.warn('SyncService: failed to log violation', { recipeId, error: err.message });
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/* ═══ 孤儿检测 ═══════════════════════════════════════════ */
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* 检测 DB 中存在但 .md 已删除的 Recipe → 标记 deprecated
|
|
332
|
-
* @returns {string[]} 孤儿 recipe id 列表
|
|
333
|
-
*/
|
|
334
|
-
_detectOrphans(db, syncedIds, dryRun) {
|
|
335
|
-
const orphanIds = [];
|
|
336
|
-
try {
|
|
337
|
-
const rows = db.prepare(
|
|
338
|
-
`SELECT id, source_file FROM recipes WHERE status != 'deprecated' AND source_file IS NOT NULL`
|
|
339
|
-
).all();
|
|
340
|
-
|
|
341
|
-
for (const row of rows) {
|
|
342
|
-
if (!syncedIds.has(row.id)) {
|
|
343
|
-
orphanIds.push(row.id);
|
|
344
|
-
if (!dryRun) {
|
|
345
|
-
db.prepare(
|
|
346
|
-
`UPDATE recipes SET status = 'deprecated', deprecation_reason = ?, deprecated_at = ?, updated_at = ? WHERE id = ?`
|
|
347
|
-
).run('source file deleted (orphan)', Math.floor(Date.now() / 1000), Math.floor(Date.now() / 1000), row.id);
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
} catch (err) {
|
|
352
|
-
this.logger.warn('SyncService: orphan detection failed', { error: err.message });
|
|
353
|
-
}
|
|
354
|
-
return orphanIds;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
2
|
-
import { CandidateStatus, isValidStateTransition } from '../types/CandidateStatus.js';
|
|
3
|
-
import Reasoning from './Reasoning.js';
|
|
4
|
-
import Logger from '../../infrastructure/logging/Logger.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Candidate - 代码片段候选实体
|
|
8
|
-
* 代表一个待审批的代码片段
|
|
9
|
-
*/
|
|
10
|
-
export class Candidate {
|
|
11
|
-
constructor(props) {
|
|
12
|
-
this.id = props.id || uuidv4();
|
|
13
|
-
this.code = props.code;
|
|
14
|
-
this.language = props.language; // javascript, python, swift, etc
|
|
15
|
-
this.category = props.category; // 分类:pattern, utility, hook, etc
|
|
16
|
-
this.source = props.source; // AI, user, imported, etc
|
|
17
|
-
|
|
18
|
-
// Reasoning 对象 — 所有提交路径必须提供推理依据
|
|
19
|
-
this.reasoning = props.reasoning instanceof Reasoning
|
|
20
|
-
? props.reasoning
|
|
21
|
-
: new Reasoning(props.reasoning || {});
|
|
22
|
-
|
|
23
|
-
// 状态管理
|
|
24
|
-
this.status = props.status || CandidateStatus.PENDING;
|
|
25
|
-
this.statusHistory = props.statusHistory || [];
|
|
26
|
-
|
|
27
|
-
// 元数据
|
|
28
|
-
this.createdBy = props.createdBy || 'unknown';
|
|
29
|
-
this.createdAt = props.createdAt || Math.floor(Date.now() / 1000);
|
|
30
|
-
this.updatedAt = props.updatedAt || Math.floor(Date.now() / 1000);
|
|
31
|
-
this.approvedAt = props.approvedAt;
|
|
32
|
-
this.approvedBy = props.approvedBy;
|
|
33
|
-
|
|
34
|
-
// 额外信息
|
|
35
|
-
this.rejectionReason = props.rejectionReason;
|
|
36
|
-
this.rejectedBy = props.rejectedBy;
|
|
37
|
-
this.appliedRecipeId = props.appliedRecipeId; // 应用到的 Recipe
|
|
38
|
-
this.metadata = props.metadata || {}; // 附加元数据
|
|
39
|
-
|
|
40
|
-
this.logger = Logger.getInstance();
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 验证 Candidate 的完整性
|
|
45
|
-
*/
|
|
46
|
-
isValid() {
|
|
47
|
-
return (
|
|
48
|
-
this.code &&
|
|
49
|
-
this.code.trim().length > 0 &&
|
|
50
|
-
this.language &&
|
|
51
|
-
this.language.trim().length > 0 &&
|
|
52
|
-
this.reasoning.isValid() &&
|
|
53
|
-
this.createdBy
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* 验证代码内容(格式等)
|
|
59
|
-
*/
|
|
60
|
-
validateCodeContent() {
|
|
61
|
-
if (!this.code || this.code.trim().length === 0) {
|
|
62
|
-
return { valid: false, error: 'Code cannot be empty' };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
if (this.code.length > 50000) {
|
|
66
|
-
return { valid: false, error: 'Code is too long (max 50000 chars)' };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return { valid: true };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 批准 Candidate
|
|
74
|
-
*/
|
|
75
|
-
approve(approver) {
|
|
76
|
-
if (!isValidStateTransition(this.status, CandidateStatus.APPROVED)) {
|
|
77
|
-
return {
|
|
78
|
-
success: false,
|
|
79
|
-
error: `Cannot approve a Candidate in ${this.status} status`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
this._changeStatus(CandidateStatus.APPROVED);
|
|
84
|
-
this.approvedAt = Math.floor(Date.now() / 1000);
|
|
85
|
-
this.approvedBy = approver;
|
|
86
|
-
|
|
87
|
-
this.logger.info('Candidate approved', {
|
|
88
|
-
candidateId: this.id,
|
|
89
|
-
approver,
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
return { success: true, candidate: this };
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 拒绝 Candidate
|
|
97
|
-
*/
|
|
98
|
-
reject(reason, rejectedBy = 'system') {
|
|
99
|
-
if (!isValidStateTransition(this.status, CandidateStatus.REJECTED)) {
|
|
100
|
-
return {
|
|
101
|
-
success: false,
|
|
102
|
-
error: `Cannot reject a Candidate in ${this.status} status`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
this._changeStatus(CandidateStatus.REJECTED);
|
|
107
|
-
this.rejectionReason = reason;
|
|
108
|
-
this.rejectedBy = rejectedBy;
|
|
109
|
-
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
110
|
-
|
|
111
|
-
this.logger.info('Candidate rejected', {
|
|
112
|
-
candidateId: this.id,
|
|
113
|
-
rejectedBy,
|
|
114
|
-
reason,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
return { success: true, candidate: this };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* 应用到 Recipe
|
|
122
|
-
*/
|
|
123
|
-
applyToRecipe(recipeId) {
|
|
124
|
-
if (!isValidStateTransition(this.status, CandidateStatus.APPLIED)) {
|
|
125
|
-
return {
|
|
126
|
-
success: false,
|
|
127
|
-
error: `Cannot apply a Candidate in ${this.status} status`,
|
|
128
|
-
};
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
this._changeStatus(CandidateStatus.APPLIED);
|
|
132
|
-
this.appliedRecipeId = recipeId;
|
|
133
|
-
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
134
|
-
|
|
135
|
-
this.logger.info('Candidate applied to recipe', {
|
|
136
|
-
candidateId: this.id,
|
|
137
|
-
recipeId,
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
return { success: true, candidate: this };
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 获取状态历史
|
|
145
|
-
*/
|
|
146
|
-
getStatusHistory() {
|
|
147
|
-
return this.statusHistory;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* 改变状态(内部方法)
|
|
152
|
-
*/
|
|
153
|
-
_changeStatus(newStatus) {
|
|
154
|
-
this.statusHistory.push({
|
|
155
|
-
from: this.status,
|
|
156
|
-
to: newStatus,
|
|
157
|
-
changedAt: Math.floor(Date.now() / 1000),
|
|
158
|
-
});
|
|
159
|
-
this.status = newStatus;
|
|
160
|
-
this.updatedAt = Math.floor(Date.now() / 1000);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* 转换为 JSON
|
|
165
|
-
*/
|
|
166
|
-
toJSON() {
|
|
167
|
-
return {
|
|
168
|
-
id: this.id,
|
|
169
|
-
code: this.code,
|
|
170
|
-
language: this.language,
|
|
171
|
-
category: this.category,
|
|
172
|
-
source: this.source,
|
|
173
|
-
reasoning: this.reasoning.toJSON(),
|
|
174
|
-
status: this.status,
|
|
175
|
-
statusHistory: this.statusHistory,
|
|
176
|
-
createdBy: this.createdBy,
|
|
177
|
-
createdAt: this.createdAt,
|
|
178
|
-
updatedAt: this.updatedAt,
|
|
179
|
-
approvedAt: this.approvedAt,
|
|
180
|
-
approvedBy: this.approvedBy,
|
|
181
|
-
rejectionReason: this.rejectionReason,
|
|
182
|
-
rejectedBy: this.rejectedBy,
|
|
183
|
-
appliedRecipeId: this.appliedRecipeId,
|
|
184
|
-
metadata: this.metadata,
|
|
185
|
-
};
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* 从 JSON 创建 Candidate
|
|
190
|
-
*/
|
|
191
|
-
static fromJSON(data) {
|
|
192
|
-
return new Candidate(data);
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
export default Candidate;
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CandidateRepository - Candidate 仓储接口
|
|
3
|
-
* 定义数据访问的契约
|
|
4
|
-
*/
|
|
5
|
-
export class CandidateRepository {
|
|
6
|
-
/**
|
|
7
|
-
* 创建 Candidate
|
|
8
|
-
* @param {Candidate} candidate
|
|
9
|
-
* @returns {Promise<Candidate>}
|
|
10
|
-
*/
|
|
11
|
-
async create(candidate) {
|
|
12
|
-
throw new Error('Not implemented');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 根据 ID 获取 Candidate
|
|
17
|
-
* @param {string} id
|
|
18
|
-
* @returns {Promise<Candidate|null>}
|
|
19
|
-
*/
|
|
20
|
-
async findById(id) {
|
|
21
|
-
throw new Error('Not implemented');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* 获取所有 Candidates
|
|
26
|
-
* @param {Object} filters
|
|
27
|
-
* @returns {Promise<Candidate[]>}
|
|
28
|
-
*/
|
|
29
|
-
async findAll(filters = {}) {
|
|
30
|
-
throw new Error('Not implemented');
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* 根据条件查询 Candidates
|
|
35
|
-
* @param {Object} query
|
|
36
|
-
* @returns {Promise<Candidate[]>}
|
|
37
|
-
*/
|
|
38
|
-
async query(query) {
|
|
39
|
-
throw new Error('Not implemented');
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* 更新 Candidate
|
|
44
|
-
* @param {string} id
|
|
45
|
-
* @param {Object} updates
|
|
46
|
-
* @returns {Promise<Candidate>}
|
|
47
|
-
*/
|
|
48
|
-
async update(id, updates) {
|
|
49
|
-
throw new Error('Not implemented');
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 删除 Candidate
|
|
54
|
-
* @param {string} id
|
|
55
|
-
* @returns {Promise<boolean>}
|
|
56
|
-
*/
|
|
57
|
-
async delete(id) {
|
|
58
|
-
throw new Error('Not implemented');
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 按状态查询
|
|
63
|
-
* @param {string} status
|
|
64
|
-
* @param {Object} pagination
|
|
65
|
-
* @returns {Promise<{candidates: Candidate[], total: number}>}
|
|
66
|
-
*/
|
|
67
|
-
async findByStatus(status, pagination = {}) {
|
|
68
|
-
throw new Error('Not implemented');
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* 按语言查询
|
|
73
|
-
* @param {string} language
|
|
74
|
-
* @returns {Promise<Candidate[]>}
|
|
75
|
-
*/
|
|
76
|
-
async findByLanguage(language) {
|
|
77
|
-
throw new Error('Not implemented');
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 按创建者查询
|
|
82
|
-
* @param {string} createdBy
|
|
83
|
-
* @returns {Promise<Candidate[]>}
|
|
84
|
-
*/
|
|
85
|
-
async findByCreatedBy(createdBy) {
|
|
86
|
-
throw new Error('Not implemented');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* 搜索 Candidates
|
|
91
|
-
* @param {string} keyword
|
|
92
|
-
* @returns {Promise<Candidate[]>}
|
|
93
|
-
*/
|
|
94
|
-
async search(keyword) {
|
|
95
|
-
throw new Error('Not implemented');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* 获取统计信息
|
|
100
|
-
* @returns {Promise<{total: number, byStatus: Object}>}
|
|
101
|
-
*/
|
|
102
|
-
async getStats() {
|
|
103
|
-
throw new Error('Not implemented');
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export default CandidateRepository;
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reasoning - 值对象,代表 AI 决策的理由
|
|
3
|
-
* 包含 Candidate 创建时的决策信息
|
|
4
|
-
*/
|
|
5
|
-
export class Reasoning {
|
|
6
|
-
constructor(props) {
|
|
7
|
-
this.whyStandard = props.whyStandard; // 为什么遵循标准
|
|
8
|
-
this.sources = props.sources || []; // 来源列表
|
|
9
|
-
this.qualitySignals = props.qualitySignals || {}; // 质量信号(如 clarity, reusability)
|
|
10
|
-
this.alternatives = props.alternatives || []; // 备选方案
|
|
11
|
-
this.confidence = props.confidence || 0.7; // 置信度 0-1(统一默认值)
|
|
12
|
-
this.generatedAt = props.generatedAt || new Date().toISOString();
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 验证推理信息的完整性
|
|
17
|
-
*/
|
|
18
|
-
isValid() {
|
|
19
|
-
return (
|
|
20
|
-
this.whyStandard &&
|
|
21
|
-
this.whyStandard.trim().length > 0 &&
|
|
22
|
-
Array.isArray(this.sources) &&
|
|
23
|
-
this.sources.length > 0 &&
|
|
24
|
-
typeof this.confidence === 'number' &&
|
|
25
|
-
this.confidence >= 0 &&
|
|
26
|
-
this.confidence <= 1
|
|
27
|
-
);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* 转换为 JSON
|
|
32
|
-
*/
|
|
33
|
-
toJSON() {
|
|
34
|
-
return {
|
|
35
|
-
whyStandard: this.whyStandard,
|
|
36
|
-
sources: this.sources,
|
|
37
|
-
qualitySignals: this.qualitySignals,
|
|
38
|
-
alternatives: this.alternatives,
|
|
39
|
-
confidence: this.confidence,
|
|
40
|
-
generatedAt: this.generatedAt,
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 从 JSON 创建 Reasoning
|
|
46
|
-
*/
|
|
47
|
-
static fromJSON(data) {
|
|
48
|
-
return new Reasoning(data);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export default Reasoning;
|