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.
Files changed (110) hide show
  1. package/README.md +5 -5
  2. package/bin/cli.js +5 -33
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
  5. package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
  6. package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +13 -11
  9. package/lib/cli/KnowledgeSyncService.js +343 -0
  10. package/lib/cli/SetupService.js +9 -27
  11. package/lib/core/ast/ProjectGraph.js +160 -0
  12. package/lib/core/gateway/GatewayActionRegistry.js +48 -58
  13. package/lib/domain/index.js +16 -11
  14. package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +109 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +125 -0
  19. package/lib/domain/knowledge/values/Content.js +86 -0
  20. package/lib/domain/knowledge/values/Quality.js +93 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +69 -0
  22. package/lib/domain/knowledge/values/Relations.js +168 -0
  23. package/lib/domain/knowledge/values/Stats.js +87 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +48 -0
  26. package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
  27. package/lib/external/mcp/McpServer.js +7 -5
  28. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
  29. package/lib/external/mcp/handlers/bootstrap.js +121 -12
  30. package/lib/external/mcp/handlers/browse.js +77 -73
  31. package/lib/external/mcp/handlers/candidate.js +29 -276
  32. package/lib/external/mcp/handlers/guard.js +2 -0
  33. package/lib/external/mcp/handlers/knowledge.js +205 -0
  34. package/lib/external/mcp/handlers/skill.js +4 -2
  35. package/lib/external/mcp/handlers/structure.js +25 -23
  36. package/lib/external/mcp/handlers/system.js +10 -12
  37. package/lib/external/mcp/tools.js +125 -138
  38. package/lib/http/HttpServer.js +4 -8
  39. package/lib/http/middleware/requestLogger.js +3 -3
  40. package/lib/http/routes/ai.js +17 -1
  41. package/lib/http/routes/extract.js +48 -4
  42. package/lib/http/routes/knowledge.js +246 -0
  43. package/lib/http/routes/search.js +12 -17
  44. package/lib/http/routes/skills.js +44 -1
  45. package/lib/infrastructure/cache/GraphCache.js +143 -0
  46. package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
  47. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  48. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  49. package/lib/infrastructure/realtime/RealtimeService.js +14 -2
  50. package/lib/injection/ServiceContainer.js +164 -63
  51. package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
  52. package/lib/repository/token/TokenUsageStore.js +162 -0
  53. package/lib/service/automation/DirectiveDetector.js +2 -3
  54. package/lib/service/automation/FileWatcher.js +67 -28
  55. package/lib/service/automation/XcodeIntegration.js +931 -156
  56. package/lib/service/automation/handlers/AlinkHandler.js +6 -4
  57. package/lib/service/automation/handlers/CreateHandler.js +53 -18
  58. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  59. package/lib/service/automation/handlers/SearchHandler.js +35 -17
  60. package/lib/service/chat/AnalystAgent.js +25 -14
  61. package/lib/service/chat/CandidateGuardrail.js +1 -1
  62. package/lib/service/chat/ChatAgent.js +280 -48
  63. package/lib/service/chat/ContextWindow.js +92 -8
  64. package/lib/service/chat/HandoffProtocol.js +26 -1
  65. package/lib/service/chat/ProducerAgent.js +11 -9
  66. package/lib/service/chat/tools.js +298 -194
  67. package/lib/service/guard/GuardCheckEngine.js +114 -10
  68. package/lib/service/guard/GuardService.js +59 -48
  69. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  70. package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
  71. package/lib/service/knowledge/KnowledgeService.js +725 -0
  72. package/lib/service/search/SearchEngine.js +92 -19
  73. package/lib/service/skills/SignalCollector.js +15 -9
  74. package/lib/service/skills/SkillAdvisor.js +13 -11
  75. package/lib/service/snippet/SnippetFactory.js +5 -5
  76. package/lib/service/spm/SpmService.js +119 -18
  77. package/package.json +1 -1
  78. package/scripts/install-cursor-skill.js +0 -6
  79. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  80. package/skills/autosnippet-analysis/SKILL.md +15 -7
  81. package/skills/autosnippet-candidates/SKILL.md +6 -6
  82. package/skills/autosnippet-coldstart/SKILL.md +7 -3
  83. package/skills/autosnippet-concepts/SKILL.md +7 -6
  84. package/skills/autosnippet-create/SKILL.md +13 -13
  85. package/skills/autosnippet-intent/SKILL.md +3 -2
  86. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  87. package/skills/autosnippet-recipes/SKILL.md +16 -4
  88. package/templates/constitution.yaml +1 -1
  89. package/templates/copilot-instructions.md +6 -6
  90. package/templates/recipes-setup/README.md +3 -3
  91. package/dashboard/dist/assets/index-CkIih2CC.css +0 -1
  92. package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
  93. package/lib/cli/CandidateSyncService.js +0 -261
  94. package/lib/cli/SyncService.js +0 -356
  95. package/lib/domain/candidate/Candidate.js +0 -196
  96. package/lib/domain/candidate/CandidateRepository.js +0 -107
  97. package/lib/domain/candidate/Reasoning.js +0 -52
  98. package/lib/domain/recipe/Recipe.js +0 -421
  99. package/lib/domain/recipe/RecipeRepository.js +0 -54
  100. package/lib/domain/types/CandidateStatus.js +0 -52
  101. package/lib/http/routes/candidates.js +0 -559
  102. package/lib/http/routes/recipes.js +0 -397
  103. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  104. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  105. package/lib/service/candidate/CandidateAggregator.js +0 -52
  106. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  107. package/lib/service/candidate/CandidateService.js +0 -973
  108. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  109. package/lib/service/recipe/RecipeService.js +0 -786
  110. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -1,261 +0,0 @@
