autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
383 lines (382 loc) • 16.4 kB
JavaScript
/**
* RecipeProductionGateway — 统一 Recipe 生产入口
*
* 所有 Recipe 创建(Agent Tool / MCP / IDE Agent / Batch Import)
* 通过此 Gateway 的统一管道,保证前置校验一致:
*
* 1. Schema Validation (UnifiedValidator)
* 2. Similarity Check — 去重检测(可选跳过)
* 3. Consolidation Scan — 融合/重组建议(可选)
* 4. KnowledgeService.create() — 包含 ConfidenceRouter → staging / pending
* 5. Quality Scoring — 质量评分
* 6. Supersede Proposal — 创建替代提案
* 7. Audit — 统一审计
*/
import { UnifiedValidator } from '#domain/knowledge/UnifiedValidator.js';
/* ═══════════════════ Gateway ═══════════════════ */
export class RecipeProductionGateway {
#knowledgeService;
#projectRoot;
#logger;
#consolidationAdvisor;
#proposalRepo;
#findSimilarRecipes;
constructor(deps) {
this.#knowledgeService = deps.knowledgeService;
this.#projectRoot = deps.projectRoot;
this.#logger = deps.logger;
this.#consolidationAdvisor = deps.consolidationAdvisor ?? null;
this.#proposalRepo = deps.proposalRepository ?? null;
this.#findSimilarRecipes = deps.findSimilarRecipes ?? null;
}
/**
* 统一创建入口
*
* Pipeline:
* 1. Schema Validation (UnifiedValidator)
* 2. Similarity Check (除非 skipSimilarityCheck)
* 3. Consolidation Scan (除非 skipConsolidation)
* 4. KnowledgeService.create() — ConfidenceRouter → staging / pending
* 5. Quality Scoring
* 6. Supersede Proposal 创建 (if supersedes)
*/
async create(request) {
const { source, items, options = {} } = request;
const userId = options.userId || this.#sourceToUserId(source);
const result = {
created: [],
rejected: [],
merged: [],
blocked: [],
duplicates: [],
supersedeProposal: null,
};
if (items.length === 0) {
return result;
}
// ── Step 1: Schema Validation ──
const validator = new UnifiedValidator({
existingTitles: options.existingTitles,
existingFingerprints: options.existingFingerprints,
});
const validItems = [];
for (let i = 0; i < items.length; i++) {
const item = items[i];
const validation = validator.validate(item, {
systemInjectedFields: options.systemInjectedFields,
skipUniqueness: options.skipUniqueness,
});
if (!validation.pass) {
result.rejected.push({
index: i,
title: item.title || '(untitled)',
reason: 'validation_failed',
errors: validation.errors,
warnings: validation.warnings,
});
this.#logger?.info(`[Gateway] ✗ validation rejected item ${i}: ${validation.errors.join('; ')}`);
}
else {
validItems.push({ index: i, item });
// 记录已提交标题/指纹以防批量内重复
validator.recordSubmission(item.title, item.content?.pattern);
}
}
// ── Step 2: Similarity Check ──
let afterSimilarityItems = validItems;
if (!options.skipSimilarityCheck && this.#findSimilarRecipes) {
const threshold = options.similarityThreshold ?? 0.7;
afterSimilarityItems = [];
for (const entry of validItems) {
const { item, index } = entry;
const contentObj = item.content && typeof item.content === 'object' ? item.content : { markdown: '' };
const cand = {
title: item.title || '',
summary: item.description || '',
code: contentObj.markdown || contentObj.pattern || '',
};
const similar = this.#findSimilarRecipes(this.#projectRoot, cand, {
threshold: 0.5,
topK: 5,
});
const hasDuplicate = similar.some((s) => s.similarity >= threshold);
if (hasDuplicate) {
result.duplicates.push({
index,
title: item.title || '(untitled)',
similarTo: similar,
});
this.#logger?.info(`[Gateway] ✗ duplicate blocked item ${index}: similarity ${similar[0]?.similarity}`);
}
else {
afterSimilarityItems.push(entry);
}
}
}
// ── Step 3: Consolidation Scan ──
let submittableItems = afterSimilarityItems;
if (!options.skipConsolidation &&
this.#consolidationAdvisor &&
afterSimilarityItems.length > 0) {
submittableItems = [];
try {
const candidates = afterSimilarityItems.map((e) => ({
title: e.item.title || '',
category: e.item.category || e.item._category || '',
...e.item,
}));
const batchAdvice = this.#consolidationAdvisor.analyzeBatch(candidates);
for (let ai = 0; ai < batchAdvice.items.length; ai++) {
const { advice } = batchAdvice.items[ai];
const validEntry = afterSimilarityItems[ai];
if (!validEntry) {
continue;
}
if (advice.action === 'create') {
submittableItems.push(validEntry);
}
else if (this.#proposalRepo) {
const proposal = this.#createProposalFromAdvice(advice, validEntry.item);
if (proposal) {
result.merged.push({
index: validEntry.index,
proposalId: proposal.proposalId,
type: proposal.type,
targetRecipeId: proposal.targetRecipeId,
targetTitle: proposal.targetTitle,
status: proposal.status,
expiresAt: proposal.expiresAt,
message: proposal.message,
});
}
else {
// Proposal 创建失败 → blocked
result.blocked.push({
index: validEntry.index,
title: validEntry.item.title || '(untitled)',
consolidation: advice,
});
}
}
else {
// 无 ProposalRepository → blocked
result.blocked.push({
index: validEntry.index,
title: validEntry.item.title || '(untitled)',
consolidation: advice,
});
}
}
}
catch (err) {
this.#logger?.warn(`[Gateway] ConsolidationAdvisor error, falling back to direct submit: ${err instanceof Error ? err.message : String(err)}`);
submittableItems = afterSimilarityItems;
}
}
// ── Step 4: Create via KnowledgeService ──
const createdIds = [];
for (const { item } of submittableItems) {
try {
const data = this.#prepareCreateData(item, source, userId);
const saved = await this.#knowledgeService.create(data, { userId });
result.created.push({
id: saved.id,
title: saved.title,
lifecycle: saved.lifecycle,
raw: saved,
});
createdIds.push(saved.id);
// ── Step 5: Quality Scoring (best effort) ──
try {
await this.#knowledgeService.updateQuality(saved.id, { userId });
}
catch {
/* best effort — 不阻塞创建流程 */
}
}
catch (err) {
result.rejected.push({
index: items.indexOf(item),
title: item.title || '(untitled)',
reason: 'create_failed',
errors: [err instanceof Error ? err.message : String(err)],
warnings: [],
});
this.#logger?.warn(`[Gateway] ✗ create failed for "${item.title}": ${err instanceof Error ? err.message : String(err)}`);
}
}
// ── Step 6: Supersede Proposal ──
if (options.supersedes && createdIds.length > 0) {
try {
// 直接使用 ProposalRepository(Gateway 不依赖 ServiceContainer)
if (this.#proposalRepo) {
const proposal = this.#proposalRepo.create({
type: 'supersede',
targetRecipeId: options.supersedes,
relatedRecipeIds: createdIds,
confidence: 0.9,
source: source === 'mcp-external' ? 'ide-agent' : 'ide-agent',
description: `Supersede proposal: ${createdIds.length} new recipe(s) replace ${options.supersedes}`,
evidence: [{ snapshotAt: Date.now(), newRecipeIds: createdIds }],
});
if (proposal) {
result.supersedeProposal = { proposalId: proposal.id };
}
}
}
catch (err) {
this.#logger?.warn(`[Gateway] Supersede proposal creation failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
this.#logger?.info(`[Gateway] create complete: ${result.created.length} created, ${result.rejected.length} rejected, ${result.merged.length} merged, ${result.duplicates.length} duplicates | source=${source}`);
return result;
}
/* ═══════════════════ Private ═══════════════════ */
#sourceToUserId(source) {
switch (source) {
case 'agent-tool':
return 'agent';
case 'mcp-external':
return 'mcp';
case 'ide-agent':
return 'ide-agent';
case 'batch-import':
return 'batch-import';
}
}
#prepareCreateData(item, source, _userId) {
const contentObj = item.content && typeof item.content === 'object'
? item.content
: { markdown: '', pattern: '' };
const reasoning = item.reasoning || {
whyStandard: '',
sources: ['agent'],
confidence: 0.7,
};
if (Array.isArray(reasoning.sources) && reasoning.sources.length === 0) {
reasoning.sources = ['agent'];
}
return {
language: item.language || '',
category: item.category || item._category || 'general',
knowledgeType: item.knowledgeType || 'code-pattern',
source: item.source || this.#sourceLabel(source),
title: item.title || '',
description: item.description || '',
tags: item.tags || [],
trigger: item.trigger || '',
kind: item.kind || 'pattern',
topicHint: item.topicHint || '',
whenClause: item.whenClause || '',
doClause: item.doClause || '',
dontClause: item.dontClause || '',
coreCode: item.coreCode || contentObj.pattern || '',
sourceRefs: item.sourceRefs || [],
content: contentObj,
reasoning,
headers: item.headers || [],
usageGuide: item.usageGuide || '',
scope: item.scope || '',
complexity: item.complexity || '',
sourceFile: '',
agentNotes: item.agentNotes || null,
aiInsight: reasoning.whyStandard || item.description || null,
};
}
#sourceLabel(source) {
switch (source) {
case 'agent-tool':
return 'agent';
case 'mcp-external':
return 'mcp';
case 'ide-agent':
return 'ide-agent';
case 'batch-import':
return 'batch-import';
}
}
#createProposalFromAdvice(advice, item) {
if (!this.#proposalRepo) {
return null;
}
const evidence = [
{
snapshotAt: Date.now(),
candidateTitle: item.title,
candidateCategory: item.category,
analysisReason: advice.reason,
mergeDirection: advice.mergeDirection,
},
];
if (advice.action === 'merge' && advice.targetRecipe) {
const proposal = this.#proposalRepo.create({
type: 'merge',
targetRecipeId: advice.targetRecipe.id,
confidence: advice.confidence,
source: 'ide-agent',
description: advice.reason,
evidence,
});
if (!proposal) {
return null;
}
return {
proposalId: proposal.id,
type: 'merge',
targetRecipeId: advice.targetRecipe.id,
targetTitle: advice.targetRecipe.title,
status: proposal.status,
expiresAt: proposal.expiresAt,
message: `已为「${advice.targetRecipe.title}」创建融合提案,${proposal.status === 'observing' ? '观察窗口 72h 后自动执行' : '等待开发者确认'}。`,
};
}
if (advice.action === 'reorganize' && advice.reorganizeTargets?.length) {
const target = advice.reorganizeTargets[0];
const proposal = this.#proposalRepo.create({
type: 'reorganize',
targetRecipeId: target.id,
relatedRecipeIds: advice.reorganizeTargets.slice(1).map((t) => t.id),
confidence: advice.confidence,
source: 'ide-agent',
description: advice.reason,
evidence,
});
if (!proposal) {
return null;
}
return {
proposalId: proposal.id,
type: 'reorganize',
targetRecipeId: target.id,
targetTitle: target.title,
status: proposal.status,
expiresAt: proposal.expiresAt,
message: `已为 ${advice.reorganizeTargets.length} 条 Recipe 创建重组提案,需开发者在 Dashboard 确认。`,
};
}
if (advice.action === 'insufficient' && advice.coveredBy?.length) {
const target = advice.coveredBy[0];
const proposal = this.#proposalRepo.create({
type: 'enhance',
targetRecipeId: target.id,
confidence: advice.confidence,
source: 'ide-agent',
description: advice.reason,
evidence,
});
if (!proposal) {
return null;
}
return {
proposalId: proposal.id,
type: 'enhance',
targetRecipeId: target.id,
targetTitle: target.title,
status: proposal.status,
expiresAt: proposal.expiresAt,
message: `候选独立价值不足,已创建增强提案建议补充到「${target.title}」。`,
};
}
return null;
}
}