195 lines
5.8 KiB
TypeScript
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 };
|
|
}
|