over / src /lib /competitive /engine.ts
luguog's picture
Deploy mailbox-automation Next.js app
9646e24 verified
Raw
History Blame Contribute Delete
15.2 kB
import { nanoid } from "nanoid";
import {
EmployeeProfile,
CompetitivePlan,
CompetitiveScore,
PerformanceTrajectory,
ActionPortfolio,
ActionRecommendation,
ActionLane,
ActionStatus,
StrategyLearning,
ExperimentContract,
ExperimentVariantRole,
ActionOutcome,
StrategyLifecycleState,
BarrierType,
HCPFunnelState,
ManagerLabView,
PersonalChallenge,
AntiGamingFlag,
CompetitiveEngineState,
} from "@/types";
import { computeCompetitiveScore, getPrimaryConstraint } from "./scoring";
import { generateTrajectory } from "./trajectory";
import {
isEmployeeEligible,
assignVariant,
getActiveExperiments,
getCompletedExperiments,
} from "./experiment";
import {
getValidatedStrategies,
getExperimentalStrategies,
getStrategiesForEmployee,
} from "./learning";
import { loadEngineState, saveEngineState } from "./store";
import { createSeedState } from "./seed-data";
function nowIso(): string {
return new Date().toISOString();
}
export function getEngineState(): CompetitiveEngineState {
const existing = loadEngineState();
if (existing) return existing;
const seed = createSeedState();
saveEngineState(seed);
return seed;
}
export function updateEngineState(state: CompetitiveEngineState): void {
saveEngineState(state);
}
export function getEmployee(employeeId: string): EmployeeProfile | null {
const state = getEngineState();
return state.employees.find((e) => e.id === employeeId) ?? null;
}
export function getComparablePeers(employee: EmployeeProfile): EmployeeProfile[] {
const state = getEngineState();
return state.employees.filter(
(e) =>
e.id !== employee.id &&
e.role === employee.role &&
e.cohortTags.some((t) => employee.cohortTags.includes(t)),
);
}
function pickProvenActions(
employee: EmployeeProfile,
strategies: StrategyLearning[],
score: CompetitiveScore,
): ActionRecommendation[] {
const validated = getValidatedStrategies(strategies);
const relevant = getStrategiesForEmployee(employee, validated);
return relevant.slice(0, 4).map((s, i) => ({
id: nanoid(),
lane: "proven" as ActionLane,
title: s.action.split("—")[0]?.trim() || s.action.slice(0, 60),
description: s.patternDescription,
competitiveReason: s.observedOutcome,
whyThisAction: `Validated strategy with ${Math.round(s.confidence * 100)}% confidence across ${s.sampleSize} observations.`,
expectedEffortMin: 30 + i * 10,
expectedUpside: s.effectSize > 0.2 ? "high" : s.effectSize > 0.1 ? "moderate" : "low",
confidence: s.confidence,
strategyStatus: s.lifecycleState,
riskLevel: "low",
status: "assigned" as ActionStatus,
assignedAt: nowIso(),
}));
}
function pickPersonalizedActions(
employee: EmployeeProfile,
score: CompetitiveScore,
outcomes: ActionOutcome[],
): ActionRecommendation[] {
const actions: ActionRecommendation[] = [];
const constraint = getPrimaryConstraint(score);
if (constraint.dimension.includes("Follow-up")) {
actions.push({
id: nanoid(),
lane: "personalized",
title: "Complete eight unresolved account commitments before adding new visits",
description:
"Your follow-up completion is below peer benchmarks. Close existing commitments first.",
competitiveReason:
"Unresolved commitments degrade account trust and waste prior engagement investment.",
whyThisAction: `Your follow-up completion rate is ${constraint.currentScore}/100 vs peer average ${constraint.peerAverage}/100.`,
expectedEffortMin: 90,
expectedUpside: "high",
confidence: 0.87,
strategyStatus: "scaled",
riskLevel: "low",
status: "assigned",
assignedAt: nowIso(),
});
}
if (constraint.dimension.includes("Field-time")) {
actions.push({
id: nanoid(),
lane: "personalized",
title: "Replace two afternoon office visits with remote follow-ups",
description:
"Your remote follow-up completion is above peer average, while afternoon office access is below average.",
competitiveReason: "Reallocating low-access visits to remote follow-ups saves time without losing progression.",
whyThisAction: `Field-time efficiency is ${constraint.currentScore}/100 vs peer average ${constraint.peerAverage}/100.`,
expectedEffortMin: 0,
expectedUpside: "moderate",
confidence: 0.84,
strategyStatus: "validated",
riskLevel: "low",
status: "assigned",
assignedAt: nowIso(),
});
}
if (constraint.dimension.includes("Stakeholder")) {
actions.push({
id: nanoid(),
lane: "personalized",
title: "Identify and engage secondary stakeholders in 3 priority accounts",
description:
"Expand stakeholder diversity in accounts where primary HCP engagement has plateaued.",
competitiveReason:
"Accounts with broader stakeholder coverage progress more often than those with single-contact engagement.",
whyThisAction: `Stakeholder coverage is ${constraint.currentScore}/100 vs peer average ${constraint.peerAverage}/100.`,
expectedEffortMin: 120,
expectedUpside: "high",
confidence: 0.79,
strategyStatus: "validated",
riskLevel: "low",
status: "assigned",
assignedAt: nowIso(),
});
}
if (actions.length === 0) {
actions.push({
id: nanoid(),
lane: "personalized",
title: `Improve ${constraint.dimension.toLowerCase()} to peer average`,
description: `Your ${constraint.dimension.toLowerCase()} score of ${constraint.currentScore} is below the peer average of ${constraint.peerAverage}.`,
competitiveReason: `This is your largest correctable disadvantage.`,
whyThisAction: `Gap of ${constraint.gap} points below peer average.`,
expectedEffortMin: 60,
expectedUpside: "moderate",
confidence: 0.75,
strategyStatus: "validated",
riskLevel: "low",
status: "assigned",
assignedAt: nowIso(),
});
}
return actions.slice(0, 3);
}
function pickExperimentalActions(
employee: EmployeeProfile,
experiments: ExperimentContract[],
existingAssignments: Map<string, ExperimentVariantRole>,
): ActionRecommendation[] {
const active = getActiveExperiments(experiments);
const actions: ActionRecommendation[] = [];
for (const exp of active) {
if (!isEmployeeEligible(employee, exp)) continue;
if (actions.length >= 2) break;
const variant = assignVariant(employee, exp, existingAssignments);
if (!variant) continue;
const variantData = exp.variants.find((v) => v.role === variant);
if (!variantData) continue;
actions.push({
id: nanoid(),
lane: "experimental",
title: variantData.description,
description: exp.hypothesis,
competitiveReason: exp.description,
whyThisAction: `Your account mix matches the experiment criteria. You were selected for the ${variant.replace("_", " ")} group.`,
expectedEffortMin: 35,
expectedUpside: "moderate",
confidence: 0.61,
strategyStatus: "limited_experiment",
experimentId: exp.id,
experimentVariant: variant,
experimentDurationDays: exp.durationDays,
experimentProtections: exp.guardrails,
riskLevel: "low",
status: "assigned",
assignedAt: nowIso(),
});
}
return actions;
}
function computePortfolioSplit(employee: EmployeeProfile): {
proven: number;
personalized: number;
experimental: number;
} {
switch (employee.experienceLevel) {
case "new":
return { proven: 80, personalized: 20, experimental: 0 };
case "intermediate":
return { proven: 60, personalized: 25, experimental: 15 };
case "expert":
return { proven: 50, personalized: 30, experimental: 20 };
case "elite":
return { proven: 40, personalized: 30, experimental: 30 };
default:
return { proven: 60, personalized: 25, experimental: 15 };
}
}
export function generateCompetitivePlan(employeeId: string): CompetitivePlan | null {
const state = getEngineState();
const employee = state.employees.find((e) => e.id === employeeId);
if (!employee) return null;
const peers = getComparablePeers(employee);
const comparisonGroup = peers.length > 0 ? peers : state.employees.filter((e) => e.id !== employee.id);
const score = computeCompetitiveScore(
employee,
[employee, ...comparisonGroup],
state.outcomes,
state.strategies,
);
const trajectory = generateTrajectory(employee, score, state.outcomes, state.strategies);
const provenActions = pickProvenActions(employee, state.strategies, score);
const personalizedActions = pickPersonalizedActions(employee, score, state.outcomes);
const existingAssignments = new Map<string, ExperimentVariantRole>();
for (const outcome of state.outcomes) {
if (outcome.employeeId === employee.id && outcome.variant) {
existingAssignments.set(employee.id, outcome.variant);
}
}
const experimentalActions = pickExperimentalActions(
employee,
state.experiments,
existingAssignments,
);
const split = computePortfolioSplit(employee);
const allActions = [...provenActions, ...personalizedActions, ...experimentalActions];
const portfolio: ActionPortfolio = {
employeeId: employee.id,
provenPercent: split.proven,
personalizedPercent: split.personalized,
experimentalPercent: split.experimental,
actions: allActions,
generatedAt: nowIso(),
};
const constraint = getPrimaryConstraint(score);
const bestAction = allActions[0];
return {
employeeId: employee.id,
currentPositionPercentile: score.adjustedPercentile,
expectedPosition30Day: trajectory.expectedPercentile30Day,
primaryPerformanceConstraint: constraint.dimension,
constraintDescription: trajectory.constraintDescription,
bestNextAction: bestAction?.title ?? "No actions available",
expectedEffect: bestAction
? `+${Math.round(bestAction.confidence * 10)}-${Math.round(bestAction.confidence * 15)}% account progression probability`
: "Insufficient data",
evidenceConfidence: bestAction?.confidence ?? 0,
portfolio,
trajectory,
score,
generatedAt: nowIso(),
};
}
export function recordActionOutcome(input: {
actionId: string;
employeeId: string;
experimentId?: string;
variant?: ExperimentVariantRole;
actionTaken: string;
outcome: ActionOutcome["outcome"];
timeToOutcomeHours: number;
context: ActionOutcome["context"];
}): ActionOutcome {
const state = getEngineState();
const outcome: ActionOutcome = {
id: nanoid(),
...input,
capturedAt: nowIso(),
};
state.outcomes.push(outcome);
const actionIdx = state.actionHistory.findIndex((a) => a.id === input.actionId);
if (actionIdx >= 0) {
state.actionHistory[actionIdx] = {
...state.actionHistory[actionIdx],
status: "completed",
completedAt: nowIso(),
};
}
saveEngineState(state);
return outcome;
}
export function updateActionStatus(
actionId: string,
status: ActionStatus,
feedback?: string,
): ActionRecommendation | null {
const state = getEngineState();
const idx = state.actionHistory.findIndex((a) => a.id === actionId);
if (idx >= 0) {
state.actionHistory[idx] = {
...state.actionHistory[idx],
status,
employeeFeedback: feedback ?? state.actionHistory[idx].employeeFeedback,
};
saveEngineState(state);
return state.actionHistory[idx];
}
const updated: ActionRecommendation = {
id: actionId,
lane: "proven",
title: "Action",
description: "",
competitiveReason: "",
whyThisAction: "",
expectedEffortMin: 0,
expectedUpside: "low",
confidence: 0,
strategyStatus: "proposed",
riskLevel: "low",
status,
employeeFeedback: feedback,
assignedAt: nowIso(),
};
state.actionHistory.push(updated);
saveEngineState(state);
return updated;
}
export function getManagerLabView(managerId: string): ManagerLabView | null {
const state = getEngineState();
const manager = state.employees.find((e) => e.id === managerId);
if (!manager || manager.role !== "regional_manager") return null;
const validated = getValidatedStrategies(state.strategies);
const experimental = getExperimentalStrategies(state.strategies);
const activeExperiments = getActiveExperiments(state.experiments);
const stopped = state.experiments.filter(
(e) => e.status === "stopped" || e.status === "analyzed",
);
const promising = experimental.filter((s) => s.confidence > 0.55);
const topStrategy = promising.sort((a, b) => b.confidence - a.confidence)[0];
const fieldReps = state.employees.filter((e) => e.role === "field_representative");
const employeeDevelopment = fieldReps.map((rep) => {
const repOutcomes = state.outcomes.filter((o) => o.employeeId === rep.id);
const score = computeCompetitiveScore(rep, state.employees, state.outcomes, state.strategies);
const constraint = getPrimaryConstraint(score);
const bestDim = [...score.dimensions].sort((a, b) => b.score - a.score)[0];
return {
employeeId: rep.id,
name: rep.name,
primaryStrength: bestDim.label,
primaryConstraint: constraint.dimension,
currentIntervention: repOutcomes.some((o) => o.experimentId)
? "Automated commitment queue + experiment participation"
: "Standard coaching",
observedEffect: `${repOutcomes.length} tracked outcomes, ${repOutcomes.filter((o) => o.outcome === "account_progressed").length} progressed`,
};
});
return {
validatedStrategies: validated.length,
activeExperiments: activeExperiments.length,
promisingStrategies: promising.length,
stoppedStrategies: stopped.length,
topEmergingAdvantage: topStrategy?.action ?? "No emerging advantages",
measuredEffect: topStrategy?.observedOutcome ?? "N/A",
confidence: topStrategy?.confidence ?? 0,
eligibleTerritories: fieldReps.length,
employeeDevelopment,
};
}
export function getPersonalChallenge(employeeId: string): PersonalChallenge | null {
const state = getEngineState();
return state.challenges.find((c) => c.employeeId === employeeId && c.status === "active") ?? null;
}
export function getAntiGamingFlags(employeeId?: string): AntiGamingFlag[] {
const state = getEngineState();
return employeeId
? state.antiGamingFlags.filter((f) => f.employeeId === employeeId)
: state.antiGamingFlags;
}
export function detectAntiGaming(employeeId: string): AntiGamingFlag | null {
const state = getEngineState();
const empOutcomes = state.outcomes.filter((o) => o.employeeId === employeeId);
const duplicateCount = empOutcomes.length - new Set(empOutcomes.map((o) => o.actionTaken)).size;
if (duplicateCount > 3) {
const flag: AntiGamingFlag = {
type: "duplicate_engagement",
employeeId,
detail: `${duplicateCount} duplicate engagements detected. Possible activity inflation.`,
severity: "moderate",
detectedAt: nowIso(),
};
state.antiGamingFlags.push(flag);
saveEngineState(state);
return flag;
}
return null;
}