over / src /lib /competitive /learning.ts
luguog's picture
Deploy mailbox-automation Next.js app
9646e24 verified
Raw
History Blame Contribute Delete
4.76 kB
import { nanoid } from "nanoid";
import {
StrategyLearning,
StrategyLifecycleState,
ActionOutcome,
StrategyContextKey,
EmployeeProfile,
} from "@/types";
function nowIso(): string {
return new Date().toISOString();
}
export function learnFromOutcomes(
outcomes: ActionOutcome[],
existingStrategies: StrategyLearning[],
): StrategyLearning[] {
const newLearnings: StrategyLearning[] = [];
const grouped = new Map<string, ActionOutcome[]>();
for (const outcome of outcomes) {
const key = contextKeyToString(outcome.context);
if (!grouped.has(key)) {
grouped.set(key, []);
}
grouped.get(key)!.push(outcome);
}
for (const [key, groupOutcomes] of grouped) {
const total = groupOutcomes.length;
if (total < 3) continue;
const positive = groupOutcomes.filter(
(o) =>
o.outcome === "account_progressed" ||
o.outcome === "barrier_resolved" ||
o.outcome === "meaningful_response" ||
o.outcome === "follow_up_completed",
).length;
const successRate = positive / total;
if (successRate < 0.3) continue;
const context = groupOutcomes[0].context;
const action = groupOutcomes[0].actionTaken;
const existing = existingStrategies.find(
(s) =>
contextKeyToString(s.context) === key &&
s.action.toLowerCase() === action.toLowerCase(),
);
if (existing) {
continue;
}
const lifecycleState: StrategyLifecycleState =
successRate > 0.7 && total >= 10
? "validated"
: successRate > 0.5 && total >= 5
? "limited_experiment"
: "proposed";
newLearnings.push({
id: nanoid(),
context,
action,
observedOutcome: `${Math.round(successRate * 100)}% positive outcome rate across ${total} observations`,
effectSize: Math.round(successRate * 100) / 100,
confidence: Math.min(0.99, 0.40 + (total / 20) * 0.3),
sampleSize: total,
lifecycleState,
discoveredAt: nowIso(),
lastValidatedAt: nowIso(),
patternDescription: `Observed pattern: ${action} in ${key} produces ${Math.round(successRate * 100)}% positive outcomes.`,
});
}
return newLearnings;
}
function contextKeyToString(ctx: StrategyContextKey): string {
return [
ctx.territoryMaturity || "any",
ctx.barrierType || "any",
ctx.accountState || "any",
ctx.employeeExperience || "any",
ctx.channel || "any",
].join("|");
}
export function getValidatedStrategies(strategies: StrategyLearning[]): StrategyLearning[] {
return strategies.filter(
(s) =>
s.lifecycleState === "validated" ||
s.lifecycleState === "scaled" ||
s.lifecycleState === "monitored",
);
}
export function getExperimentalStrategies(strategies: StrategyLearning[]): StrategyLearning[] {
return strategies.filter(
(s) =>
s.lifecycleState === "limited_experiment" ||
s.lifecycleState === "shadow_tested" ||
s.lifecycleState === "simulated",
);
}
export function getStrategiesForEmployee(
employee: EmployeeProfile,
strategies: StrategyLearning[],
): StrategyLearning[] {
return strategies.filter(
(s) =>
s.context.territoryMaturity === employee.territoryMaturity ||
s.context.employeeExperience === employee.experienceLevel ||
!s.context.territoryMaturity,
);
}
export function advanceLifecycle(
strategy: StrategyLearning,
outcomes: ActionOutcome[],
): StrategyLearning {
const relevantOutcomes = outcomes.filter(
(o) => contextKeyToString(o.context) === contextKeyToString(strategy.context),
);
if (relevantOutcomes.length < 5) return strategy;
const positive = relevantOutcomes.filter(
(o) =>
o.outcome === "account_progressed" ||
o.outcome === "barrier_resolved" ||
o.outcome === "meaningful_response",
).length;
const successRate = positive / relevantOutcomes.length;
const newState: StrategyLifecycleState =
successRate > 0.7 && strategy.sampleSize >= 20
? "validated"
: successRate > 0.5 && strategy.sampleSize >= 10
? "scaled"
: strategy.lifecycleState;
return {
...strategy,
lifecycleState: newState,
sampleSize: strategy.sampleSize + relevantOutcomes.length,
confidence: Math.min(0.99, strategy.confidence + 0.05),
lastValidatedAt: nowIso(),
};
}
export function retireStaleStrategies(
strategies: StrategyLearning[],
maxAgeDays: number = 180,
): StrategyLearning[] {
const cutoff = Date.now() - maxAgeDays * 86400000;
return strategies.map((s) => {
if (new Date(s.lastValidatedAt).getTime() < cutoff && s.lifecycleState !== "retired") {
return { ...s, lifecycleState: "retired" as StrategyLifecycleState };
}
return s;
});
}