195 lines
5.8 KiB
TypeScript

/**
* Jake's Preference Model
*
* Builds a statistical model of what Jake approves vs. rejects:
* - Approval rates by server type and pipeline stage
* - Decision speed as quality signal
* - Consistent approval/rejection patterns
*/
import type {
EnrichedFeedbackEvent,
ApprovalPatterns,
} from "../types.js";
/**
* Build the complete approval patterns model from feedback events.
*/
export async function buildPreferenceModel(
events: EnrichedFeedbackEvent[]
): Promise<ApprovalPatterns> {
const decisions = events.filter((e) => e.feedback.decision);
if (decisions.length === 0) {
return {
overall: { approvalRate: 0, totalDecisions: 0, avgTimeToDecisionMs: 0 },
byServerType: {},
byStage: {},
};
}
// ─── Overall ───
const approved = decisions.filter(
(e) => e.feedback.decision?.decision === "approved"
).length;
const avgTime =
decisions.reduce((sum, e) => sum + e.meta.timeToDecisionMs, 0) /
decisions.length;
// ─── By server type ───
const byServerType: ApprovalPatterns["byServerType"] = {};
for (const event of decisions) {
const serverType = event.mcpServerType ?? "unknown";
if (!byServerType[serverType]) {
byServerType[serverType] = {
approved: 0,
rejected: 0,
needsWork: 0,
approvalRate: 0,
};
}
const entry = byServerType[serverType]!;
const d = event.feedback.decision?.decision;
if (d === "approved") entry.approved++;
else if (d === "rejected") entry.rejected++;
else if (d === "needs_work") entry.needsWork++;
}
for (const entry of Object.values(byServerType)) {
const total = entry.approved + entry.rejected + entry.needsWork;
entry.approvalRate = total > 0 ? entry.approved / total : 0;
}
// ─── By stage ───
const byStage: ApprovalPatterns["byStage"] = {};
for (const event of decisions) {
const stage = event.pipelineStage ?? "unknown";
if (!byStage[stage]) {
byStage[stage] = { approvalRate: 0, avgTimeMs: 0, count: 0 };
}
const entry = byStage[stage]!;
entry.count++;
}
// Compute rates per stage
for (const [stage, entry] of Object.entries(byStage)) {
const stageEvents = decisions.filter(
(e) => (e.pipelineStage ?? "unknown") === stage
);
const stageApproved = stageEvents.filter(
(e) => e.feedback.decision?.decision === "approved"
).length;
entry.approvalRate =
stageEvents.length > 0 ? stageApproved / stageEvents.length : 0;
entry.avgTimeMs =
stageEvents.length > 0
? stageEvents.reduce((sum, e) => sum + e.meta.timeToDecisionMs, 0) /
stageEvents.length
: 0;
}
return {
overall: {
approvalRate: approved / decisions.length,
totalDecisions: decisions.length,
avgTimeToDecisionMs: avgTime,
},
byServerType,
byStage,
};
}
/**
* Analyze decision speed patterns:
* - Fast approvals (< 30s) → Buba nailed it
* - Fast rejections (< 30s) → Obvious flaw
* - Slow approvals (> 5min) → Edge case
* - Slow rejections (> 5min) → Complex problem
*/
export function analyzeDecisionSpeed(events: EnrichedFeedbackEvent[]): {
fastApprovals: number;
fastRejections: number;
slowApprovals: number;
slowRejections: number;
avgApprovalTimeMs: number;
avgRejectionTimeMs: number;
} {
const FAST_THRESHOLD = 30000; // 30 seconds
const SLOW_THRESHOLD = 300000; // 5 minutes
const approvals = events.filter(
(e) => e.feedback.decision?.decision === "approved"
);
const rejections = events.filter(
(e) => e.feedback.decision?.decision === "rejected"
);
return {
fastApprovals: approvals.filter(
(e) => e.meta.timeToDecisionMs < FAST_THRESHOLD
).length,
fastRejections: rejections.filter(
(e) => e.meta.timeToDecisionMs < FAST_THRESHOLD
).length,
slowApprovals: approvals.filter(
(e) => e.meta.timeToDecisionMs > SLOW_THRESHOLD
).length,
slowRejections: rejections.filter(
(e) => e.meta.timeToDecisionMs > SLOW_THRESHOLD
).length,
avgApprovalTimeMs:
approvals.length > 0
? approvals.reduce((sum, e) => sum + e.meta.timeToDecisionMs, 0) /
approvals.length
: 0,
avgRejectionTimeMs:
rejections.length > 0
? rejections.reduce((sum, e) => sum + e.meta.timeToDecisionMs, 0) /
rejections.length
: 0,
};
}
/**
* Identify patterns Jake consistently approves or rejects.
* These are high-value signals for the pre-check system.
*/
export function identifyConsistentPatterns(events: EnrichedFeedbackEvent[]): {
alwaysApproved: string[]; // Themes/patterns with 100% approval rate
alwaysRejected: string[]; // Themes/patterns with 0% approval rate
highApproval: string[]; // >90% approval rate
lowApproval: string[]; // <30% approval rate
} {
const themeOutcomes = new Map<string, { approved: number; total: number }>();
for (const event of events) {
const themes = event._themes ?? [];
const isApproved = event.feedback.decision?.decision === "approved";
for (const theme of themes) {
if (!themeOutcomes.has(theme)) {
themeOutcomes.set(theme, { approved: 0, total: 0 });
}
const entry = themeOutcomes.get(theme)!;
entry.total++;
if (isApproved) entry.approved++;
}
}
const alwaysApproved: string[] = [];
const alwaysRejected: string[] = [];
const highApproval: string[] = [];
const lowApproval: string[] = [];
for (const [theme, data] of themeOutcomes) {
if (data.total < 3) continue; // Need minimum data
const rate = data.approved / data.total;
if (rate === 1.0) alwaysApproved.push(theme);
else if (rate === 0.0) alwaysRejected.push(theme);
else if (rate > 0.9) highApproval.push(theme);
else if (rate < 0.3) lowApproval.push(theme);
}
return { alwaysApproved, alwaysRejected, highApproval, lowApproval };
}