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
@@ -237,18 +237,33 @@ export class GuardCheckEngine {
237
237
  getRules(language = null) {
238
238
  let rules = [];
239
239
 
240
- // 从数据库加载自定义规则(kind='rule' 的 Recipe,覆盖 code-standard/code-style/best-practice/boundary-constraint)
240
+ // 从数据库加载自定义规则
241
+ // 优先从 knowledge_entries 表查询(V3),回退到 recipes 表(V2)
241
242
  try {
242
243
  const now = Date.now();
243
244
  if (!this._customRulesCache || now - this._cacheTime > this._cacheTTL) {
244
- const rows = this.db.prepare(
245
- `SELECT id, title, description, language, scope, constraints_json
246
- FROM recipes WHERE (kind = 'rule' OR knowledge_type = 'boundary-constraint') AND status = 'active'`
247
- ).all();
245
+ let rows = [];
246
+ try {
247
+ // V3: knowledge_entries
248
+ rows = this.db.prepare(
249
+ `SELECT id, title, description, language, scope, constraints
250
+ FROM knowledge_entries
251
+ WHERE (kind = 'rule' OR knowledge_type = 'boundary-constraint')
252
+ AND lifecycle = 'active'`
253
+ ).all();
254
+ } catch {
255
+ // V3 表不存在,回退到 V2 recipes 表
256
+ try {
257
+ rows = this.db.prepare(
258
+ `SELECT id, title, description, language, scope, constraints_json AS constraints
259
+ FROM recipes WHERE (kind = 'rule' OR knowledge_type = 'boundary-constraint') AND status = 'active'`
260
+ ).all();
261
+ } catch { /* neither table exists */ }
262
+ }
248
263
  this._customRulesCache = rows.map(r => {
249
264
  let guards = [];
250
265
  try {
251
- const constraints = JSON.parse(r.constraints_json || '{}');
266
+ const constraints = JSON.parse(r.constraints || '{}');
252
267
  guards = constraints.guards || [];
253
268
  } catch { /* ignore */ }
254
269
  // Each guard entry becomes a rule
@@ -267,7 +282,7 @@ export class GuardCheckEngine {
267
282
  }
268
283
  rules.push(...this._customRulesCache);
269
284
  } catch {
270
- // recipes table or knowledge_type column may not exist
285
+ // table or column may not exist
271
286
  }
272
287
 
273
288
  // 合并内置规则(不覆盖同名数据库规则)
@@ -377,9 +392,22 @@ export class GuardCheckEngine {
377
392
  hitMap.set(v.ruleId, count + 1);
378
393
  }
379
394
 
380
- const updateStmt = this.db.prepare(
381
- `UPDATE recipes SET guard_hit_count = guard_hit_count + ?, updated_at = ? WHERE id = ?`
382
- );
395
+ // V3: guard_hit_count 已合并到 stats JSON,使用 json_set 更新
396
+ let updateStmt;
397
+ try {
398
+ updateStmt = this.db.prepare(
399
+ `UPDATE knowledge_entries
400
+ SET stats = json_set(COALESCE(stats, '{}'), '$.guard_hits',
401
+ COALESCE(json_extract(stats, '$.guard_hits'), 0) + ?),
402
+ updated_at = ?
403
+ WHERE id = ?`
404
+ );
405
+ } catch {
406
+ // V3 表不存在,回退到 V2
407
+ updateStmt = this.db.prepare(
408
+ `UPDATE recipes SET guard_hit_count = guard_hit_count + ?, updated_at = ? WHERE id = ?`
409
+ );
410
+ }
383
411
  const now = Math.floor(Date.now() / 1000);
384
412
 
385
413
  for (const [ruleId, count] of hitMap) {
@@ -464,6 +492,9 @@ export class GuardCheckEngine {
464
492
 
465
493
  /**
466
494
  * 批量文件审计
495
+ * @param {Array<{path: string, content: string}>} files
496
+ * @param {object} options - {scope: 'file'|'target'|'project'}
497
+ * @returns {{files, summary, crossFileViolations}}
467
498
  */
468
499
  auditFiles(files, options = {}) {
469
500
  const results = [];
@@ -477,8 +508,14 @@ export class GuardCheckEngine {
477
508
  totalErrors += result.summary.errors;
478
509
  }
479
510
 
511
+ // ── 跨文件检查 ──
512
+ const crossFileViolations = this._runCrossFileChecks(files);
513
+ totalViolations += crossFileViolations.length;
514
+ totalErrors += crossFileViolations.filter(v => v.severity === 'error').length;
515
+
480
516
  return {
481
517
  files: results,
518
+ crossFileViolations,
482
519
  summary: {
483
520
  filesChecked: results.length,
484
521
  totalViolations,
@@ -488,6 +525,73 @@ export class GuardCheckEngine {
488
525
  };
489
526
  }
490
527
 
528
+ /**
529
+ * 跨文件检查 — 需要多文件上下文才能发现的问题
530
+ * @param {Array<{path: string, content: string}>} files
531
+ * @returns {Array<{ruleId, message, severity, locations}>}
532
+ */
533
+ _runCrossFileChecks(files) {
534
+ const violations = [];
535
+
536
+ // ── ObjC Category 跨文件重名检查 ──
537
+ // 收集所有文件中的 @interface ClassName(CategoryName) 声明
538
+ const categoryMap = new Map(); // key: "ClassName(CategoryName)" → [{filePath, line, snippet}]
539
+ const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g;
540
+
541
+ for (const { path: filePath, content } of files) {
542
+ const ext = filePath.split('.').pop()?.toLowerCase();
543
+ if (ext !== 'm' && ext !== 'mm' && ext !== 'h') continue;
544
+
545
+ const lines = content.split(/\r?\n/);
546
+ for (let i = 0; i < lines.length; i++) {
547
+ categoryRegex.lastIndex = 0;
548
+ let m;
549
+ while ((m = categoryRegex.exec(lines[i])) !== null) {
550
+ const key = `${m[1]}(${m[2]})`;
551
+ if (!categoryMap.has(key)) categoryMap.set(key, []);
552
+ categoryMap.get(key).push({
553
+ filePath,
554
+ line: i + 1,
555
+ snippet: lines[i].trim().slice(0, 120),
556
+ });
557
+ }
558
+ }
559
+ }
560
+
561
+ // .h 和 .m 成对出现是正常的(声明 + 实现),只有同类型文件重名才是问题
562
+ // 或者超过 2 处声明就一定有问题
563
+ for (const [key, locations] of categoryMap) {
564
+ if (locations.length <= 1) continue;
565
+
566
+ // 按文件扩展名分组: .h 和 .m/.mm 各一个是合法的
567
+ const hFiles = locations.filter(l => l.filePath.endsWith('.h'));
568
+ const mFiles = locations.filter(l => !l.filePath.endsWith('.h'));
569
+
570
+ // 同类型文件中有多个声明 → 重名冲突
571
+ const hasDuplicateH = hFiles.length > 1;
572
+ const hasDuplicateM = mFiles.length > 1;
573
+ // 超过 2 处总声明(如 3 个文件都声明了同一个 Category)→ 一定有问题
574
+ const tooMany = locations.length > 2;
575
+
576
+ if (hasDuplicateH || hasDuplicateM || tooMany) {
577
+ // 收集冲突的那些位置
578
+ const conflictLocations = tooMany ? locations
579
+ : hasDuplicateH && hasDuplicateM ? locations
580
+ : hasDuplicateH ? hFiles
581
+ : mFiles;
582
+
583
+ violations.push({
584
+ ruleId: 'objc-cross-file-duplicate-category',
585
+ message: `Category ${key} 在 ${conflictLocations.length} 个文件中重复声明,可能导致方法覆盖或未定义行为`,
586
+ severity: 'warning',
587
+ locations: conflictLocations,
588
+ });
589
+ }
590
+ }
591
+
592
+ return violations;
593
+ }
594
+
491
595
  /**
492
596
  * 清除规则缓存
493
597
  */
@@ -1,36 +1,40 @@
1
- import { Recipe, RecipeStatus, KnowledgeType } from '../../domain/index.js';
2
1
  import Logger from '../../infrastructure/logging/Logger.js';
3
2
  import { ValidationError, ConflictError, NotFoundError } from '../../shared/errors/index.js';
4
3
  import { v4 as uuidv4 } from 'uuid';
5
4
 
6
5
  /**
7
6
  * GuardService
8
- * 管理 Guard 约束规则的生命周期(现在统一存储在 Recipe 实体中)
9
- * Guard 规则 = knowledgeType: 'boundary-constraint' 的 Recipe,
7
+ * 管理 Guard 约束规则的生命周期 (V3: 使用 KnowledgeEntry / knowledgeRepository)
8
+ * Guard 规则 = kind='rule' + knowledge_type='boundary-constraint' 的 KnowledgeEntry,
10
9
  * 具体 pattern 存在 constraints.guards[] 里
11
10
  */
12
11
  export class GuardService {
13
- constructor(recipeRepository, auditLogger, gateway) {
14
- this.recipeRepository = recipeRepository;
12
+ /**
13
+ * @param {import('../../domain/knowledge/KnowledgeRepository.js').KnowledgeRepository} knowledgeRepository
14
+ */
15
+ constructor(knowledgeRepository, auditLogger, gateway) {
16
+ this.knowledgeRepository = knowledgeRepository;
15
17
  this.auditLogger = auditLogger;
16
18
  this.gateway = gateway;
17
19
  this.logger = Logger.getInstance();
18
20
  }
19
21
 
20
22
  /**
21
- * 创建新规则 → 创建一个 knowledgeType=boundary-constraint 的 Recipe
23
+ * 创建新规则 → 创建一个 kind=rule, knowledge_type=boundary-constraint 的 KnowledgeEntry
22
24
  */
23
25
  async createRule(data, context) {
24
26
  try {
25
27
  this._validateCreateInput(data);
26
28
 
27
- const recipe = new Recipe({
29
+ const { KnowledgeEntry } = await import('../../domain/knowledge/KnowledgeEntry.js');
30
+ const entry = KnowledgeEntry.fromJSON({
28
31
  id: uuidv4(),
29
32
  title: data.name,
30
33
  description: data.description,
31
34
  language: (data.languages || [])[0] || '',
32
35
  category: data.category || 'guard',
33
- knowledgeType: KnowledgeType.BOUNDARY_CONSTRAINT,
36
+ kind: 'rule',
37
+ knowledge_type: 'boundary-constraint',
34
38
  content: {
35
39
  pattern: data.pattern || '',
36
40
  rationale: data.note || data.sourceReason || '',
@@ -38,7 +42,7 @@ export class GuardService {
38
42
  constraints: {
39
43
  boundaries: [],
40
44
  preconditions: [],
41
- sideEffects: [],
45
+ side_effects: [],
42
46
  guards: [{
43
47
  pattern: data.pattern,
44
48
  severity: data.severity || 'warning',
@@ -46,18 +50,18 @@ export class GuardService {
46
50
  }],
47
51
  },
48
52
  tags: data.languages || [],
49
- status: RecipeStatus.ACTIVE,
50
- createdBy: context.userId,
53
+ lifecycle: 'active',
54
+ created_by: context.userId,
51
55
  });
52
56
 
53
- const created = await this.recipeRepository.create(recipe);
57
+ const created = await this.knowledgeRepository.create(entry);
54
58
 
55
59
  await this.auditLogger.log({
56
60
  action: 'create_guard_rule',
57
- resourceType: 'recipe',
61
+ resourceType: 'knowledge_entry',
58
62
  resourceId: created.id,
59
63
  actor: context.userId,
60
- details: `Created guard recipe: ${data.name}`,
64
+ details: `Created guard rule: ${data.name}`,
61
65
  timestamp: Math.floor(Date.now() / 1000),
62
66
  });
63
67
 
@@ -69,28 +73,28 @@ export class GuardService {
69
73
  }
70
74
 
71
75
  /**
72
- * 启用规则(将 Recipe 状态设为 ACTIVE
76
+ * 启用规则(将 lifecycle 设为 active
73
77
  */
74
78
  async enableRule(ruleId, context) {
75
79
  try {
76
- const recipe = await this.recipeRepository.findById(ruleId);
77
- if (!recipe) throw new NotFoundError('Guard recipe not found', 'recipe', ruleId);
78
- if (recipe.status === RecipeStatus.ACTIVE) {
80
+ const entry = await this.knowledgeRepository.findById(ruleId);
81
+ if (!entry) throw new NotFoundError('Guard rule not found', 'knowledge_entry', ruleId);
82
+ if (entry.lifecycle === 'active') {
79
83
  throw new ConflictError('Rule is already enabled', 'Cannot enable an already enabled rule');
80
84
  }
81
85
 
82
- await this.recipeRepository.update(ruleId, { status: RecipeStatus.ACTIVE });
86
+ await this.knowledgeRepository.update(ruleId, { lifecycle: 'active' });
83
87
 
84
88
  await this.auditLogger.log({
85
89
  action: 'enable_guard_rule',
86
- resourceType: 'recipe',
90
+ resourceType: 'knowledge_entry',
87
91
  resourceId: ruleId,
88
92
  actor: context.userId,
89
- details: `Enabled guard recipe: ${recipe.title}`,
93
+ details: `Enabled guard rule: ${entry.title}`,
90
94
  timestamp: Math.floor(Date.now() / 1000),
91
95
  });
92
96
 
93
- return this.recipeRepository.findById(ruleId);
97
+ return this.knowledgeRepository.findById(ruleId);
94
98
  } catch (error) {
95
99
  this.logger.error('Error enabling guard rule', { ruleId, error: error.message });
96
100
  throw error;
@@ -98,13 +102,13 @@ export class GuardService {
98
102
  }
99
103
 
100
104
  /**
101
- * 禁用规则(将 Recipe 状态设为 DEPRECATED
105
+ * 禁用规则(将 lifecycle 设为 deprecated
102
106
  */
103
107
  async disableRule(ruleId, reason, context) {
104
108
  try {
105
- const recipe = await this.recipeRepository.findById(ruleId);
106
- if (!recipe) throw new NotFoundError('Guard recipe not found', 'recipe', ruleId);
107
- if (recipe.status === RecipeStatus.DEPRECATED) {
109
+ const entry = await this.knowledgeRepository.findById(ruleId);
110
+ if (!entry) throw new NotFoundError('Guard rule not found', 'knowledge_entry', ruleId);
111
+ if (entry.lifecycle === 'deprecated') {
108
112
  throw new ConflictError('Rule is already disabled', 'Cannot disable an already disabled rule');
109
113
  }
110
114
 
@@ -112,22 +116,21 @@ export class GuardService {
112
116
  throw new ValidationError('Disable reason is required');
113
117
  }
114
118
 
115
- await this.recipeRepository.update(ruleId, {
116
- status: RecipeStatus.DEPRECATED,
117
- deprecation_reason: reason,
118
- deprecated_at: Math.floor(Date.now() / 1000),
119
+ await this.knowledgeRepository.update(ruleId, {
120
+ lifecycle: 'deprecated',
121
+ rejection_reason: reason,
119
122
  });
120
123
 
121
124
  await this.auditLogger.log({
122
125
  action: 'disable_guard_rule',
123
- resourceType: 'recipe',
126
+ resourceType: 'knowledge_entry',
124
127
  resourceId: ruleId,
125
128
  actor: context.userId,
126
- details: `Disabled guard recipe: ${reason}`,
129
+ details: `Disabled guard rule: ${reason}`,
127
130
  timestamp: Math.floor(Date.now() / 1000),
128
131
  });
129
132
 
130
- return this.recipeRepository.findById(ruleId);
133
+ return this.knowledgeRepository.findById(ruleId);
131
134
  } catch (error) {
132
135
  this.logger.error('Error disabling guard rule', { ruleId, error: error.message });
133
136
  throw error;
@@ -136,6 +139,7 @@ export class GuardService {
136
139
 
137
140
  /**
138
141
  * 检查代码是否匹配 Guard 规则
142
+ * 查询所有 active 的 rule 实体的 constraints.guards[]
139
143
  */
140
144
  async checkCode(code, options = {}) {
141
145
  try {
@@ -145,19 +149,27 @@ export class GuardService {
145
149
 
146
150
  const { language = null } = options;
147
151
 
148
- // 获取所有包含 guards active Recipe
149
- const guardRecipes = await this.recipeRepository.findWithGuards(language);
152
+ // V3: 使用 findActiveRules() 查询 kind='rule' + lifecycle='active'
153
+ let guardEntries = await this.knowledgeRepository.findActiveRules();
154
+
155
+ // 按语言过滤
156
+ if (language) {
157
+ guardEntries = guardEntries.filter(
158
+ e => !e.language || e.language === language
159
+ );
160
+ }
150
161
 
151
162
  const matches = [];
152
- for (const recipe of guardRecipes) {
153
- for (const guard of (recipe.constraints?.guards || [])) {
163
+ for (const entry of guardEntries) {
164
+ const guards = entry.constraints?.guards || [];
165
+ for (const guard of guards) {
154
166
  try {
155
167
  const regex = new RegExp(guard.pattern, 'gm');
156
168
  const codeMatches = [...code.matchAll(regex)];
157
169
  if (codeMatches.length > 0) {
158
170
  matches.push({
159
- ruleId: recipe.id,
160
- ruleName: recipe.title,
171
+ ruleId: entry.id,
172
+ ruleName: entry.title,
161
173
  severity: guard.severity || 'warning',
162
174
  message: guard.message || '',
163
175
  matches: codeMatches.map(m => ({
@@ -169,7 +181,7 @@ export class GuardService {
169
181
  });
170
182
  }
171
183
  } catch (e) {
172
- this.logger.warn('Error matching guard pattern', { recipeId: recipe.id, error: e.message });
184
+ this.logger.warn('Error matching guard pattern', { entryId: entry.id, error: e.message });
173
185
  }
174
186
  }
175
187
  }
@@ -182,13 +194,14 @@ export class GuardService {
182
194
  }
183
195
 
184
196
  /**
185
- * 查询规则列表(boundary-constraint Recipes)
197
+ * 查询规则列表 (kind='rule' + knowledge_type='boundary-constraint')
186
198
  */
187
199
  async listRules(filters = {}, pagination = {}) {
188
200
  try {
189
201
  const { page = 1, pageSize = 20 } = pagination;
190
- return this.recipeRepository.findByKnowledgeType(
191
- KnowledgeType.BOUNDARY_CONSTRAINT, { page, pageSize }
202
+ return this.knowledgeRepository.findWithPagination(
203
+ { kind: 'rule', knowledge_type: 'boundary-constraint' },
204
+ { page, pageSize }
192
205
  );
193
206
  } catch (error) {
194
207
  this.logger.error('Error listing rules', { error: error.message, filters });
@@ -202,10 +215,9 @@ export class GuardService {
202
215
  async searchRules(keyword, pagination = {}) {
203
216
  try {
204
217
  const { page = 1, pageSize = 20 } = pagination;
205
- // 搜索所有 Recipes 然后过滤 boundary-constraint
206
- const result = await this.recipeRepository.search(keyword, { page, pageSize });
218
+ const result = await this.knowledgeRepository.search(keyword, { page, pageSize });
207
219
  result.data = (result.data || []).filter(
208
- r => r.knowledgeType === KnowledgeType.BOUNDARY_CONSTRAINT
220
+ r => r.kind === 'rule' && r.knowledge_type === 'boundary-constraint'
209
221
  );
210
222
  result.total = result.data.length;
211
223
  return result;
@@ -220,8 +232,7 @@ export class GuardService {
220
232
  */
221
233
  async getRuleStats() {
222
234
  try {
223
- // 基于 recipes 表的 boundary-constraint 统计
224
- return this.recipeRepository.getStats();
235
+ return this.knowledgeRepository.getStats();
225
236
  } catch (error) {
226
237
  this.logger.error('Error getting rule stats', { error: error.message });
227
238
  throw error;
@@ -0,0 +1,159 @@
1
+ import Logger from '../../infrastructure/logging/Logger.js';
2
+
3
+ /**
4
+ * ConfidenceRouter — 知识条目自动审核路由器
5
+ *
6
+ * 根据 KnowledgeEntry 的 reasoning.confidence、质量评分、
7
+ * 内容完整性等信号判断是否可自动审核通过。
8
+ *
9
+ * 路由结果:
10
+ * auto_approve — 置信度高、内容完整,自动通过 + fastTrack
11
+ * pending — 需要人工审核
12
+ * reject — 置信度过低或不满足基本要求
13
+ */
14
+
15
+ const DEFAULT_CONFIG = {
16
+ /** 自动通过的最低 confidence 阈值 */
17
+ autoApproveThreshold: 0.85,
18
+ /** 自动驳回的 confidence 阈值 */
19
+ rejectThreshold: 0.2,
20
+ /** 需要的最少内容字符数 */
21
+ minContentLength: 20,
22
+ /** 自动通过要求 reasoning.isValid() */
23
+ requireReasoning: true,
24
+ /** 来源白名单(这些来源可以适用更宽松的阈值) */
25
+ trustedSources: ['bootstrap', 'cursor-scan'],
26
+ /** 可信来源的自动通过阈值 */
27
+ trustedAutoApproveThreshold: 0.7,
28
+ };
29
+
30
+ export class ConfidenceRouter {
31
+ /**
32
+ * @param {Object} [config] - 路由配置
33
+ * @param {import('../quality/QualityScorer.js').QualityScorer} [qualityScorer]
34
+ */
35
+ constructor(config = {}, qualityScorer = null) {
36
+ this._config = { ...DEFAULT_CONFIG, ...config };
37
+ this._qualityScorer = qualityScorer;
38
+ this.logger = Logger.getInstance();
39
+ }
40
+
41
+ /**
42
+ * 路由决策
43
+ * @param {import('../../domain/knowledge/KnowledgeEntry.js').KnowledgeEntry} entry
44
+ * @returns {Promise<{ action: 'auto_approve'|'pending'|'reject', reason: string, confidence?: number }>}
45
+ */
46
+ async route(entry) {
47
+ const confidence = entry.reasoning?.confidence ?? 0;
48
+ const source = entry.source || 'manual';
49
+ const isTrusted = this._config.trustedSources.includes(source);
50
+
51
+ // ── 阶段 1: 基本过滤 — 内容不完整直接 pending ──
52
+ if (!entry.isValid()) {
53
+ return {
54
+ action: 'pending',
55
+ reason: 'Content incomplete (title or content missing)',
56
+ confidence,
57
+ };
58
+ }
59
+
60
+ // ── 阶段 2: 低置信度驳回 ──
61
+ if (confidence < this._config.rejectThreshold && confidence > 0) {
62
+ return {
63
+ action: 'reject',
64
+ reason: `Confidence too low: ${confidence.toFixed(2)} < ${this._config.rejectThreshold}`,
65
+ confidence,
66
+ };
67
+ }
68
+
69
+ // ── 阶段 3: 内容最短长度检查 ──
70
+ const contentLength = this._estimateContentLength(entry);
71
+ if (contentLength < this._config.minContentLength) {
72
+ return {
73
+ action: 'pending',
74
+ reason: `Content too short: ${contentLength} chars < ${this._config.minContentLength}`,
75
+ confidence,
76
+ };
77
+ }
78
+
79
+ // ── 阶段 4: Reasoning 检查 ──
80
+ if (this._config.requireReasoning && !entry.reasoning?.isValid?.()) {
81
+ // 无 reasoning 不驳回,但进入人工审核
82
+ return {
83
+ action: 'pending',
84
+ reason: 'Reasoning not provided or invalid',
85
+ confidence,
86
+ };
87
+ }
88
+
89
+ // ── 阶段 5: 质量评分(可选) ──
90
+ let qualityScore = null;
91
+ if (this._qualityScorer) {
92
+ try {
93
+ const scorerInput = {
94
+ title: entry.title,
95
+ trigger: entry.trigger,
96
+ code: entry.content?.pattern || entry.content?.markdown || '',
97
+ language: entry.language,
98
+ category: entry.category,
99
+ summary: entry.summaryCn || entry.summaryEn || '',
100
+ usageGuide: entry.usageGuideCn || entry.usageGuideEn || '',
101
+ headers: entry.headers || [],
102
+ tags: entry.tags || [],
103
+ };
104
+ const result = this._qualityScorer.score(scorerInput);
105
+ qualityScore = result.score;
106
+ } catch {
107
+ // 评分失败不阻塞路由
108
+ }
109
+ }
110
+
111
+ // ── 阶段 6: 自动通过判定 ──
112
+ const threshold = isTrusted
113
+ ? this._config.trustedAutoApproveThreshold
114
+ : this._config.autoApproveThreshold;
115
+
116
+ if (confidence >= threshold) {
117
+ // 如果有质量评分且太低,降级到 pending
118
+ if (qualityScore !== null && qualityScore < 0.3) {
119
+ return {
120
+ action: 'pending',
121
+ reason: `Confidence OK (${confidence.toFixed(2)}) but quality low (${qualityScore.toFixed(2)})`,
122
+ confidence,
123
+ };
124
+ }
125
+
126
+ return {
127
+ action: 'auto_approve',
128
+ reason: `Confidence ${confidence.toFixed(2)} >= threshold ${threshold} (source: ${source})`,
129
+ confidence,
130
+ };
131
+ }
132
+
133
+ // ── 默认: 需要人工审核 ──
134
+ return {
135
+ action: 'pending',
136
+ reason: `Confidence ${confidence.toFixed(2)} < threshold ${threshold}`,
137
+ confidence,
138
+ };
139
+ }
140
+
141
+ /**
142
+ * 估算内容长度
143
+ */
144
+ _estimateContentLength(entry) {
145
+ const content = entry.content;
146
+ if (!content) return 0;
147
+
148
+ const parts = [
149
+ content.pattern,
150
+ content.rationale,
151
+ content.markdown,
152
+ ...(content.steps || []).map(s => typeof s === 'string' ? s : s?.description || ''),
153
+ ].filter(Boolean);
154
+
155
+ return parts.reduce((sum, p) => sum + p.length, 0);
156
+ }
157
+ }
158
+
159
+ export default ConfidenceRouter;