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.
Files changed (115) hide show
  1. package/README.md +12 -12
  2. package/bin/cli.js +53 -40
  3. package/config/constitution.yaml +9 -2
  4. package/dashboard/dist/assets/{icons-CH-H9x0E.js → icons-D4IWpDIk.js} +105 -100
  5. package/dashboard/dist/assets/index-CWBNcF9z.css +1 -0
  6. package/dashboard/dist/assets/index-DHtzhbuG.js +120 -0
  7. package/dashboard/dist/index.html +3 -3
  8. package/lib/cli/AiScanService.js +35 -36
  9. package/lib/cli/KnowledgeSyncService.js +345 -0
  10. package/lib/cli/SetupService.js +8 -26
  11. package/lib/cli/UpgradeService.js +28 -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 +289 -0
  15. package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
  16. package/lib/domain/knowledge/Lifecycle.js +99 -0
  17. package/lib/domain/knowledge/index.js +27 -0
  18. package/lib/domain/knowledge/values/Constraints.js +128 -0
  19. package/lib/domain/knowledge/values/Content.js +69 -0
  20. package/lib/domain/knowledge/values/Quality.js +81 -0
  21. package/lib/domain/knowledge/values/Reasoning.js +70 -0
  22. package/lib/domain/knowledge/values/Relations.js +142 -0
  23. package/lib/domain/knowledge/values/Stats.js +72 -0
  24. package/lib/domain/knowledge/values/index.js +9 -0
  25. package/lib/external/ai/AiProvider.js +85 -11
  26. package/lib/external/mcp/McpServer.js +7 -5
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +18 -2
  28. package/lib/external/mcp/handlers/bootstrap.js +116 -11
  29. package/lib/external/mcp/handlers/browse.js +76 -73
  30. package/lib/external/mcp/handlers/candidate.js +26 -275
  31. package/lib/external/mcp/handlers/guard.js +2 -0
  32. package/lib/external/mcp/handlers/knowledge.js +267 -0
  33. package/lib/external/mcp/handlers/structure.js +25 -23
  34. package/lib/external/mcp/handlers/system.js +10 -12
  35. package/lib/external/mcp/tools.js +134 -140
  36. package/lib/http/HttpServer.js +14 -8
  37. package/lib/http/routes/ai.js +4 -3
  38. package/lib/http/routes/extract.js +48 -4
  39. package/lib/http/routes/knowledge.js +246 -0
  40. package/lib/http/routes/search.js +12 -17
  41. package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
  42. package/lib/infrastructure/database/migrations/017_camelcase_knowledge_entries.js +107 -0
  43. package/lib/infrastructure/external/XcodeAutomation.js +187 -103
  44. package/lib/injection/ServiceContainer.js +69 -60
  45. package/lib/repository/knowledge/KnowledgeRepository.impl.js +338 -0
  46. package/lib/service/automation/DirectiveDetector.js +2 -3
  47. package/lib/service/automation/FileWatcher.js +59 -28
  48. package/lib/service/automation/XcodeIntegration.js +931 -156
  49. package/lib/service/automation/handlers/AlinkHandler.js +5 -4
  50. package/lib/service/automation/handlers/CreateHandler.js +53 -19
  51. package/lib/service/automation/handlers/DraftHandler.js +1 -1
  52. package/lib/service/automation/handlers/GuardHandler.js +183 -20
  53. package/lib/service/automation/handlers/SearchHandler.js +25 -22
  54. package/lib/service/candidate/SimilarityService.js +2 -2
  55. package/lib/service/chat/AnalystAgent.js +9 -0
  56. package/lib/service/chat/CandidateGuardrail.js +22 -11
  57. package/lib/service/chat/ChatAgent.js +132 -54
  58. package/lib/service/chat/ContextWindow.js +5 -5
  59. package/lib/service/chat/HandoffProtocol.js +1 -0
  60. package/lib/service/chat/ProducerAgent.js +40 -13
  61. package/lib/service/chat/ReasoningLayer.js +854 -0
  62. package/lib/service/chat/ReasoningTrace.js +329 -0
  63. package/lib/service/chat/tools.js +308 -205
  64. package/lib/service/cursor/CursorDeliveryPipeline.js +279 -0
  65. package/lib/service/cursor/KnowledgeCompressor.js +87 -0
  66. package/lib/service/cursor/RulesGenerator.js +168 -0
  67. package/lib/service/cursor/SkillsSyncer.js +268 -0
  68. package/lib/service/cursor/TokenBudget.js +58 -0
  69. package/lib/service/cursor/TopicClassifier.js +141 -0
  70. package/lib/service/guard/GuardCheckEngine.js +99 -10
  71. package/lib/service/guard/GuardService.js +57 -46
  72. package/lib/service/knowledge/ConfidenceRouter.js +159 -0
  73. package/lib/service/knowledge/KnowledgeFileWriter.js +595 -0
  74. package/lib/service/knowledge/KnowledgeService.js +802 -0
  75. package/lib/service/recipe/RecipeParser.js +3 -12
  76. package/lib/service/search/SearchEngine.js +67 -22
  77. package/lib/service/skills/SignalCollector.js +14 -9
  78. package/lib/service/skills/SkillAdvisor.js +13 -11
  79. package/lib/service/snippet/SnippetFactory.js +5 -5
  80. package/lib/service/spm/SpmService.js +15 -48
  81. package/lib/shared/RecipeReadinessChecker.js +6 -11
  82. package/package.json +1 -1
  83. package/scripts/install-cursor-skill.js +0 -6
  84. package/scripts/migrate-md-to-knowledge.mjs +364 -0
  85. package/skills/autosnippet-analysis/SKILL.md +15 -7
  86. package/skills/autosnippet-candidates/SKILL.md +8 -8
  87. package/skills/autosnippet-coldstart/SKILL.md +8 -4
  88. package/skills/autosnippet-concepts/SKILL.md +7 -6
  89. package/skills/autosnippet-create/SKILL.md +13 -13
  90. package/skills/autosnippet-intent/SKILL.md +3 -2
  91. package/skills/autosnippet-lifecycle/SKILL.md +5 -5
  92. package/skills/autosnippet-recipes/SKILL.md +18 -6
  93. package/templates/constitution.yaml +1 -1
  94. package/templates/copilot-instructions.md +6 -6
  95. package/templates/recipes-setup/README.md +3 -3
  96. package/dashboard/dist/assets/index-CqJRvYRL.js +0 -197
  97. package/dashboard/dist/assets/index-DICm9PNa.css +0 -1
  98. package/lib/cli/CandidateSyncService.js +0 -261
  99. package/lib/cli/SyncService.js +0 -356
  100. package/lib/domain/candidate/Candidate.js +0 -196
  101. package/lib/domain/candidate/CandidateRepository.js +0 -107
  102. package/lib/domain/candidate/Reasoning.js +0 -52
  103. package/lib/domain/recipe/Recipe.js +0 -421
  104. package/lib/domain/recipe/RecipeRepository.js +0 -54
  105. package/lib/domain/types/CandidateStatus.js +0 -52
  106. package/lib/http/routes/candidates.js +0 -559
  107. package/lib/http/routes/recipes.js +0 -397
  108. package/lib/repository/candidate/CandidateRepository.impl.js +0 -230
  109. package/lib/repository/recipe/RecipeRepository.impl.js +0 -498
  110. package/lib/service/candidate/CandidateAggregator.js +0 -52
  111. package/lib/service/candidate/CandidateFileWriter.js +0 -383
  112. package/lib/service/candidate/CandidateService.js +0 -1001
  113. package/lib/service/recipe/RecipeFileWriter.js +0 -514
  114. package/lib/service/recipe/RecipeService.js +0 -786
  115. package/lib/service/recipe/RecipeStatsTracker.js +0 -148
@@ -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;