autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
420 lines (419 loc) • 18.6 kB
JavaScript
/**
* ProposalExecutor — 到期自动执行引擎
*
* 核心职责:
* 1. 扫描所有 observing 状态的 Proposal,检查是否到期
* 2. 到期 → 收集观察期表现数据 → 评估执行判据
* 3. 通过 → 执行操作(merge/deprecate/enhance/...)
* 4. 不通过 → 拒绝 Proposal,Recipe 恢复原状态
*
* 触发时机:UiStartupTasks Stage 5
*
* 安全边界:
* - Agent 只做分析,ProposalExecutor 做执行
* - merge/enhance 执行后 Recipe → staging(走正常路径)
* - contradiction/reorganize 始终等开发者确认(不自动执行)
* - 到期无判据 → expired
*/
import Logger from '../../infrastructure/logging/Logger.js';
/* ────────────────────── Constants ────────────────────── */
/** 高风险类型:需开发者确认,不自动执行 */
const HIGH_RISK_TYPES = new Set(['contradiction', 'reorganize']);
/** 超过此天数未操作的 pending Proposal 自动过期 */
const PENDING_EXPIRY_DAYS = 14;
/* ────────────────────── Class ────────────────────── */
export class ProposalExecutor {
#knowledgeRepo;
#edgeRepo;
#repo;
#signalBus;
#contentPatcher;
#supervisor;
#logger = Logger.getInstance();
constructor(knowledgeRepo, repo, options = {}) {
this.#knowledgeRepo = knowledgeRepo;
this.#repo = repo;
this.#edgeRepo = options.knowledgeEdgeRepo ?? null;
this.#signalBus = options.signalBus ?? null;
this.#contentPatcher = options.contentPatcher ?? null;
this.#supervisor = options.supervisor ?? null;
}
/**
* 定期调用(UiStartupTasks Stage 5)
*
* 扫描所有到期 Proposal → 评估 → 执行/拒绝/过期
*/
async checkAndExecute() {
const result = {
executed: [],
rejected: [],
expired: [],
skipped: [],
};
// 1. 处理到期的 observing Proposal
const expiredObserving = this.#repo.findExpiredObserving();
for (const proposal of expiredObserving) {
if (HIGH_RISK_TYPES.has(proposal.type)) {
// 高风险类型跳过自动执行
result.skipped.push({
id: proposal.id,
type: proposal.type,
reason: 'high-risk type requires developer confirmation',
});
continue;
}
await this.#processExpiredProposal(proposal, result);
}
// 2. 清理超期未操作的 pending Proposal
this.#expireOldPending(result);
if (result.executed.length > 0 || result.rejected.length > 0 || result.expired.length > 0) {
this.#logger.info(`[ProposalExecutor] checkAndExecute complete: ` +
`executed=${result.executed.length}, rejected=${result.rejected.length}, expired=${result.expired.length}`);
}
return result;
}
/* ═══════════════════ Internal ═══════════════════ */
async #processExpiredProposal(proposal, result) {
const metrics = await this.#collectRecipeMetrics(proposal.targetRecipeId);
const snapshot = this.#extractSnapshot(proposal);
switch (proposal.type) {
case 'merge':
case 'enhance':
await this.#executeMergeOrEnhance(proposal, metrics, snapshot, result);
break;
case 'supersede':
await this.#executeSupersede(proposal, metrics, snapshot, result);
break;
case 'deprecate':
await this.#executeDeprecate(proposal, metrics, snapshot, result);
break;
case 'correction':
await this.#executeCorrection(proposal, metrics, result);
break;
default:
result.skipped.push({
id: proposal.id,
type: proposal.type,
reason: `unhandled type: ${proposal.type}`,
});
}
}
/* ── merge / enhance ── */
async #executeMergeOrEnhance(proposal, metrics, snapshot, result) {
// 执行判据:
// - 目标 Recipe 在观察期内无 FP rate 异常飙升
// - 目标 Recipe 在观察期内仍有使用
const fpOk = metrics.ruleFalsePositiveRate < 0.4;
const hasUsage = metrics.guardHits > 0 || metrics.searchHits > 0;
if (fpOk && hasUsage) {
// 通过 → evolving → ContentPatcher → staging(重走 Grace Period)
await this.#transitionRecipe(proposal.targetRecipeId, 'evolving', 'proposal-attach', proposal.id);
const patchResult = await this.#tryApplyPatch(proposal, 'agent-suggestion');
if (patchResult?.skipped || (!patchResult?.success && patchResult !== null)) {
await this.#transitionRecipe(proposal.targetRecipeId, 'active', 'content-patch-complete', proposal.id);
const skipInfo = patchResult?.skipReason ? `: ${patchResult.skipReason}` : '';
this.#repo.markExecuted(proposal.id, `观察期合格但 patch 未生效${skipInfo}, 回退 active`);
}
else {
await this.#transitionRecipe(proposal.targetRecipeId, 'staging', 'content-patch-complete', proposal.id);
const patchInfo = patchResult?.success
? `, patched=[${patchResult.fieldsPatched.join(',')}]`
: '';
this.#repo.markExecuted(proposal.id, `观察期表现合格: FP=${(metrics.ruleFalsePositiveRate * 100).toFixed(0)}%, hits=${metrics.guardHits + metrics.searchHits}${patchInfo}`);
}
result.executed.push({
id: proposal.id,
type: proposal.type,
targetRecipeId: proposal.targetRecipeId,
});
this.#emitSignal(proposal, 'executed');
}
else {
// 不通过 → Recipe 恢复原状态
await this.#restoreRecipe(proposal.targetRecipeId);
this.#repo.markRejected(proposal.id, `观察期表现不达标: FP=${(metrics.ruleFalsePositiveRate * 100).toFixed(0)}%, hasUsage=${hasUsage}`);
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: fpOk ? 'no usage during observation' : 'FP rate too high',
});
this.#emitSignal(proposal, 'rejected');
}
}
/* ── supersede ── */
async #executeSupersede(proposal, metrics, snapshot, result) {
// 新 Recipe 必须已到达 active
const newRecipeId = proposal.relatedRecipeIds[0];
if (!newRecipeId) {
this.#repo.markRejected(proposal.id, 'no related new recipe specified');
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: 'no related new recipe',
});
return;
}
const newRecipe = await this.#getRecipeLifecycle(newRecipeId);
if (newRecipe?.lifecycle !== 'active') {
// 新 Recipe 尚未 active → 跳过,等下次检查
result.skipped.push({
id: proposal.id,
type: proposal.type,
reason: `new recipe ${newRecipeId} not yet active (lifecycle: ${newRecipe?.lifecycle ?? 'unknown'})`,
});
return;
}
// 对比新旧 Recipe 的使用数据
const newMetrics = await this.#collectRecipeMetrics(newRecipeId);
const oldUsage = metrics.guardHits + metrics.searchHits;
const newUsage = newMetrics.guardHits + newMetrics.searchHits;
if (newUsage >= oldUsage * 0.5 || oldUsage === 0) {
// 新 Recipe 表现足够 → 旧 Recipe → decaying,建立 deprecated_by
await this.#transitionRecipe(proposal.targetRecipeId, 'decaying', 'proposal-execution', proposal.id);
await this.#createDeprecatedByEdge(newRecipeId, proposal.targetRecipeId);
this.#repo.markExecuted(proposal.id, `supersede executed: new usage=${newUsage}, old usage=${oldUsage}`);
result.executed.push({
id: proposal.id,
type: proposal.type,
targetRecipeId: proposal.targetRecipeId,
});
this.#emitSignal(proposal, 'executed');
}
else {
// 新 Recipe 表现不足 → 拒绝
await this.#restoreRecipe(proposal.targetRecipeId);
this.#repo.markRejected(proposal.id, `new recipe usage (${newUsage}) < 50% of old (${oldUsage})`);
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: 'new recipe insufficient usage',
});
this.#emitSignal(proposal, 'rejected');
}
}
/* ── deprecate ── */
async #executeDeprecate(proposal, metrics, snapshot, result) {
const currentDecay = metrics.decayScore;
const snapshotDecay = snapshot?.decayScore ?? currentDecay;
// 观察期内 decayScore 有回升 → 拒绝
if (currentDecay > snapshotDecay + 10) {
await this.#restoreRecipe(proposal.targetRecipeId);
this.#repo.markRejected(proposal.id, `decayScore recovered: ${snapshotDecay} → ${currentDecay}`);
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: 'decay score recovered during observation',
});
this.#emitSignal(proposal, 'rejected');
return;
}
// 无回升 → 根据 decayScore 决定操作
if (currentDecay <= 19) {
// 死亡 → 直接 deprecated
await this.#transitionRecipe(proposal.targetRecipeId, 'deprecated', 'proposal-execution', proposal.id);
this.#repo.markExecuted(proposal.id, `deprecated (dead): decayScore=${currentDecay}`);
}
else if (currentDecay <= 40) {
// 严重 → decaying
await this.#transitionRecipe(proposal.targetRecipeId, 'decaying', 'proposal-execution', proposal.id);
this.#repo.markExecuted(proposal.id, `decaying (severe): decayScore=${currentDecay}`);
}
else {
// 衰退减缓 → 拒绝
await this.#restoreRecipe(proposal.targetRecipeId);
this.#repo.markRejected(proposal.id, `decayScore above threshold (${currentDecay}), not critical enough`);
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: `decayScore (${currentDecay}) not critical`,
});
this.#emitSignal(proposal, 'rejected');
return;
}
result.executed.push({
id: proposal.id,
type: proposal.type,
targetRecipeId: proposal.targetRecipeId,
});
this.#emitSignal(proposal, 'executed');
}
/* ── correction ── */
async #executeCorrection(proposal, metrics, result) {
// correction 低风险,到期直接执行(Recipe → evolving → patch → staging 重新审核)
const hasUsage = metrics.guardHits > 0 || metrics.searchHits > 0;
if (hasUsage) {
await this.#transitionRecipe(proposal.targetRecipeId, 'evolving', 'proposal-attach', proposal.id);
const patchResult = await this.#tryApplyPatch(proposal, 'correction');
if (patchResult?.skipped || (!patchResult?.success && patchResult !== null)) {
await this.#transitionRecipe(proposal.targetRecipeId, 'active', 'content-patch-complete', proposal.id);
const skipInfo = patchResult?.skipReason ? `: ${patchResult.skipReason}` : '';
this.#repo.markExecuted(proposal.id, `correction patch 未生效${skipInfo}, 回退 active`);
}
else {
await this.#transitionRecipe(proposal.targetRecipeId, 'staging', 'content-patch-complete', proposal.id);
const patchInfo = patchResult?.success
? `, patched=[${patchResult.fieldsPatched.join(',')}]`
: '';
this.#repo.markExecuted(proposal.id, `correction applied, recipe → evolving → staging for re-review${patchInfo}`);
}
result.executed.push({
id: proposal.id,
type: proposal.type,
targetRecipeId: proposal.targetRecipeId,
});
this.#emitSignal(proposal, 'executed');
}
else {
this.#repo.markRejected(proposal.id, 'no usage during observation, correction unnecessary');
result.rejected.push({
id: proposal.id,
type: proposal.type,
reason: 'no usage',
});
this.#emitSignal(proposal, 'rejected');
}
}
/* ── expired pending cleanup ── */
#expireOldPending(result) {
const cutoff = Date.now() - PENDING_EXPIRY_DAYS * 24 * 60 * 60 * 1000;
const oldPending = this.#repo.find({
status: 'pending',
expiredBefore: cutoff,
});
for (const proposal of oldPending) {
this.#repo.markExpired(proposal.id);
result.expired.push({
id: proposal.id,
type: proposal.type,
});
}
}
/* ═══════════════════ DB Helpers ═══════════════════ */
async #collectRecipeMetrics(recipeId) {
const entry = await this.#knowledgeRepo.findById(recipeId);
if (!entry) {
return {
guardHits: 0,
searchHits: 0,
hitsLast30d: 0,
decayScore: 0,
ruleFalsePositiveRate: 0,
quality: 0,
};
}
const stats = (entry.stats ?? {});
const quality = (entry.quality ?? {});
return {
guardHits: stats.guardHits ?? 0,
searchHits: stats.searchHits ?? 0,
hitsLast30d: stats.hitsLast30d ?? 0,
decayScore: stats.decayScore ?? 50,
ruleFalsePositiveRate: stats.ruleFalsePositiveRate ?? 0,
quality: quality.overall ?? 0,
};
}
#extractSnapshot(proposal) {
for (const ev of proposal.evidence) {
if (ev.snapshotAt && ev.metrics) {
const m = ev.metrics;
return {
guardHits: m.guardHits ?? 0,
searchHits: m.searchHits ?? 0,
hitsLast30d: m.hitsLast30d ?? 0,
decayScore: m.decayScore ?? 50,
ruleFalsePositiveRate: m.ruleFalsePositiveRate ?? 0,
quality: m.quality?.overall ?? 0,
};
}
}
return null;
}
async #getRecipeLifecycle(recipeId) {
const entry = await this.#knowledgeRepo.findById(recipeId);
return entry ? { lifecycle: entry.lifecycle } : null;
}
async #transitionRecipe(recipeId, newLifecycle, trigger = 'proposal-execution', proposalId) {
if (this.#supervisor) {
const result = await this.#supervisor.transition({
recipeId,
targetState: newLifecycle,
trigger,
proposalId,
operatorId: 'system',
});
if (!result.success) {
this.#logger.warn(`[ProposalExecutor] Supervisor rejected transition ${recipeId} → ${newLifecycle}: ${result.error}`);
await this.#knowledgeRepo.updateLifecycle(recipeId, newLifecycle);
}
}
else {
await this.#knowledgeRepo.updateLifecycle(recipeId, newLifecycle);
}
}
async #restoreRecipe(recipeId) {
const current = await this.#getRecipeLifecycle(recipeId);
if (current && (current.lifecycle === 'evolving' || current.lifecycle === 'decaying')) {
await this.#transitionRecipe(recipeId, 'active');
}
}
/**
* 尝试通过 ContentPatcher 应用 Proposal 中的 suggestedChanges
* 降级容忍:无 ContentPatcher 或 patch 失败时返回 null/skipped,不阻塞状态转移
*/
async #tryApplyPatch(proposal, patchSource) {
if (!this.#contentPatcher) {
return null;
}
try {
return await this.#contentPatcher.applyProposal(proposal, patchSource);
}
catch (err) {
this.#logger.warn(`[ProposalExecutor] ContentPatcher failed for proposal ${proposal.id}: ${err instanceof Error ? err.message : String(err)}`);
return null;
}
}
async #createDeprecatedByEdge(newRecipeId, oldRecipeId) {
try {
if (this.#edgeRepo) {
await this.#edgeRepo.upsertEdge({
fromId: newRecipeId,
fromType: 'recipe',
toId: oldRecipeId,
toType: 'recipe',
relation: 'deprecated_by',
weight: 1.0,
});
}
}
catch {
// knowledge_edges 表可能不存在(降级容忍)
}
}
/* ═══════════════════ Signal ═══════════════════ */
#emitSignal(proposal, action) {
if (!this.#signalBus) {
return;
}
this.#signalBus.send('lifecycle', 'ProposalExecutor', proposal.confidence, {
target: proposal.targetRecipeId,
metadata: {
proposalId: proposal.id,
proposalType: proposal.type,
action,
source: proposal.source,
},
});
}
}
/* ────────────────────── Util ────────────────────── */
function _safeJsonParse(json, fallback) {
if (!json) {
return fallback;
}
try {
return JSON.parse(json);
}
catch {
return fallback;
}
}