Add evolver safety gate

This commit is contained in:
Trae AI 2026-05-26 01:18:52 +08:00
parent eedd327694
commit a96b289ba7
3 changed files with 282 additions and 0 deletions

View file

@ -3,3 +3,4 @@ export * from './metric-evaluator.js';
export * from './resource-manager.js';
export * from './variant-generator.js';
export * from './sandbox-runner.js';
export * from './safety-gate.js';

View file

@ -0,0 +1,80 @@
import type { EvaluationReport, GateDecision, VariantSpec } from './types.js';
const decidedAt = '2026-05-25T00:00:00.000Z';
export function decidePromotion(variant: VariantSpec, report: EvaluationReport): GateDecision {
const gateId = `gate-${variant.id}-${report.id}`;
if (report.safetyFindings.length > 0) {
return reject(gateId, variant, report, ['evaluation report contains safety findings']);
}
if (variant.provenance.length === 0) {
return reject(gateId, variant, report, ['variant provenance is required']);
}
if (variant.rollbackPlan.operations.length === 0 && variant.mutationType === 'parameter') {
return reject(gateId, variant, report, ['rollback operations are required for automatic promotion']);
}
if (report.verdict === 'reject') {
return reject(gateId, variant, report, ['evaluation report rejected variant']);
}
if (variant.mutationType === 'algorithm') {
return needsReview(gateId, variant, report, ['algorithm variants require review before promotion']);
}
if (variant.mutationType === 'structure') {
return needsReview(gateId, variant, report, ['structure variants require review before promotion']);
}
if (variant.riskLevel !== 'low') {
return needsReview(gateId, variant, report, ['non-low-risk variants require review before promotion']);
}
if (report.verdict !== 'promote') {
return needsReview(gateId, variant, report, ['evaluation report does not recommend promotion']);
}
return {
id: gateId,
variantId: variant.id,
reportId: report.id,
decision: 'allow',
reasons: ['parameter variant passed promotion gate'],
decidedAt,
};
}
function reject(
id: string,
variant: VariantSpec,
report: EvaluationReport,
reasons: string[],
): GateDecision {
return {
id,
variantId: variant.id,
reportId: report.id,
decision: 'reject',
reasons,
decidedAt,
};
}
function needsReview(
id: string,
variant: VariantSpec,
report: EvaluationReport,
reasons: string[],
): GateDecision {
return {
id,
variantId: variant.id,
reportId: report.id,
decision: 'needs-review',
reasons,
decidedAt,
};
}

View file

@ -0,0 +1,201 @@
import { describe, expect, it } from 'vitest';
import { decidePromotion } from '../../../src/core/evolver/index.js';
import type { EvaluationReport, VariantSpec } from '../../../src/core/evolver/index.js';
const capturedAt = '2026-05-25T00:00:00.000Z';
function parameterVariant(overrides: Partial<VariantSpec> = {}): VariantSpec {
return {
id: 'variant-parameter-1',
planId: 'plan-1',
mutationType: 'parameter',
description: 'Adjust topK',
patch: {
operations: [
{
op: 'setParameter',
path: 'retrieval.topK',
previousValue: 5,
nextValue: 8,
},
],
},
expectedGain: [
{
metricId: 'task_success_rate',
expectedDelta: 0.05,
direction: 'increase',
},
],
riskLevel: 'low',
rollbackPlan: {
strategy: 'restore-previous-parameters',
operations: [
{
op: 'setParameter',
path: 'retrieval.topK',
previousValue: 8,
nextValue: 5,
},
],
},
provenance: [
{
source: 'synthetic-benchmark',
reference: 'run-1',
capturedAt,
},
],
...overrides,
};
}
function promoteReport(overrides: Partial<EvaluationReport> = {}): EvaluationReport {
return {
id: 'report-1',
variantId: 'variant-parameter-1',
baselineId: 'baseline-1',
metricDeltas: [
{
metricId: 'task_success_rate',
baselineValue: 0.72,
candidateValue: 0.81,
delta: 0.09,
direction: 'increase',
passed: true,
},
],
regressions: [],
resourceCost: {
durationMs: 120,
computeUnits: 1,
apiCostUsd: 0,
storageBytes: 256,
},
safetyFindings: [],
verdict: 'promote',
...overrides,
};
}
describe('decidePromotion', () => {
it('allows a low-risk parameter variant with valid evaluation, provenance, rollback, and safety checks', () => {
const decision = decidePromotion(parameterVariant(), promoteReport());
expect(decision).toEqual({
id: 'gate-variant-parameter-1-report-1',
variantId: 'variant-parameter-1',
reportId: 'report-1',
decision: 'allow',
reasons: ['parameter variant passed promotion gate'],
decidedAt: capturedAt,
});
});
it('marks an algorithm variant as needs-review', () => {
const variant = parameterVariant({
id: 'variant-algorithm-1',
mutationType: 'algorithm',
riskLevel: 'medium',
patch: {
operations: [
{
op: 'selectAlgorithm',
policyId: 'ranking-policy',
previousAlgorithm: 'baseline-ranker',
nextAlgorithm: 'candidate-ranker',
},
],
},
rollbackPlan: {
strategy: 'disable-candidate',
operations: [
{
op: 'selectAlgorithm',
policyId: 'ranking-policy',
previousAlgorithm: 'candidate-ranker',
nextAlgorithm: 'baseline-ranker',
},
],
},
});
const decision = decidePromotion(
variant,
promoteReport({ id: 'report-algorithm', variantId: variant.id, verdict: 'needs-review' }),
);
expect(decision.decision).toBe('needs-review');
expect(decision.reasons).toEqual(['algorithm variants require review before promotion']);
});
it('marks a structure variant as needs-review', () => {
const variant = parameterVariant({
id: 'variant-structure-1',
mutationType: 'structure',
riskLevel: 'high',
patch: {
operations: [
{
op: 'proposeStructureChange',
planPath: 'docs/aegis/plans/structure-change.md',
summary: 'Split evaluator into a plugin pipeline',
},
],
},
rollbackPlan: {
strategy: 'manual-review-required',
operations: [],
},
});
const decision = decidePromotion(
variant,
promoteReport({ id: 'report-structure', variantId: variant.id, verdict: 'needs-review' }),
);
expect(decision.decision).toBe('needs-review');
expect(decision.reasons).toEqual(['structure variants require review before promotion']);
});
it('rejects a variant without provenance', () => {
const decision = decidePromotion(parameterVariant({ provenance: [] }), promoteReport());
expect(decision.decision).toBe('reject');
expect(decision.reasons).toEqual(['variant provenance is required']);
});
it('rejects a variant without rollback operations', () => {
const decision = decidePromotion(
parameterVariant({
rollbackPlan: {
strategy: 'restore-previous-parameters',
operations: [],
},
}),
promoteReport(),
);
expect(decision.decision).toBe('reject');
expect(decision.reasons).toEqual(['rollback operations are required for automatic promotion']);
});
it('rejects a variant when the evaluation report contains safety findings', () => {
const decision = decidePromotion(
parameterVariant(),
promoteReport({
safetyFindings: [
{
id: 'safety-1',
severity: 'high',
message: 'unsafe action attempted',
},
],
verdict: 'reject',
}),
);
expect(decision.decision).toBe('reject');
expect(decision.reasons).toEqual(['evaluation report contains safety findings']);
});
});