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.
- package/README.md +5 -5
- package/bin/cli.js +5 -33
- package/config/constitution.yaml +9 -2
- package/dashboard/dist/assets/{icons-B_Xg4B-s.js → icons-BkT3XrKf.js} +105 -100
- package/dashboard/dist/assets/index-BsB7DzW4.css +1 -0
- package/dashboard/dist/assets/index-DdmQMrJJ.js +155 -0
- package/dashboard/dist/index.html +3 -3
- package/lib/cli/AiScanService.js +13 -11
- package/lib/cli/KnowledgeSyncService.js +343 -0
- package/lib/cli/SetupService.js +9 -27
- package/lib/core/ast/ProjectGraph.js +160 -0
- package/lib/core/gateway/GatewayActionRegistry.js +48 -58
- package/lib/domain/index.js +16 -11
- package/lib/domain/knowledge/KnowledgeEntry.js +351 -0
- package/lib/domain/knowledge/KnowledgeRepository.js +123 -0
- package/lib/domain/knowledge/Lifecycle.js +109 -0
- package/lib/domain/knowledge/index.js +27 -0
- package/lib/domain/knowledge/values/Constraints.js +125 -0
- package/lib/domain/knowledge/values/Content.js +86 -0
- package/lib/domain/knowledge/values/Quality.js +93 -0
- package/lib/domain/knowledge/values/Reasoning.js +69 -0
- package/lib/domain/knowledge/values/Relations.js +168 -0
- package/lib/domain/knowledge/values/Stats.js +87 -0
- package/lib/domain/knowledge/values/index.js +9 -0
- package/lib/external/ai/AiProvider.js +48 -0
- package/lib/external/ai/providers/GoogleGeminiProvider.js +12 -3
- package/lib/external/mcp/McpServer.js +7 -5
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +3 -2
- package/lib/external/mcp/handlers/bootstrap.js +121 -12
- package/lib/external/mcp/handlers/browse.js +77 -73
- package/lib/external/mcp/handlers/candidate.js +29 -276
- package/lib/external/mcp/handlers/guard.js +2 -0
- package/lib/external/mcp/handlers/knowledge.js +205 -0
- package/lib/external/mcp/handlers/skill.js +4 -2
- package/lib/external/mcp/handlers/structure.js +25 -23
- package/lib/external/mcp/handlers/system.js +10 -12
- package/lib/external/mcp/tools.js +125 -138
- package/lib/http/HttpServer.js +4 -8
- package/lib/http/middleware/requestLogger.js +3 -3
- package/lib/http/routes/ai.js +17 -1
- 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/http/routes/skills.js +44 -1
- package/lib/infrastructure/cache/GraphCache.js +143 -0
- package/lib/infrastructure/database/migrations/015_create_token_usage.js +27 -0
- package/lib/infrastructure/database/migrations/016_unified_knowledge_entries.js +395 -0
- package/lib/infrastructure/external/XcodeAutomation.js +187 -103
- package/lib/infrastructure/realtime/RealtimeService.js +14 -2
- package/lib/injection/ServiceContainer.js +164 -63
- package/lib/repository/knowledge/KnowledgeRepository.impl.js +373 -0
- package/lib/repository/token/TokenUsageStore.js +162 -0
- package/lib/service/automation/DirectiveDetector.js +2 -3
- package/lib/service/automation/FileWatcher.js +67 -28
- package/lib/service/automation/XcodeIntegration.js +931 -156
- package/lib/service/automation/handlers/AlinkHandler.js +6 -4
- package/lib/service/automation/handlers/CreateHandler.js +53 -18
- package/lib/service/automation/handlers/GuardHandler.js +183 -20
- package/lib/service/automation/handlers/SearchHandler.js +35 -17
- package/lib/service/chat/AnalystAgent.js +25 -14
- package/lib/service/chat/CandidateGuardrail.js +1 -1
- package/lib/service/chat/ChatAgent.js +280 -48
- package/lib/service/chat/ContextWindow.js +92 -8
- package/lib/service/chat/HandoffProtocol.js +26 -1
- package/lib/service/chat/ProducerAgent.js +11 -9
- package/lib/service/chat/tools.js +298 -194
- package/lib/service/guard/GuardCheckEngine.js +114 -10
- package/lib/service/guard/GuardService.js +59 -48
- package/lib/service/knowledge/ConfidenceRouter.js +159 -0
- package/lib/service/knowledge/KnowledgeFileWriter.js +602 -0
- package/lib/service/knowledge/KnowledgeService.js +725 -0
- package/lib/service/search/SearchEngine.js +92 -19
- package/lib/service/skills/SignalCollector.js +15 -9
- package/lib/service/skills/SkillAdvisor.js +13 -11
- package/lib/service/snippet/SnippetFactory.js +5 -5
- package/lib/service/spm/SpmService.js +119 -18
- 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 +6 -6
- package/skills/autosnippet-coldstart/SKILL.md +7 -3
- 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 +16 -4
- 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-CkIih2CC.css +0 -1
- package/dashboard/dist/assets/index-Duc8Qk-c.js +0 -197
- 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 -973
- package/lib/service/recipe/RecipeFileWriter.js +0 -514
- package/lib/service/recipe/RecipeService.js +0 -786
- 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
|
-
//
|
|
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
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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.
|
|
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
|
-
//
|
|
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
|
-
|
|
381
|
-
|
|
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
|
|
9
|
-
* Guard 规则 =
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
* 创建新规则 → 创建一个
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
53
|
+
lifecycle: 'active',
|
|
54
|
+
created_by: context.userId,
|
|
51
55
|
});
|
|
52
56
|
|
|
53
|
-
const created = await this.
|
|
57
|
+
const created = await this.knowledgeRepository.create(entry);
|
|
54
58
|
|
|
55
59
|
await this.auditLogger.log({
|
|
56
60
|
action: 'create_guard_rule',
|
|
57
|
-
resourceType: '
|
|
61
|
+
resourceType: 'knowledge_entry',
|
|
58
62
|
resourceId: created.id,
|
|
59
63
|
actor: context.userId,
|
|
60
|
-
details: `Created guard
|
|
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
|
-
* 启用规则(将
|
|
76
|
+
* 启用规则(将 lifecycle 设为 active)
|
|
73
77
|
*/
|
|
74
78
|
async enableRule(ruleId, context) {
|
|
75
79
|
try {
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
if (
|
|
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.
|
|
86
|
+
await this.knowledgeRepository.update(ruleId, { lifecycle: 'active' });
|
|
83
87
|
|
|
84
88
|
await this.auditLogger.log({
|
|
85
89
|
action: 'enable_guard_rule',
|
|
86
|
-
resourceType: '
|
|
90
|
+
resourceType: 'knowledge_entry',
|
|
87
91
|
resourceId: ruleId,
|
|
88
92
|
actor: context.userId,
|
|
89
|
-
details: `Enabled guard
|
|
93
|
+
details: `Enabled guard rule: ${entry.title}`,
|
|
90
94
|
timestamp: Math.floor(Date.now() / 1000),
|
|
91
95
|
});
|
|
92
96
|
|
|
93
|
-
return this.
|
|
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
|
-
* 禁用规则(将
|
|
105
|
+
* 禁用规则(将 lifecycle 设为 deprecated)
|
|
102
106
|
*/
|
|
103
107
|
async disableRule(ruleId, reason, context) {
|
|
104
108
|
try {
|
|
105
|
-
const
|
|
106
|
-
if (!
|
|
107
|
-
if (
|
|
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.
|
|
116
|
-
|
|
117
|
-
|
|
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: '
|
|
126
|
+
resourceType: 'knowledge_entry',
|
|
124
127
|
resourceId: ruleId,
|
|
125
128
|
actor: context.userId,
|
|
126
|
-
details: `Disabled guard
|
|
129
|
+
details: `Disabled guard rule: ${reason}`,
|
|
127
130
|
timestamp: Math.floor(Date.now() / 1000),
|
|
128
131
|
});
|
|
129
132
|
|
|
130
|
-
return this.
|
|
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
|
-
//
|
|
149
|
-
|
|
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
|
|
153
|
-
|
|
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:
|
|
160
|
-
ruleName:
|
|
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', {
|
|
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
|
-
*
|
|
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.
|
|
191
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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;
|