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
|
@@ -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-DHtzhbuG.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-D4IWpDIk.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-CWBNcF9z.css">
|
|
16
16
|
</head>
|
|
17
17
|
<body>
|
|
18
18
|
<div id="root"></div>
|
package/lib/cli/AiScanService.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AiScanService — `asd ais [Target]` 的核心逻辑
|
|
3
3
|
*
|
|
4
|
-
* 按文件粒度扫描 Target 源码,调用 AI Provider 提取 Recipe
|
|
5
|
-
*
|
|
4
|
+
* 按文件粒度扫描 Target 源码,调用 AI Provider 提取 Recipe,
|
|
5
|
+
* 创建后自动发布(PENDING → ACTIVE),无需 Dashboard 人工审核。
|
|
6
6
|
*
|
|
7
7
|
* 与 bootstrap.js 的区别:
|
|
8
8
|
* - bootstrap 是纯启发式(正则),本服务全程使用 LLM
|
|
9
|
-
* - bootstrap 输出 9 条概要
|
|
9
|
+
* - bootstrap 输出 9 条概要 Recipe,本服务按文件输出细粒度 Recipe
|
|
10
10
|
* - 本服务可脱离 MCP 独立在 CLI 运行
|
|
11
11
|
*/
|
|
12
12
|
|
|
@@ -29,14 +29,14 @@ export class AiScanService {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* 扫描指定 Target(或全部 Target
|
|
32
|
+
* 扫描指定 Target(或全部 Target)的源文件并提取 Recipe,创建后直接发布
|
|
33
33
|
* @param {string|null} targetName Target 名称;null 时扫描全部
|
|
34
34
|
* @param {object} opts { maxFiles, dryRun, concurrency }
|
|
35
|
-
* @returns {{
|
|
35
|
+
* @returns {{ published: number, files: number, errors: string[] }}
|
|
36
36
|
*/
|
|
37
37
|
async scan(targetName, opts = {}) {
|
|
38
38
|
const { maxFiles = 200, dryRun = false } = opts;
|
|
39
|
-
const report = {
|
|
39
|
+
const report = { published: 0, files: 0, errors: [], skipped: 0 };
|
|
40
40
|
|
|
41
41
|
// 1. 初始化 AI Provider
|
|
42
42
|
try {
|
|
@@ -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) {
|
|
@@ -85,43 +85,42 @@ export class AiScanService {
|
|
|
85
85
|
continue;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// 4.
|
|
88
|
+
// 4. 创建并发布 Recipe — AI 输出完整 V3 结构直透
|
|
89
89
|
for (const recipe of recipes) {
|
|
90
|
-
if (!recipe.
|
|
90
|
+
if (!recipe.content?.pattern || recipe.content.pattern.length < 20) continue;
|
|
91
91
|
|
|
92
92
|
if (dryRun) {
|
|
93
|
-
report.
|
|
93
|
+
report.published++;
|
|
94
94
|
continue;
|
|
95
95
|
}
|
|
96
96
|
|
|
97
97
|
try {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
report.candidates++;
|
|
98
|
+
// 来源标记(非 AI 职责)
|
|
99
|
+
recipe.source = 'ai-scan';
|
|
100
|
+
recipe.tags = [...new Set([...(recipe.tags || []), 'ai-scan', file.targetName])];
|
|
101
|
+
|
|
102
|
+
// V3 字段注入:moduleName + sourceFile
|
|
103
|
+
recipe.moduleName = file.targetName;
|
|
104
|
+
recipe.sourceFile = file.relativePath || file.name;
|
|
105
|
+
|
|
106
|
+
// 7.3.9 aiInsight — AI 已输出则直透,否则从 description 生成
|
|
107
|
+
if (!recipe.aiInsight && recipe.description) {
|
|
108
|
+
recipe.aiInsight = recipe.description;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const saved = await knowledgeService.create(recipe, { userId: 'ai-scan' });
|
|
112
|
+
|
|
113
|
+
// QualityScorer 自动评分
|
|
114
|
+
try {
|
|
115
|
+
await knowledgeService.updateQuality(saved.id, { userId: 'ai-scan' });
|
|
116
|
+
} catch { /* best effort */ }
|
|
117
|
+
|
|
118
|
+
// 直接发布:PENDING → ACTIVE
|
|
119
|
+
await knowledgeService.publish(saved.id, { userId: 'ai-scan' });
|
|
120
|
+
|
|
121
|
+
report.published++;
|
|
123
122
|
} catch (err) {
|
|
124
|
-
report.errors.push(`${file.name}:
|
|
123
|
+
report.errors.push(`${file.name}: recipe publish failed — ${err.message}`);
|
|
125
124
|
}
|
|
126
125
|
}
|
|
127
126
|
} catch (err) {
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* KnowledgeSyncService — 将 .md 文件增量同步到 SQLite DB(knowledge_entries 表)
|
|
3
|
+
*
|
|
4
|
+
* 统一替代 SyncService (Recipe) + CandidateSyncService。
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:
|
|
7
|
+
* - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
|
|
8
|
+
* - 通过 contentHash 检测手写/手改 .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.contentHash;
|
|
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: parsed.trigger || '',
|
|
185
|
+
description: parsed.description || '',
|
|
186
|
+
lifecycle: parsed.lifecycle || 'pending',
|
|
187
|
+
lifecycleHistory: JSON.stringify(parsed.lifecycleHistory || []),
|
|
188
|
+
autoApprovable: parsed.autoApprovable ? 1 : 0,
|
|
189
|
+
language: parsed.language || 'swift',
|
|
190
|
+
category: parsed.category || 'general',
|
|
191
|
+
kind: parsed.kind || 'pattern',
|
|
192
|
+
knowledgeType: parsed.knowledgeType || 'code-pattern',
|
|
193
|
+
complexity: parsed.complexity || 'intermediate',
|
|
194
|
+
scope: parsed.scope || 'universal',
|
|
195
|
+
difficulty: parsed.difficulty || null,
|
|
196
|
+
tags: JSON.stringify(parsed.tags || []),
|
|
197
|
+
content: JSON.stringify(parsed.content || {}),
|
|
198
|
+
relations: JSON.stringify(parsed.relations || {}),
|
|
199
|
+
constraints: JSON.stringify(parsed.constraints || {}),
|
|
200
|
+
reasoning: JSON.stringify(parsed.reasoning || {}),
|
|
201
|
+
quality: JSON.stringify(parsed.quality || {}),
|
|
202
|
+
stats: JSON.stringify(parsed.stats || {}),
|
|
203
|
+
headers: JSON.stringify(parsed.headers || []),
|
|
204
|
+
headerPaths: JSON.stringify(parsed.headerPaths || []),
|
|
205
|
+
moduleName: parsed.moduleName || '',
|
|
206
|
+
includeHeaders: parsed.includeHeaders ? 1 : 0,
|
|
207
|
+
topicHint: parsed.topicHint || null,
|
|
208
|
+
whenClause: parsed.whenClause || null,
|
|
209
|
+
doClause: parsed.doClause || null,
|
|
210
|
+
dontClause: parsed.dontClause || null,
|
|
211
|
+
coreCode: parsed.coreCode || null,
|
|
212
|
+
agentNotes: parsed.agentNotes ? JSON.stringify(parsed.agentNotes) : null,
|
|
213
|
+
aiInsight: parsed.aiInsight || null,
|
|
214
|
+
reviewedBy: parsed.reviewedBy || null,
|
|
215
|
+
reviewedAt: parsed.reviewedAt || null,
|
|
216
|
+
rejectionReason: parsed.rejectionReason || null,
|
|
217
|
+
source: parsed.source || 'file-sync',
|
|
218
|
+
sourceFile: relPath,
|
|
219
|
+
sourceCandidateId: parsed.sourceCandidateId || null,
|
|
220
|
+
createdBy: parsed.createdBy || 'file-sync',
|
|
221
|
+
createdAt: parsed.createdAt || now,
|
|
222
|
+
updatedAt: parsed.updatedAt || now,
|
|
223
|
+
publishedAt: parsed.publishedAt || null,
|
|
224
|
+
publishedBy: parsed.publishedBy || null,
|
|
225
|
+
contentHash: contentHash,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* 准备 upsert 语句(INSERT ... ON CONFLICT DO UPDATE 全字段)
|
|
231
|
+
*/
|
|
232
|
+
_prepareUpsert(db) {
|
|
233
|
+
const cols = [
|
|
234
|
+
'id', 'title', 'trigger', 'description',
|
|
235
|
+
'lifecycle', 'lifecycleHistory', 'autoApprovable',
|
|
236
|
+
'language', 'category', 'kind', 'knowledgeType', 'complexity', 'scope', 'difficulty',
|
|
237
|
+
'tags',
|
|
238
|
+
'content', 'relations', 'constraints', 'reasoning', 'quality', 'stats',
|
|
239
|
+
'headers', 'headerPaths', 'moduleName', 'includeHeaders',
|
|
240
|
+
'topicHint', 'whenClause', 'doClause', 'dontClause', 'coreCode',
|
|
241
|
+
'agentNotes', 'aiInsight',
|
|
242
|
+
'reviewedBy', 'reviewedAt', 'rejectionReason',
|
|
243
|
+
'source', 'sourceFile', 'sourceCandidateId',
|
|
244
|
+
'createdBy', 'createdAt', 'updatedAt',
|
|
245
|
+
'publishedAt', 'publishedBy',
|
|
246
|
+
'contentHash',
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
// ON CONFLICT 更新除 id, createdBy, createdAt 以外的所有列
|
|
250
|
+
const updateCols = cols.filter(c => !['id', 'createdBy', 'createdAt'].includes(c));
|
|
251
|
+
const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
|
|
252
|
+
|
|
253
|
+
const sql = `
|
|
254
|
+
INSERT INTO knowledge_entries (${cols.join(', ')})
|
|
255
|
+
VALUES (${cols.map(() => '?').join(', ')})
|
|
256
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
257
|
+
${setClauses}
|
|
258
|
+
`;
|
|
259
|
+
|
|
260
|
+
return db.prepare(sql);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* 检查 entry 是否已存在于 DB
|
|
265
|
+
*/
|
|
266
|
+
_entryExists(db, id) {
|
|
267
|
+
const row = db.prepare('SELECT 1 FROM knowledge_entries WHERE id = ?').get(id);
|
|
268
|
+
return !!row;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/* ═══ 违规记录 ═══════════════════════════════════════════ */
|
|
272
|
+
|
|
273
|
+
_prepareAuditInsert(db) {
|
|
274
|
+
try {
|
|
275
|
+
return db.prepare(`
|
|
276
|
+
INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
|
|
277
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
278
|
+
`);
|
|
279
|
+
} catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
_logViolation(stmt, entryId, filePath, expectedHash, actualHash) {
|
|
285
|
+
try {
|
|
286
|
+
stmt.run(
|
|
287
|
+
randomUUID(),
|
|
288
|
+
Math.floor(Date.now() / 1000),
|
|
289
|
+
'sync',
|
|
290
|
+
JSON.stringify({ source: 'cli' }),
|
|
291
|
+
'manual_knowledge_edit',
|
|
292
|
+
entryId,
|
|
293
|
+
JSON.stringify({ file: filePath, expectedHash, actualHash }),
|
|
294
|
+
'violation_detected',
|
|
295
|
+
null,
|
|
296
|
+
0,
|
|
297
|
+
);
|
|
298
|
+
} catch (err) {
|
|
299
|
+
this.logger.warn('KnowledgeSyncService: failed to log violation', {
|
|
300
|
+
entryId,
|
|
301
|
+
error: err.message,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/* ═══ 孤儿检测 ═══════════════════════════════════════════ */
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* 检测 DB 中存在但 .md 已删除的 Entry → 标记 deprecated
|
|
310
|
+
* @returns {string[]} 孤儿 entry id 列表
|
|
311
|
+
*/
|
|
312
|
+
_detectOrphans(db, syncedIds, dryRun) {
|
|
313
|
+
const orphanIds = [];
|
|
314
|
+
try {
|
|
315
|
+
const rows = db.prepare(
|
|
316
|
+
`SELECT id, sourceFile FROM knowledge_entries
|
|
317
|
+
WHERE lifecycle NOT IN ('deprecated')
|
|
318
|
+
AND sourceFile IS NOT NULL`
|
|
319
|
+
).all();
|
|
320
|
+
|
|
321
|
+
for (const row of rows) {
|
|
322
|
+
if (!syncedIds.has(row.id)) {
|
|
323
|
+
orphanIds.push(row.id);
|
|
324
|
+
if (!dryRun) {
|
|
325
|
+
const now = Math.floor(Date.now() / 1000);
|
|
326
|
+
db.prepare(
|
|
327
|
+
`UPDATE knowledge_entries
|
|
328
|
+
SET lifecycle = 'deprecated',
|
|
329
|
+
rejectionReason = ?,
|
|
330
|
+
updatedAt = ?
|
|
331
|
+
WHERE id = ?`
|
|
332
|
+
).run('source file deleted (orphan)', now, row.id);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (err) {
|
|
337
|
+
this.logger.warn('KnowledgeSyncService: orphan detection failed', {
|
|
338
|
+
error: err.message,
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
return orphanIds;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
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
|
|
|
@@ -163,6 +163,34 @@ export class UpgradeService {
|
|
|
163
163
|
mkdirSync(destDir, { recursive: true });
|
|
164
164
|
copyFileSync(src, dest);
|
|
165
165
|
console.log(' ✅ .cursor/rules/autosnippet-conventions.mdc');
|
|
166
|
+
|
|
167
|
+
// 动态生成 4 通道交付物料(如 ServiceContainer 已初始化)
|
|
168
|
+
this._triggerCursorDelivery();
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 触发 Cursor Delivery Pipeline 动态生成
|
|
173
|
+
* 非阻塞 — 失败不影响 upgrade 流程
|
|
174
|
+
*/
|
|
175
|
+
_triggerCursorDelivery() {
|
|
176
|
+
import('../injection/ServiceContainer.js')
|
|
177
|
+
.then(({ getServiceContainer }) => {
|
|
178
|
+
const container = getServiceContainer();
|
|
179
|
+
if (container.services.cursorDeliveryPipeline) {
|
|
180
|
+
const pipeline = container.get('cursorDeliveryPipeline');
|
|
181
|
+
pipeline.deliver()
|
|
182
|
+
.then(result => {
|
|
183
|
+
console.log(` ✅ Cursor Delivery: ${result.channelA.rulesCount} rules, ` +
|
|
184
|
+
`${result.channelB.topicCount} topics, ${result.channelC.synced} skills`);
|
|
185
|
+
})
|
|
186
|
+
.catch(err => {
|
|
187
|
+
console.log(` ⚠️ Cursor Delivery 跳过: ${err.message}`);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
.catch(() => {
|
|
192
|
+
// ServiceContainer 未初始化 — 正常(upgrade 可能在无 DB 环境执行)
|
|
193
|
+
});
|
|
166
194
|
}
|
|
167
195
|
|
|
168
196
|
/* ═══ Copilot Instructions ══════════════════════════ */
|