1
- /**
2
- * CandidateSyncService — 将 AutoSnippet/candidates/*.md 增量同步到 SQLite DB
3
- *
4
- * 设计原则(与 RecipeSyncService 对齐):
5
- * - .md 文件 = 完整唯一数据源(Source of Truth),DB = 索引缓存
6
- * - 通过 _contentHash 检测手写/手改 .md → 进入违规统计
7
- * - 孤儿 Candidate(DB 有但 .md 不存在)→ 自动标记(仅报告,不删除)
8
- *
9
- * 使用方式:
10
- * - CLI: `asd sync` 同时同步 Recipes + Candidates
11
- * - 内部: SetupService 委托调用
12
- */
13
-
14
- import fs from 'node:fs';
15
- import path from 'node:path';
16
- import { randomUUID } from 'node:crypto';
17
- import { CANDIDATES_DIR } from '../infrastructure/config/Defaults.js';
18
- import { computeCandidateHash, parseCandidateMarkdown } from '../service/candidate/CandidateFileWriter.js';
19
- import Logger from '../infrastructure/logging/Logger.js';
20
-
21
- export class CandidateSyncService {
22
- /**
23
- * @param {string} projectRoot
24
- */
25
- constructor(projectRoot) {
26
- this.projectRoot = projectRoot;
27
- this.candidatesDir = path.join(projectRoot, CANDIDATES_DIR);
28
- this.logger = Logger.getInstance();
29
- }
30
-
31
- /**
32
- * 执行增量同步:.md → DB
33
- * @param {import('better-sqlite3').Database} db better-sqlite3 原始句柄
34
- * @param {object} [opts={}]
35
- * @param {boolean} [opts.dryRun=false]
36
- * @param {boolean} [opts.force=false]
37
- * @param {boolean} [opts.skipViolations=false]
38
- * @returns {{ synced: number, created: number, updated: number, violations: string[], orphaned: string[], skipped: number }}
39
- */
40
- sync(db, opts = {}) {
41
- const { dryRun = false, force = false, skipViolations = false } = opts;
42
-
43
- const report = {
44
- synced: 0,
45
- created: 0,
46
- updated: 0,
47
- violations: [],
48
- orphaned: [],
49
- skipped: 0,
50
- };
51
-
52
- // ── 1. 收集 .md 文件 ──
53
- const mdFiles = this._collectMdFiles();
54
- if (mdFiles.length === 0) {
55
- this.logger.info('CandidateSyncService: no .md files found in candidates/');
56
- return report;
57
- }
58
-
59
- // ── 2. 准备 upsert 语句 ──
60
- const upsertStmt = dryRun ? null : this._prepareUpsert(db);
61
- const auditStmt = (dryRun || skipViolations) ? null : this._prepareAuditInsert(db);
62
-
63
- // ── 3. 逐文件同步 ──
64
- const syncedIds = new Set();
65
-
66
- for (const { absPath, relPath } of mdFiles) {
67
- try {
68
- const content = fs.readFileSync(absPath, 'utf8');
69
- const parsed = parseCandidateMarkdown(content, relPath);
70
-
71
- if (!parsed.id) {
72
- this.logger.warn(`CandidateSyncService: skip file without id — ${relPath}`);
73
- report.skipped++;
74
- continue;
75
- }
76
-
77
- syncedIds.add(parsed.id);
78
-
79
- // ── 检测手动编辑 ──
80
- const actualHash = computeCandidateHash(content);
81
- const storedHash = parsed._contentHash;
82
- const isManualEdit = storedHash && storedHash !== actualHash && !force;
83
-
84
- if (isManualEdit) {
85
- report.violations.push(relPath);
86
- if (auditStmt) {
87
- this._logViolation(auditStmt, parsed.id, relPath, storedHash, actualHash);
88
- }
89
- }
90
-
91
- // ── upsert ──
92
- if (!dryRun) {
93
- const existed = this._candidateExists(db, parsed.id);
94
- const row = this._buildDbRow(parsed, content);
95
- upsertStmt.run(...Object.values(row));
96
-
97
- if (existed) {
98
- report.updated++;
99
- } else {
100
- report.created++;
101
- }
102
- }
103
-
104
- report.synced++;
105
- } catch (err) {
106
- this.logger.error(`CandidateSyncService: failed to sync ${relPath}`, { error: err.message });
107
- report.skipped++;
108
- }
109
- }
110
-
111
- // ── 4. 检测孤儿 ──
112
- report.orphaned = this._detectOrphans(db, syncedIds, dryRun);
113
-
114
- this.logger.info('CandidateSyncService: sync complete', {
115
- synced: report.synced,
116
- created: report.created,
117
- updated: report.updated,
118
- violations: report.violations.length,
119
- orphaned: report.orphaned.length,
120
- skipped: report.skipped,
121
- });
122
-
123
- return report;
124
- }
125
-
126
- /* ═══ 文件收集 ═══════════════════════════════════════════ */
127
-
128
- _collectMdFiles() {
129
- if (!fs.existsSync(this.candidatesDir)) return [];
130
-
131
- const results = [];
132
- const walk = (dir, base) => {
133
- for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
134
- const full = path.join(dir, entry.name);
135
- const rel = base ? `${base}/${entry.name}` : entry.name;
136
-
137
- if (entry.isDirectory()) {
138
- walk(full, rel);
139
- } else if (entry.name.endsWith('.md') && !entry.name.startsWith('_')) {
140
- results.push({ absPath: full, relPath: rel });
141
- }
142
- }
143
- };
144
- walk(this.candidatesDir, '');
145
- return results;
146
- }
147
-
148
- /* ═══ DB 操作 ═══════════════════════════════════════════ */
149
-
150
- _buildDbRow(parsed, rawContent) {
151
- // 从 body 提取代码块
152
- const code = parsed._bodyCode || '';
153
-
154
- // 重建 JSON 字段
155
- const reasoning = parsed._reasoning || null;
156
- const metadata = parsed._metadata || {};
157
- const history = parsed._statusHistory || '[]';
158
-
159
- return {
160
- id: parsed.id,
161
- code: code,
162
- language: parsed.language || 'swift',
163
- category: parsed.category || 'general',
164
- source: parsed.source || 'manual',
165
- reasoning_json: typeof reasoning === 'object' ? JSON.stringify(reasoning) : (reasoning || null),
166
- status: parsed.status || 'pending',
167
- status_history_json: typeof history === 'object' ? JSON.stringify(history) : (history || '[]'),
168
- approved_by: parsed.approvedBy || null,
169
- approved_at: parsed.approvedAt || null,
170
- rejected_by: parsed.rejectedBy || null,
171
- rejection_reason: parsed.rejectionReason || null,
172
- applied_recipe_id: parsed.appliedRecipeId || null,
173
- metadata_json: typeof metadata === 'object' ? JSON.stringify(metadata) : (metadata || '{}'),
174
- created_by: parsed.createdBy || 'file-sync',
175
- created_at: parsed.createdAt || Math.floor(Date.now() / 1000),
176
- updated_at: parsed.updatedAt || Math.floor(Date.now() / 1000),
177
- };
178
- }
179
-
180
- _prepareUpsert(db) {
181
- const cols = [
182
- 'id', 'code', 'language', 'category', 'source',
183
- 'reasoning_json', 'status', 'status_history_json',
184
- 'approved_by', 'approved_at', 'rejected_by', 'rejection_reason',
185
- 'applied_recipe_id', 'metadata_json',
186
- 'created_by', 'created_at', 'updated_at',
187
- ];
188
-
189
- const updateCols = cols.filter(c => !['id', 'created_by', 'created_at'].includes(c));
190
- const setClauses = updateCols.map(c => `${c} = excluded.${c}`).join(',\n ');
191
-
192
- const sql = `
193
- INSERT INTO candidates (${cols.join(', ')})
194
- VALUES (${cols.map(() => '?').join(', ')})
195
- ON CONFLICT(id) DO UPDATE SET
196
- ${setClauses}
197
- `;
198
-
199
- return db.prepare(sql);
200
- }
201
-
202
- _candidateExists(db, id) {
203
- const row = db.prepare('SELECT 1 FROM candidates WHERE id = ?').get(id);
204
- return !!row;
205
- }
206
-
207
- /* ═══ 违规记录 ═══════════════════════════════════════════ */
208
-
209
- _prepareAuditInsert(db) {
210
- try {
211
- return db.prepare(`
212
- INSERT INTO audit_logs (id, timestamp, actor, actor_context, action, resource, operation_data, result, error_message, duration)
213
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
214
- `);
215
- } catch {
216
- return null;
217
- }
218
- }
219
-
220
- _logViolation(stmt, candidateId, filePath, expectedHash, actualHash) {
221
- try {
222
- stmt.run(
223
- randomUUID(),
224
- Math.floor(Date.now() / 1000),
225
- 'sync',
226
- JSON.stringify({ source: 'cli' }),
227
- 'manual_candidate_edit',
228
- candidateId,
229
- JSON.stringify({ file: filePath, expectedHash, actualHash }),
230
- 'violation_detected',
231
- null,
232
- 0,
233
- );
234
- } catch (err) {
235
- this.logger.warn('CandidateSyncService: failed to log violation', { candidateId, error: err.message });
236
- }
237
- }
238
-
239
- /* ═══ 孤儿检测 ═══════════════════════════════════════════ */
240
-
241
- /**
242
- * 检测 DB 中存在但 .md 已删除的 Candidate(仅报告,不自动删除)
243
- */
244
- _detectOrphans(db, syncedIds, _dryRun) {
245
- const orphanIds = [];
246
- try {
247
- const rows = db.prepare(
248
- `SELECT id FROM candidates WHERE status NOT IN ('applied', 'rejected')`
249
- ).all();
250
-
251
- for (const row of rows) {
252
- if (!syncedIds.has(row.id)) {
253
- orphanIds.push(row.id);
254
- }
255
- }
256
- } catch (err) {
257
- this.logger.warn('CandidateSyncService: orphan detection failed', { error: err.message });
258
- }
259
- return orphanIds;
260
- }
261
- }
@@ -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
- }