mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-05 02:43:32 +00:00
Add evolver safety gate
This commit is contained in:
parent
eedd327694
commit
a96b289ba7
3 changed files with 282 additions and 0 deletions
|
|
@ -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';
|
||||
|
|
|
|||
80
gitnexus/src/core/evolver/safety-gate.ts
Normal file
80
gitnexus/src/core/evolver/safety-gate.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
201
gitnexus/test/unit/evolver/safety-gate.test.ts
Normal file
201
gitnexus/test/unit/evolver/safety-gate.test.ts
Normal 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']);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue