autosnippet 3.3.4 → 3.3.5
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 +174 -83
- package/config/constitution.yaml +2 -0
- package/dist/lib/cli/KnowledgeSyncService.d.ts +5 -1
- package/dist/lib/cli/KnowledgeSyncService.js +5 -2
- package/dist/lib/domain/knowledge/values/Stats.d.ts +1 -1
- package/dist/lib/domain/knowledge/values/Stats.js +2 -2
- package/dist/lib/external/mcp/handlers/consolidated.js +178 -0
- package/dist/lib/external/mcp/tools.js +2 -1
- package/dist/lib/injection/modules/InfraModule.js +4 -1
- package/dist/lib/injection/modules/KnowledgeModule.js +23 -0
- package/dist/lib/repository/evolution/ProposalRepository.d.ts +99 -0
- package/dist/lib/repository/evolution/ProposalRepository.js +255 -0
- package/dist/lib/service/bootstrap/UiStartupTasks.d.ts +17 -4
- package/dist/lib/service/bootstrap/UiStartupTasks.js +53 -5
- package/dist/lib/service/evolution/DecayDetector.d.ts +4 -3
- package/dist/lib/service/evolution/DecayDetector.js +97 -22
- package/dist/lib/service/evolution/KnowledgeMetabolism.d.ts +4 -2
- package/dist/lib/service/evolution/KnowledgeMetabolism.js +29 -2
- package/dist/lib/service/evolution/ProposalExecutor.d.ts +62 -0
- package/dist/lib/service/evolution/ProposalExecutor.js +360 -0
- package/dist/lib/service/evolution/StagingManager.js +5 -3
- package/dist/lib/service/guard/GuardCrossFileChecks.js +2 -0
- package/dist/lib/service/guard/ReverseGuard.d.ts +1 -1
- package/dist/lib/service/guard/ReverseGuard.js +32 -2
- package/dist/lib/service/knowledge/SourceRefReconciler.d.ts +2 -0
- package/dist/lib/service/knowledge/SourceRefReconciler.js +48 -0
- package/dist/lib/shared/schemas/mcp-tools.d.ts +1 -0
- package/dist/lib/shared/schemas/mcp-tools.js +4 -0
- package/package.json +1 -1
|
@@ -220,6 +220,7 @@ export async function enhancedSubmitKnowledge(ctx, args) {
|
|
|
220
220
|
const source = args.source || 'mcp';
|
|
221
221
|
const dimensionId = args.dimensionId;
|
|
222
222
|
const clientId = args.client_id;
|
|
223
|
+
const supersedes = args.supersedes;
|
|
223
224
|
// ── Step 1: 限流 ──
|
|
224
225
|
const { checkRecipeSave } = await import('#http/middleware/RateLimiter.js');
|
|
225
226
|
const { resolveProjectRoot } = await import('#shared/resolveProjectRoot.js');
|
|
@@ -283,6 +284,7 @@ export async function enhancedSubmitKnowledge(ctx, args) {
|
|
|
283
284
|
// ── Step 3: 融合分析(统一对所有有效条目运行) ──
|
|
284
285
|
const submittableItems = [];
|
|
285
286
|
const blockedItems = [];
|
|
287
|
+
const createdProposals = [];
|
|
286
288
|
if (skipConsolidation) {
|
|
287
289
|
submittableItems.push(...validItems);
|
|
288
290
|
}
|
|
@@ -307,11 +309,37 @@ export async function enhancedSubmitKnowledge(ctx, args) {
|
|
|
307
309
|
content: v.item.content,
|
|
308
310
|
}));
|
|
309
311
|
const batchAdvice = advisor.analyzeBatch(candidates);
|
|
312
|
+
// 尝试获取 ProposalRepository 以创建 Proposal(降级容忍)
|
|
313
|
+
let proposalRepo = null;
|
|
314
|
+
try {
|
|
315
|
+
proposalRepo = ctx.container.get('proposalRepository') ?? null;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
/* ProposalRepository 未注册,降级为旧的 blocked 模式 */
|
|
319
|
+
}
|
|
310
320
|
for (const { index: adviceIdx, advice } of batchAdvice.items) {
|
|
311
321
|
const validEntry = validItems[adviceIdx];
|
|
312
322
|
if (advice.action === 'create') {
|
|
313
323
|
submittableItems.push(validEntry);
|
|
314
324
|
}
|
|
325
|
+
else if (proposalRepo &&
|
|
326
|
+
(advice.action === 'merge' ||
|
|
327
|
+
advice.action === 'reorganize' ||
|
|
328
|
+
advice.action === 'insufficient')) {
|
|
329
|
+
// 创建 Proposal 而非简单 block — 系统后续自动处理
|
|
330
|
+
const proposal = _createProposalFromAdvice(proposalRepo, advice, validEntry.item);
|
|
331
|
+
if (proposal) {
|
|
332
|
+
createdProposals.push(proposal);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
// Proposal 创建失败(可能去重)→ 仍作为 blocked 返回
|
|
336
|
+
blockedItems.push({
|
|
337
|
+
index: validEntry.index,
|
|
338
|
+
title: validEntry.item.title || '(untitled)',
|
|
339
|
+
consolidation: advice,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
}
|
|
315
343
|
else {
|
|
316
344
|
blockedItems.push({
|
|
317
345
|
index: validEntry.index,
|
|
@@ -390,6 +418,63 @@ export async function enhancedSubmitKnowledge(ctx, args) {
|
|
|
390
418
|
});
|
|
391
419
|
}
|
|
392
420
|
}
|
|
421
|
+
// ── Step 4b: Supersede 提案创建 ──
|
|
422
|
+
// 当 Agent 声明 supersedes 旧 Recipe 时,创建 supersede Proposal
|
|
423
|
+
if (supersedes && successIds.length > 0) {
|
|
424
|
+
let proposalRepo = null;
|
|
425
|
+
try {
|
|
426
|
+
proposalRepo = ctx.container.get('proposalRepository') ?? null;
|
|
427
|
+
}
|
|
428
|
+
catch {
|
|
429
|
+
/* ProposalRepository 未注册,跳过 */
|
|
430
|
+
}
|
|
431
|
+
if (proposalRepo) {
|
|
432
|
+
// 验证旧 Recipe 存在
|
|
433
|
+
const oldRecipeExists = (() => {
|
|
434
|
+
try {
|
|
435
|
+
const db = ctx.container.get('database');
|
|
436
|
+
if (!db) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
const rawDb = db.getDb();
|
|
440
|
+
const row = rawDb
|
|
441
|
+
.prepare('SELECT id FROM knowledge_entries WHERE id = ?')
|
|
442
|
+
.get(supersedes);
|
|
443
|
+
return row !== undefined;
|
|
444
|
+
}
|
|
445
|
+
catch {
|
|
446
|
+
return false;
|
|
447
|
+
}
|
|
448
|
+
})();
|
|
449
|
+
if (oldRecipeExists) {
|
|
450
|
+
const proposal = proposalRepo.create({
|
|
451
|
+
type: 'supersede',
|
|
452
|
+
targetRecipeId: supersedes,
|
|
453
|
+
relatedRecipeIds: successIds,
|
|
454
|
+
confidence: 0.8,
|
|
455
|
+
source: 'ide-agent',
|
|
456
|
+
description: `Agent 声明新 Recipe [${successIds.join(', ')}] 替代旧 Recipe [${supersedes}]。观察窗口内将对比新旧表现。`,
|
|
457
|
+
evidence: [
|
|
458
|
+
{
|
|
459
|
+
snapshotAt: Date.now(),
|
|
460
|
+
newRecipeIds: successIds,
|
|
461
|
+
declaredBy: 'agent',
|
|
462
|
+
},
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
if (proposal) {
|
|
466
|
+
createdProposals.push({
|
|
467
|
+
proposalId: proposal.id,
|
|
468
|
+
type: 'supersede',
|
|
469
|
+
targetRecipe: { id: supersedes, title: supersedes },
|
|
470
|
+
status: proposal.status,
|
|
471
|
+
expiresAt: proposal.expiresAt,
|
|
472
|
+
message: `已创建替代提案:新 Recipe 将在观察窗口到期后自动替代旧 Recipe [${supersedes}]。`,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
393
478
|
// ── Step 5: 构建统一响应 ──
|
|
394
479
|
const data = {
|
|
395
480
|
count: successCount,
|
|
@@ -417,6 +502,13 @@ export async function enhancedSubmitKnowledge(ctx, args) {
|
|
|
417
502
|
message: `${blockedItems.length} 条因融合分析被阻塞(与已有 Recipe 重叠或实质性不足)。设 skipConsolidation: true 可跳过。`,
|
|
418
503
|
};
|
|
419
504
|
}
|
|
505
|
+
if (createdProposals.length > 0) {
|
|
506
|
+
data.proposals = createdProposals;
|
|
507
|
+
data.proposalSummary = {
|
|
508
|
+
proposalCount: createdProposals.length,
|
|
509
|
+
message: `${createdProposals.length} 条已创建进化提案,系统将在观察窗口到期后自动执行。无需额外操作。`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
420
512
|
const allOk = successCount === items.length;
|
|
421
513
|
return envelope({
|
|
422
514
|
success: successCount > 0,
|
|
@@ -467,3 +559,89 @@ function _trackRejection(ctx, item, dimensionId) {
|
|
|
467
559
|
/* best effort */
|
|
468
560
|
}
|
|
469
561
|
}
|
|
562
|
+
// ── Proposal 创建辅助函数 ───────────────────────────
|
|
563
|
+
/**
|
|
564
|
+
* 将 ConsolidationAdvisor 分析结果转为 evolution_proposals 记录。
|
|
565
|
+
*
|
|
566
|
+
* merge → Proposal(type: merge, target: 已有 Recipe)
|
|
567
|
+
* reorganize → Proposal(type: reorganize, 高风险 → pending 等开发者确认)
|
|
568
|
+
* insufficient → Proposal(type: enhance, target: 最相似 Recipe)
|
|
569
|
+
*/
|
|
570
|
+
function _createProposalFromAdvice(repo, advice, candidateItem) {
|
|
571
|
+
const evidence = [
|
|
572
|
+
{
|
|
573
|
+
snapshotAt: Date.now(),
|
|
574
|
+
candidateTitle: candidateItem.title,
|
|
575
|
+
candidateCategory: candidateItem.category,
|
|
576
|
+
analysisReason: advice.reason,
|
|
577
|
+
mergeDirection: advice.mergeDirection,
|
|
578
|
+
},
|
|
579
|
+
];
|
|
580
|
+
if (advice.action === 'merge' && advice.targetRecipe) {
|
|
581
|
+
const proposal = repo.create({
|
|
582
|
+
type: 'merge',
|
|
583
|
+
targetRecipeId: advice.targetRecipe.id,
|
|
584
|
+
confidence: advice.confidence,
|
|
585
|
+
source: 'ide-agent',
|
|
586
|
+
description: advice.reason,
|
|
587
|
+
evidence,
|
|
588
|
+
});
|
|
589
|
+
if (!proposal) {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
return {
|
|
593
|
+
proposalId: proposal.id,
|
|
594
|
+
type: 'merge',
|
|
595
|
+
targetRecipe: { id: advice.targetRecipe.id, title: advice.targetRecipe.title },
|
|
596
|
+
status: proposal.status,
|
|
597
|
+
expiresAt: proposal.expiresAt,
|
|
598
|
+
message: `已为「${advice.targetRecipe.title}」创建融合提案,${proposal.status === 'observing' ? '观察窗口 72h 后自动执行' : '等待开发者确认'}。`,
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
if (advice.action === 'reorganize' && advice.reorganizeTargets?.length) {
|
|
602
|
+
const target = advice.reorganizeTargets[0];
|
|
603
|
+
const proposal = repo.create({
|
|
604
|
+
type: 'reorganize',
|
|
605
|
+
targetRecipeId: target.id,
|
|
606
|
+
relatedRecipeIds: advice.reorganizeTargets.slice(1).map((t) => t.id),
|
|
607
|
+
confidence: advice.confidence,
|
|
608
|
+
source: 'ide-agent',
|
|
609
|
+
description: advice.reason,
|
|
610
|
+
evidence,
|
|
611
|
+
});
|
|
612
|
+
if (!proposal) {
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return {
|
|
616
|
+
proposalId: proposal.id,
|
|
617
|
+
type: 'reorganize',
|
|
618
|
+
targetRecipe: { id: target.id, title: target.title },
|
|
619
|
+
status: proposal.status,
|
|
620
|
+
expiresAt: proposal.expiresAt,
|
|
621
|
+
message: `已为 ${advice.reorganizeTargets.length} 条 Recipe 创建重组提案,需开发者在 Dashboard 确认。`,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
if (advice.action === 'insufficient' && advice.coveredBy?.length) {
|
|
625
|
+
const target = advice.coveredBy[0];
|
|
626
|
+
const proposal = repo.create({
|
|
627
|
+
type: 'enhance',
|
|
628
|
+
targetRecipeId: target.id,
|
|
629
|
+
confidence: advice.confidence,
|
|
630
|
+
source: 'ide-agent',
|
|
631
|
+
description: advice.reason,
|
|
632
|
+
evidence,
|
|
633
|
+
});
|
|
634
|
+
if (!proposal) {
|
|
635
|
+
return null;
|
|
636
|
+
}
|
|
637
|
+
return {
|
|
638
|
+
proposalId: proposal.id,
|
|
639
|
+
type: 'enhance',
|
|
640
|
+
targetRecipe: { id: target.id, title: target.title },
|
|
641
|
+
status: proposal.status,
|
|
642
|
+
expiresAt: proposal.expiresAt,
|
|
643
|
+
message: `候选独立价值不足,已创建增强提案建议补充到「${target.title}」。`,
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
@@ -147,8 +147,9 @@ export const TOOLS = [
|
|
|
147
147
|
description: 'Submit knowledge entries (single/batch unified pipeline). Pass 1~N items via the items array.\n' +
|
|
148
148
|
'• All entries undergo strict validation; all V3 fields must be provided at once\n' +
|
|
149
149
|
'• Unified consolidation analysis: detects overlap with existing Recipes and batch candidates\n' +
|
|
150
|
-
'•
|
|
150
|
+
'• Overlap detected → evolution proposal created automatically (merge/enhance/reorganize); system auto-executes after observation window\n' +
|
|
151
151
|
'• Set skipConsolidation: true to skip consolidation check. content and reasoning must be objects.\n' +
|
|
152
|
+
'• Set supersedes: "old-recipe-id" to declare the new Recipe replaces an existing one (creates a supersede proposal with observation window).\n' +
|
|
152
153
|
'⚠️ Batch rule: items in the array must NOT be cross-redundant — no highly overlapping doClause/coreCode/trigger within the same batch. ' +
|
|
153
154
|
'If two entries share 80%+ content, merge into one or split into primary + extends supplementary entries.',
|
|
154
155
|
inputSchema: zodToMcpSchema(SubmitKnowledgeInput),
|
|
@@ -65,7 +65,10 @@ export function register(c) {
|
|
|
65
65
|
});
|
|
66
66
|
c.singleton('knowledgeSyncService', (ct) => {
|
|
67
67
|
const projectRoot = resolveProjectRoot(ct);
|
|
68
|
-
|
|
68
|
+
const sourceRefReconciler = ct.singletons.sourceRefReconciler;
|
|
69
|
+
return new KnowledgeSyncService(projectRoot, {
|
|
70
|
+
sourceRefReconciler: sourceRefReconciler || undefined,
|
|
71
|
+
});
|
|
69
72
|
});
|
|
70
73
|
// ═══ ReportStore ═══
|
|
71
74
|
c.singleton('reportStore', (ct) => {
|
|
@@ -13,18 +13,21 @@ import { getEnhancementRegistry } from '../../core/enhancement/index.js';
|
|
|
13
13
|
import { HnswVectorAdapter } from '../../infrastructure/vector/HnswVectorAdapter.js';
|
|
14
14
|
import { IndexingPipeline } from '../../infrastructure/vector/IndexingPipeline.js';
|
|
15
15
|
import { JsonVectorAdapter } from '../../infrastructure/vector/JsonVectorAdapter.js';
|
|
16
|
+
import { ProposalRepository } from '../../repository/evolution/ProposalRepository.js';
|
|
16
17
|
import { DimensionCopy } from '../../service/bootstrap/DimensionCopyRegistry.js';
|
|
17
18
|
import { ConsolidationAdvisor } from '../../service/evolution/ConsolidationAdvisor.js';
|
|
18
19
|
import { ContradictionDetector } from '../../service/evolution/ContradictionDetector.js';
|
|
19
20
|
import { DecayDetector } from '../../service/evolution/DecayDetector.js';
|
|
20
21
|
import { EnhancementSuggester } from '../../service/evolution/EnhancementSuggester.js';
|
|
21
22
|
import { KnowledgeMetabolism } from '../../service/evolution/KnowledgeMetabolism.js';
|
|
23
|
+
import { ProposalExecutor } from '../../service/evolution/ProposalExecutor.js';
|
|
22
24
|
import { RedundancyAnalyzer } from '../../service/evolution/RedundancyAnalyzer.js';
|
|
23
25
|
import { StagingManager } from '../../service/evolution/StagingManager.js';
|
|
24
26
|
import { CodeEntityGraph } from '../../service/knowledge/CodeEntityGraph.js';
|
|
25
27
|
import { ConfidenceRouter } from '../../service/knowledge/ConfidenceRouter.js';
|
|
26
28
|
import { KnowledgeGraphService } from '../../service/knowledge/KnowledgeGraphService.js';
|
|
27
29
|
import { KnowledgeService } from '../../service/knowledge/KnowledgeService.js';
|
|
30
|
+
import { SourceRefReconciler } from '../../service/knowledge/SourceRefReconciler.js';
|
|
28
31
|
import { HybridRetriever } from '../../service/search/HybridRetriever.js';
|
|
29
32
|
import { SearchEngine } from '../../service/search/SearchEngine.js';
|
|
30
33
|
import { LanguageService } from '../../shared/LanguageService.js';
|
|
@@ -129,6 +132,13 @@ export function register(c) {
|
|
|
129
132
|
c.register('aiProvider', () => c.singletons.aiProvider || null);
|
|
130
133
|
c.register('projectGraph', () => c.singletons.projectGraph || null);
|
|
131
134
|
// ═══ Governance / Evolution ═══
|
|
135
|
+
c.singleton('sourceRefReconciler', (ct) => {
|
|
136
|
+
const db = ct.get('database');
|
|
137
|
+
const projectRoot = resolveProjectRoot();
|
|
138
|
+
return new SourceRefReconciler(projectRoot, db.getDb(), {
|
|
139
|
+
signalBus: ct.singletons.signalBus || undefined,
|
|
140
|
+
});
|
|
141
|
+
});
|
|
132
142
|
c.singleton('stagingManager', (ct) => {
|
|
133
143
|
const db = ct.get('database');
|
|
134
144
|
return new StagingManager(db.getDb(), {
|
|
@@ -165,6 +175,19 @@ export function register(c) {
|
|
|
165
175
|
redundancyAnalyzer: ct.get('redundancyAnalyzer'),
|
|
166
176
|
decayDetector: ct.get('decayDetector'),
|
|
167
177
|
signalBus: ct.singletons.signalBus || undefined,
|
|
178
|
+
proposalRepository: ct.services.proposalRepository
|
|
179
|
+
? ct.get('proposalRepository')
|
|
180
|
+
: undefined,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
c.singleton('proposalRepository', (ct) => {
|
|
184
|
+
const db = ct.get('database');
|
|
185
|
+
return new ProposalRepository(db.getDb());
|
|
186
|
+
});
|
|
187
|
+
c.singleton('proposalExecutor', (ct) => {
|
|
188
|
+
const db = ct.get('database');
|
|
189
|
+
return new ProposalExecutor(db.getDb(), ct.get('proposalRepository'), {
|
|
190
|
+
signalBus: ct.singletons.signalBus || undefined,
|
|
168
191
|
});
|
|
169
192
|
});
|
|
170
193
|
c.singleton('consolidationAdvisor', (ct) => {
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProposalRepository — evolution_proposals 表 CRUD
|
|
3
|
+
*
|
|
4
|
+
* 操作 evolution_proposals 表,存储进化提案(merge/supersede/enhance/deprecate/
|
|
5
|
+
* reorganize/contradiction/correction)。
|
|
6
|
+
*
|
|
7
|
+
* 设计要求:
|
|
8
|
+
* - 去重:同 target + 同 type 不允许多个 observing 状态的 Proposal
|
|
9
|
+
* - Rate Limit:同一 target 不允许同时存在多个相同类型的 observing Proposal
|
|
10
|
+
* - JSON 字段(evidence/related_recipe_ids)序列化/反序列化
|
|
11
|
+
*/
|
|
12
|
+
interface DatabaseLike {
|
|
13
|
+
prepare(sql: string): {
|
|
14
|
+
all(...params: unknown[]): Record<string, unknown>[];
|
|
15
|
+
get(...params: unknown[]): Record<string, unknown> | undefined;
|
|
16
|
+
run(...params: unknown[]): {
|
|
17
|
+
changes: number;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** Proposal 类型 — 统一标准 */
|
|
22
|
+
export type ProposalType = 'merge' | 'supersede' | 'enhance' | 'deprecate' | 'reorganize' | 'contradiction' | 'correction';
|
|
23
|
+
/** Proposal 来源 */
|
|
24
|
+
export type ProposalSource = 'ide-agent' | 'metabolism' | 'decay-scan';
|
|
25
|
+
/** Proposal 状态 */
|
|
26
|
+
export type ProposalStatus = 'pending' | 'observing' | 'executed' | 'rejected' | 'expired';
|
|
27
|
+
/** evolution_proposals 行对象 */
|
|
28
|
+
export interface ProposalRecord {
|
|
29
|
+
id: string;
|
|
30
|
+
type: ProposalType;
|
|
31
|
+
targetRecipeId: string;
|
|
32
|
+
relatedRecipeIds: string[];
|
|
33
|
+
confidence: number;
|
|
34
|
+
source: ProposalSource;
|
|
35
|
+
description: string;
|
|
36
|
+
evidence: Record<string, unknown>[];
|
|
37
|
+
status: ProposalStatus;
|
|
38
|
+
proposedAt: number;
|
|
39
|
+
expiresAt: number;
|
|
40
|
+
resolvedAt: number | null;
|
|
41
|
+
resolvedBy: string | null;
|
|
42
|
+
resolution: string | null;
|
|
43
|
+
}
|
|
44
|
+
/** 创建 Proposal 输入 */
|
|
45
|
+
export interface CreateProposalInput {
|
|
46
|
+
type: ProposalType;
|
|
47
|
+
targetRecipeId: string;
|
|
48
|
+
relatedRecipeIds?: string[];
|
|
49
|
+
confidence: number;
|
|
50
|
+
source: ProposalSource;
|
|
51
|
+
description: string;
|
|
52
|
+
evidence?: Record<string, unknown>[];
|
|
53
|
+
status?: ProposalStatus;
|
|
54
|
+
expiresAt?: number;
|
|
55
|
+
}
|
|
56
|
+
/** 查询过滤器 */
|
|
57
|
+
export interface ProposalFilter {
|
|
58
|
+
status?: ProposalStatus | ProposalStatus[];
|
|
59
|
+
type?: ProposalType;
|
|
60
|
+
targetRecipeId?: string;
|
|
61
|
+
source?: ProposalSource;
|
|
62
|
+
expiredBefore?: number;
|
|
63
|
+
}
|
|
64
|
+
export declare class ProposalRepository {
|
|
65
|
+
#private;
|
|
66
|
+
constructor(db: DatabaseLike);
|
|
67
|
+
/**
|
|
68
|
+
* 创建 Proposal 并写入 DB。
|
|
69
|
+
*
|
|
70
|
+
* - 自动生成 ID(ep-{timestamp}-{random})
|
|
71
|
+
* - 自动设定 expiresAt(按 type 默认窗口)
|
|
72
|
+
* - 自动判断 status(低风险 + 高置信度 → observing,否则 pending)
|
|
73
|
+
* - 去重:同 target + 同 type 已有 pending/observing 时拒绝创建
|
|
74
|
+
*/
|
|
75
|
+
create(input: CreateProposalInput): ProposalRecord | null;
|
|
76
|
+
/** 按 ID 查询 */
|
|
77
|
+
findById(id: string): ProposalRecord | null;
|
|
78
|
+
/** 按条件查询 */
|
|
79
|
+
find(filter?: ProposalFilter): ProposalRecord[];
|
|
80
|
+
/** 查询已到期的 observing 状态 Proposal */
|
|
81
|
+
findExpiredObserving(): ProposalRecord[];
|
|
82
|
+
/** 查询所有未完成的 Proposal(pending + observing) */
|
|
83
|
+
findActive(): ProposalRecord[];
|
|
84
|
+
/** 按 target Recipe ID 查询活跃 Proposal */
|
|
85
|
+
findByTarget(targetRecipeId: string): ProposalRecord[];
|
|
86
|
+
/** 将 Proposal 状态转为 observing */
|
|
87
|
+
startObserving(id: string): boolean;
|
|
88
|
+
/** 标记 Proposal 为已执行 */
|
|
89
|
+
markExecuted(id: string, resolution: string, resolvedBy?: string): boolean;
|
|
90
|
+
/** 标记 Proposal 为已拒绝 */
|
|
91
|
+
markRejected(id: string, resolution: string, resolvedBy?: string): boolean;
|
|
92
|
+
/** 标记 Proposal 为过期 */
|
|
93
|
+
markExpired(id: string): boolean;
|
|
94
|
+
/** 更新 evidence(用于追加观察期指标快照) */
|
|
95
|
+
updateEvidence(id: string, evidence: Record<string, unknown>[]): boolean;
|
|
96
|
+
/** 统计各状态的 Proposal 数量 */
|
|
97
|
+
stats(): Record<ProposalStatus, number>;
|
|
98
|
+
}
|
|
99
|
+
export {};
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ProposalRepository — evolution_proposals 表 CRUD
|
|
3
|
+
*
|
|
4
|
+
* 操作 evolution_proposals 表,存储进化提案(merge/supersede/enhance/deprecate/
|
|
5
|
+
* reorganize/contradiction/correction)。
|
|
6
|
+
*
|
|
7
|
+
* 设计要求:
|
|
8
|
+
* - 去重:同 target + 同 type 不允许多个 observing 状态的 Proposal
|
|
9
|
+
* - Rate Limit:同一 target 不允许同时存在多个相同类型的 observing Proposal
|
|
10
|
+
* - JSON 字段(evidence/related_recipe_ids)序列化/反序列化
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
/* ────────────────────── Constants ────────────────────── */
|
|
14
|
+
/** 默认观察窗口:7 天 */
|
|
15
|
+
const DEFAULT_OBSERVATION_WINDOW = 7 * 24 * 60 * 60 * 1000;
|
|
16
|
+
/** 各 Proposal 类型的默认观察窗口(ms) */
|
|
17
|
+
const OBSERVATION_WINDOWS = {
|
|
18
|
+
enhance: 48 * 60 * 60 * 1000, // 48h
|
|
19
|
+
correction: 24 * 60 * 60 * 1000, // 24h
|
|
20
|
+
merge: 72 * 60 * 60 * 1000, // 72h
|
|
21
|
+
supersede: 72 * 60 * 60 * 1000, // 72h
|
|
22
|
+
deprecate: 7 * 24 * 60 * 60 * 1000, // 7d
|
|
23
|
+
contradiction: 7 * 24 * 60 * 60 * 1000, // 7d
|
|
24
|
+
reorganize: 7 * 24 * 60 * 60 * 1000, // 7d
|
|
25
|
+
};
|
|
26
|
+
/** 自动进入观察状态的置信度阈值 */
|
|
27
|
+
const AUTO_OBSERVE_THRESHOLDS = {
|
|
28
|
+
enhance: 0.7,
|
|
29
|
+
correction: 0.7,
|
|
30
|
+
merge: 0.75,
|
|
31
|
+
supersede: 0.8,
|
|
32
|
+
deprecate: 0.0, // decayScore ≤ 40 即可
|
|
33
|
+
contradiction: Infinity, // 需开发者确认
|
|
34
|
+
reorganize: Infinity, // 需开发者确认
|
|
35
|
+
};
|
|
36
|
+
/* ────────────────────── Class ────────────────────── */
|
|
37
|
+
export class ProposalRepository {
|
|
38
|
+
#db;
|
|
39
|
+
constructor(db) {
|
|
40
|
+
this.#db = db;
|
|
41
|
+
}
|
|
42
|
+
/* ═══════════════════ Create ═══════════════════ */
|
|
43
|
+
/**
|
|
44
|
+
* 创建 Proposal 并写入 DB。
|
|
45
|
+
*
|
|
46
|
+
* - 自动生成 ID(ep-{timestamp}-{random})
|
|
47
|
+
* - 自动设定 expiresAt(按 type 默认窗口)
|
|
48
|
+
* - 自动判断 status(低风险 + 高置信度 → observing,否则 pending)
|
|
49
|
+
* - 去重:同 target + 同 type 已有 pending/observing 时拒绝创建
|
|
50
|
+
*/
|
|
51
|
+
create(input) {
|
|
52
|
+
const now = Date.now();
|
|
53
|
+
// 去重检查
|
|
54
|
+
if (this.#hasDuplicate(input.targetRecipeId, input.type)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
const id = ProposalRepository.#generateId(now);
|
|
58
|
+
const expiresAt = input.expiresAt ?? now + (OBSERVATION_WINDOWS[input.type] ?? DEFAULT_OBSERVATION_WINDOW);
|
|
59
|
+
const status = input.status ?? this.#resolveInitialStatus(input.type, input.confidence);
|
|
60
|
+
const record = {
|
|
61
|
+
id,
|
|
62
|
+
type: input.type,
|
|
63
|
+
targetRecipeId: input.targetRecipeId,
|
|
64
|
+
relatedRecipeIds: input.relatedRecipeIds ?? [],
|
|
65
|
+
confidence: input.confidence,
|
|
66
|
+
source: input.source,
|
|
67
|
+
description: input.description,
|
|
68
|
+
evidence: input.evidence ?? [],
|
|
69
|
+
status,
|
|
70
|
+
proposedAt: now,
|
|
71
|
+
expiresAt,
|
|
72
|
+
resolvedAt: null,
|
|
73
|
+
resolvedBy: null,
|
|
74
|
+
resolution: null,
|
|
75
|
+
};
|
|
76
|
+
this.#db
|
|
77
|
+
.prepare(`INSERT INTO evolution_proposals
|
|
78
|
+
(id, type, target_recipe_id, related_recipe_ids, confidence, source, description, evidence, status, proposed_at, expires_at)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
80
|
+
.run(record.id, record.type, record.targetRecipeId, JSON.stringify(record.relatedRecipeIds), record.confidence, record.source, record.description, JSON.stringify(record.evidence), record.status, record.proposedAt, record.expiresAt);
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
83
|
+
/* ═══════════════════ Read ═══════════════════ */
|
|
84
|
+
/** 按 ID 查询 */
|
|
85
|
+
findById(id) {
|
|
86
|
+
const row = this.#db.prepare(`SELECT * FROM evolution_proposals WHERE id = ?`).get(id);
|
|
87
|
+
return row ? ProposalRepository.#mapRow(row) : null;
|
|
88
|
+
}
|
|
89
|
+
/** 按条件查询 */
|
|
90
|
+
find(filter = {}) {
|
|
91
|
+
const conditions = [];
|
|
92
|
+
const params = [];
|
|
93
|
+
if (filter.status) {
|
|
94
|
+
if (Array.isArray(filter.status)) {
|
|
95
|
+
const placeholders = filter.status.map(() => '?').join(', ');
|
|
96
|
+
conditions.push(`status IN (${placeholders})`);
|
|
97
|
+
params.push(...filter.status);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
conditions.push('status = ?');
|
|
101
|
+
params.push(filter.status);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (filter.type) {
|
|
105
|
+
conditions.push('type = ?');
|
|
106
|
+
params.push(filter.type);
|
|
107
|
+
}
|
|
108
|
+
if (filter.targetRecipeId) {
|
|
109
|
+
conditions.push('target_recipe_id = ?');
|
|
110
|
+
params.push(filter.targetRecipeId);
|
|
111
|
+
}
|
|
112
|
+
if (filter.source) {
|
|
113
|
+
conditions.push('source = ?');
|
|
114
|
+
params.push(filter.source);
|
|
115
|
+
}
|
|
116
|
+
if (filter.expiredBefore) {
|
|
117
|
+
conditions.push('expires_at <= ?');
|
|
118
|
+
params.push(filter.expiredBefore);
|
|
119
|
+
}
|
|
120
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
121
|
+
const rows = this.#db
|
|
122
|
+
.prepare(`SELECT * FROM evolution_proposals ${where} ORDER BY proposed_at DESC`)
|
|
123
|
+
.all(...params);
|
|
124
|
+
return rows.map(ProposalRepository.#mapRow);
|
|
125
|
+
}
|
|
126
|
+
/** 查询已到期的 observing 状态 Proposal */
|
|
127
|
+
findExpiredObserving() {
|
|
128
|
+
return this.find({
|
|
129
|
+
status: 'observing',
|
|
130
|
+
expiredBefore: Date.now(),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
/** 查询所有未完成的 Proposal(pending + observing) */
|
|
134
|
+
findActive() {
|
|
135
|
+
return this.find({
|
|
136
|
+
status: ['pending', 'observing'],
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
/** 按 target Recipe ID 查询活跃 Proposal */
|
|
140
|
+
findByTarget(targetRecipeId) {
|
|
141
|
+
return this.find({
|
|
142
|
+
targetRecipeId,
|
|
143
|
+
status: ['pending', 'observing'],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
/* ═══════════════════ Update ═══════════════════ */
|
|
147
|
+
/** 将 Proposal 状态转为 observing */
|
|
148
|
+
startObserving(id) {
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const proposal = this.findById(id);
|
|
151
|
+
if (!proposal || proposal.status !== 'pending') {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
const expiresAt = now + (OBSERVATION_WINDOWS[proposal.type] ?? DEFAULT_OBSERVATION_WINDOW);
|
|
155
|
+
const result = this.#db
|
|
156
|
+
.prepare(`UPDATE evolution_proposals SET status = 'observing', expires_at = ? WHERE id = ? AND status = 'pending'`)
|
|
157
|
+
.run(expiresAt, id);
|
|
158
|
+
return result.changes > 0;
|
|
159
|
+
}
|
|
160
|
+
/** 标记 Proposal 为已执行 */
|
|
161
|
+
markExecuted(id, resolution, resolvedBy = 'auto') {
|
|
162
|
+
const result = this.#db
|
|
163
|
+
.prepare(`UPDATE evolution_proposals SET status = 'executed', resolved_at = ?, resolved_by = ?, resolution = ? WHERE id = ? AND status = 'observing'`)
|
|
164
|
+
.run(Date.now(), resolvedBy, resolution, id);
|
|
165
|
+
return result.changes > 0;
|
|
166
|
+
}
|
|
167
|
+
/** 标记 Proposal 为已拒绝 */
|
|
168
|
+
markRejected(id, resolution, resolvedBy = 'auto') {
|
|
169
|
+
const result = this.#db
|
|
170
|
+
.prepare(`UPDATE evolution_proposals SET status = 'rejected', resolved_at = ?, resolved_by = ?, resolution = ? WHERE id = ? AND status IN ('pending', 'observing')`)
|
|
171
|
+
.run(Date.now(), resolvedBy, resolution, id);
|
|
172
|
+
return result.changes > 0;
|
|
173
|
+
}
|
|
174
|
+
/** 标记 Proposal 为过期 */
|
|
175
|
+
markExpired(id) {
|
|
176
|
+
const result = this.#db
|
|
177
|
+
.prepare(`UPDATE evolution_proposals SET status = 'expired', resolved_at = ? WHERE id = ? AND status IN ('pending', 'observing')`)
|
|
178
|
+
.run(Date.now(), id);
|
|
179
|
+
return result.changes > 0;
|
|
180
|
+
}
|
|
181
|
+
/** 更新 evidence(用于追加观察期指标快照) */
|
|
182
|
+
updateEvidence(id, evidence) {
|
|
183
|
+
const result = this.#db
|
|
184
|
+
.prepare(`UPDATE evolution_proposals SET evidence = ? WHERE id = ?`)
|
|
185
|
+
.run(JSON.stringify(evidence), id);
|
|
186
|
+
return result.changes > 0;
|
|
187
|
+
}
|
|
188
|
+
/* ═══════════════════ Stats ═══════════════════ */
|
|
189
|
+
/** 统计各状态的 Proposal 数量 */
|
|
190
|
+
stats() {
|
|
191
|
+
const rows = this.#db
|
|
192
|
+
.prepare(`SELECT status, COUNT(*) as count FROM evolution_proposals GROUP BY status`)
|
|
193
|
+
.all();
|
|
194
|
+
const result = {
|
|
195
|
+
pending: 0,
|
|
196
|
+
observing: 0,
|
|
197
|
+
executed: 0,
|
|
198
|
+
rejected: 0,
|
|
199
|
+
expired: 0,
|
|
200
|
+
};
|
|
201
|
+
for (const row of rows) {
|
|
202
|
+
result[row.status] = row.count;
|
|
203
|
+
}
|
|
204
|
+
return result;
|
|
205
|
+
}
|
|
206
|
+
/* ═══════════════════ Private ═══════════════════ */
|
|
207
|
+
/** 去重检查:同 target + 同 type 是否已有 pending/observing Proposal */
|
|
208
|
+
#hasDuplicate(targetRecipeId, type) {
|
|
209
|
+
const row = this.#db
|
|
210
|
+
.prepare(`SELECT 1 FROM evolution_proposals WHERE target_recipe_id = ? AND type = ? AND status IN ('pending', 'observing') LIMIT 1`)
|
|
211
|
+
.get(targetRecipeId, type);
|
|
212
|
+
return row !== undefined;
|
|
213
|
+
}
|
|
214
|
+
/** 根据 type + confidence 判断初始状态 */
|
|
215
|
+
#resolveInitialStatus(type, confidence) {
|
|
216
|
+
const threshold = AUTO_OBSERVE_THRESHOLDS[type];
|
|
217
|
+
return confidence >= threshold ? 'observing' : 'pending';
|
|
218
|
+
}
|
|
219
|
+
/** 生成 Proposal ID */
|
|
220
|
+
static #generateId(timestamp) {
|
|
221
|
+
const rand = randomBytes(4).toString('hex');
|
|
222
|
+
return `ep-${timestamp}-${rand}`;
|
|
223
|
+
}
|
|
224
|
+
/** DB 行 → ProposalRecord */
|
|
225
|
+
static #mapRow(row) {
|
|
226
|
+
return {
|
|
227
|
+
id: row.id,
|
|
228
|
+
type: row.type,
|
|
229
|
+
targetRecipeId: row.target_recipe_id,
|
|
230
|
+
relatedRecipeIds: safeJsonParse(row.related_recipe_ids, []),
|
|
231
|
+
confidence: row.confidence,
|
|
232
|
+
source: row.source,
|
|
233
|
+
description: row.description,
|
|
234
|
+
evidence: safeJsonParse(row.evidence, []),
|
|
235
|
+
status: row.status,
|
|
236
|
+
proposedAt: row.proposed_at,
|
|
237
|
+
expiresAt: row.expires_at,
|
|
238
|
+
resolvedAt: row.resolved_at ?? null,
|
|
239
|
+
resolvedBy: row.resolved_by ?? null,
|
|
240
|
+
resolution: row.resolution ?? null,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
/* ────────────────────── Util ────────────────────── */
|
|
245
|
+
function safeJsonParse(json, fallback) {
|
|
246
|
+
if (!json) {
|
|
247
|
+
return fallback;
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
return JSON.parse(json);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
return fallback;
|
|
254
|
+
}
|
|
255
|
+
}
|