///
import {
type Cell,
cell,
Default,
derive,
handler,
lift,
recipe,
str,
} from "commontools";
interface LeadInput {
id?: string;
name?: string;
base?: number;
signals?: Record;
}
interface SignalWeightInput {
signal?: string;
label?: string;
weight?: number;
}
const defaultSignalWeights: SignalWeightInput[] = [
{ signal: "engagement", label: "Engagement", weight: 2 },
{ signal: "fit", label: "Product Fit", weight: 3 },
{ signal: "timing", label: "Timing", weight: 1 },
];
interface LeadScoringArgs {
leads: Default;
signalWeights: Default<
SignalWeightInput[],
typeof defaultSignalWeights
>;
defaultWeight: Default;
}
interface LeadState {
id: string;
name: string;
base: number;
signals: Record;
}
interface SignalWeightState {
signal: string;
label: string;
weight: number;
}
interface LeadSignalBreakdown {
signal: string;
label: string;
count: number;
weight: number;
contribution: number;
}
interface LeadScoreSummary extends LeadState {
score: number;
signalBreakdown: LeadSignalBreakdown[];
}
interface SignalAggregate {
signal: string;
label: string;
totalCount: number;
weightedTotal: number;
}
interface SignalMutationEvent {
leadId?: string;
signal?: string;
delta?: number;
set?: number;
weight?: number;
label?: string;
}
const roundTwo = (value: number): number => {
return Math.round(value * 100) / 100;
};
const sanitizeLabel = (value: unknown, fallback: string): string => {
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return fallback;
};
const slugify = (value: string, fallback: string): string => {
const slug = value
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "");
return slug.length > 0 ? slug : fallback;
};
const normalizeName = (value: unknown, fallback: string): string => {
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) {
return trimmed;
}
}
return fallback;
};
const ensureUniqueId = (
id: string,
used: Set,
fallback: string,
): string => {
let candidate = id.length > 0 ? id : fallback;
let suffix = 2;
while (used.has(candidate)) {
candidate = `${id}-${suffix}`;
suffix++;
}
used.add(candidate);
return candidate;
};
const buildSignalLabel = (value: string): string => {
return value.split("-").map((part) => {
const head = part.charAt(0).toUpperCase();
return `${head}${part.slice(1)}`;
}).join(" ");
};
const sanitizeSignalKey = (value: unknown, fallback: string): string => {
if (typeof value === "string") {
const trimmed = value.trim();
if (trimmed.length > 0) {
return slugify(trimmed, fallback);
}
}
return fallback;
};
const sanitizeWeightValue = (value: unknown, fallback: number): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return roundTwo(Math.max(fallback, 0));
}
return roundTwo(Math.max(value, 0));
};
const sanitizeCountValue = (value: unknown, fallback: number): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return roundTwo(Math.max(fallback, 0));
}
return roundTwo(Math.max(value, 0));
};
const sanitizeDeltaValue = (value: unknown, fallback: number): number => {
if (typeof value !== "number" || !Number.isFinite(value)) {
return roundTwo(fallback);
}
return roundTwo(value);
};
const sanitizeSignalRecord = (
input: Record | undefined,
): Record => {
if (!input) return {};
const sanitized: Record = {};
for (const [rawKey, rawValue] of Object.entries(input)) {
const fallbackKey = rawKey.length > 0
? slugify(rawKey, "signal")
: "signal";
const signal = sanitizeSignalKey(rawKey, fallbackKey);
if (signal.length === 0) continue;
const value = sanitizeCountValue(rawValue, 0);
if (value <= 0) continue;
sanitized[signal] = value;
}
const sortedKeys = Object.keys(sanitized).sort();
const result: Record = {};
for (const key of sortedKeys) {
result[key] = sanitizeCountValue(sanitized[key], 0);
}
return result;
};
const sanitizeLeadList = (
input: readonly LeadInput[] | undefined,
): LeadState[] => {
if (!Array.isArray(input)) return [];
const sanitized: LeadState[] = [];
const used = new Set();
for (let index = 0; index < input.length; index++) {
const raw = input[index];
const fallbackName = `Lead ${index + 1}`;
const name = normalizeName(raw?.name, fallbackName);
const fallbackId = slugify(fallbackName, `lead-${index + 1}`);
const idSource = typeof raw?.id === "string" && raw.id.length > 0
? raw.id
: name;
const id = ensureUniqueId(
slugify(idSource, fallbackId),
used,
fallbackId,
);
const base = sanitizeCountValue(raw?.base, 0);
sanitized.push({
id,
name,
base,
signals: sanitizeSignalRecord(raw?.signals),
});
}
sanitized.sort((left, right) => left.name.localeCompare(right.name));
return sanitized;
};
const sanitizeSignalWeights = (
input: readonly SignalWeightInput[] | undefined,
fallbackWeight: number,
): SignalWeightState[] => {
const source = Array.isArray(input) && input.length > 0
? input
: defaultSignalWeights;
const sanitized: SignalWeightState[] = [];
const used = new Set();
for (let index = 0; index < source.length; index++) {
const raw = source[index];
const fallbackSignal = `signal-${index + 1}`;
const candidate = raw?.signal ?? raw?.label ?? fallbackSignal;
const signal = sanitizeSignalKey(candidate, fallbackSignal);
if (signal.length === 0 || used.has(signal)) continue;
used.add(signal);
const label = sanitizeLabel(
raw?.label,
buildSignalLabel(signal),
);
const weight = sanitizeWeightValue(raw?.weight, fallbackWeight);
sanitized.push({ signal, label, weight });
}
if (sanitized.length === 0) {
return sanitizeSignalWeights(defaultSignalWeights, fallbackWeight);
}
sanitized.sort((left, right) => left.label.localeCompare(right.label));
return sanitized;
};
const computeWeightRecord = (
weights: readonly SignalWeightState[],
): Record => {
const record: Record = {};
for (const entry of weights) {
record[entry.signal] = entry;
}
return record;
};
const computeLeadSummaries = (
leads: readonly LeadState[],
weights: Record,
fallbackWeight: number,
): LeadScoreSummary[] => {
const known = new Set();
for (const entry of Object.keys(weights)) {
known.add(entry);
}
for (const lead of leads) {
for (const key of Object.keys(lead.signals)) {
known.add(key);
}
}
const orderedSignals = Array.from(known.values()).sort();
const summaries: LeadScoreSummary[] = [];
const sanitizedFallback = sanitizeWeightValue(fallbackWeight, 1);
for (const lead of leads) {
const breakdown: LeadSignalBreakdown[] = [];
let score = sanitizeCountValue(lead.base, 0);
for (const key of orderedSignals) {
const count = sanitizeCountValue(lead.signals[key], 0);
if (count <= 0) continue;
const match = weights[key];
const weight = match
? sanitizeWeightValue(match.weight, sanitizedFallback)
: sanitizedFallback;
const label = match
? sanitizeLabel(match.label, buildSignalLabel(key))
: buildSignalLabel(key);
const contribution = roundTwo(count * weight);
breakdown.push({ signal: key, label, count, weight, contribution });
score = roundTwo(score + contribution);
}
summaries.push({
id: lead.id,
name: lead.name,
base: sanitizeCountValue(lead.base, 0),
signals: { ...lead.signals },
score,
signalBreakdown: breakdown,
});
}
summaries.sort((left, right) => {
const delta = roundTwo(right.score - left.score);
if (Math.abs(delta) > 0.001) return delta > 0 ? 1 : -1;
return left.name.localeCompare(right.name);
});
return summaries;
};
const aggregateSignalTotals = (
summaries: readonly LeadScoreSummary[],
): SignalAggregate[] => {
const totals = new Map();
for (const summary of summaries) {
for (const entry of summary.signalBreakdown) {
const existing = totals.get(entry.signal);
if (existing) {
existing.totalCount = roundTwo(existing.totalCount + entry.count);
existing.weightedTotal = roundTwo(
existing.weightedTotal + entry.contribution,
);
} else {
totals.set(entry.signal, {
signal: entry.signal,
label: entry.label,
totalCount: entry.count,
weightedTotal: entry.contribution,
});
}
}
}
const list = Array.from(totals.values());
list.sort((left, right) => left.label.localeCompare(right.label));
return list;
};
const formatDecimal = (value: number): string => {
return roundTwo(value).toFixed(2);
};
const applySignalMutation = handler(
(
event: SignalMutationEvent | undefined,
context: {
leads: Cell;
weights: Cell;
defaultWeight: Cell;
history: Cell;
lastMutation: Cell;
sequence: Cell;
},
) => {
const fallbackWeight = sanitizeWeightValue(
context.defaultWeight.get(),
1,
);
const leads = sanitizeLeadList(context.leads.get());
const weights = sanitizeSignalWeights(
context.weights.get(),
fallbackWeight,
);
const signalFallback = `signal-${weights.length + 1}`;
const signalKey = sanitizeSignalKey(
event?.signal,
signalFallback,
);
if (signalKey.length === 0) {
context.lastMutation.set("ignored missing-signal");
return;
}
const leadId = sanitizeSignalKey(event?.leadId, "");
const targetIndex = leadId.length > 0
? leads.findIndex((lead) => lead.id === leadId)
: -1;
if (targetIndex < 0) {
context.lastMutation.set(`${signalKey} missing-lead`);
return;
}
const delta = sanitizeDeltaValue(event?.delta, 0);
const hasOverride = typeof event?.set === "number" &&
Number.isFinite(event.set);
const override = hasOverride
? sanitizeCountValue(event?.set, 0)
: undefined;
const proposedWeight = typeof event?.weight === "number"
? sanitizeWeightValue(event.weight, fallbackWeight)
: undefined;
const hasLabelOverride = typeof event?.label === "string" &&
event.label.trim().length > 0;
const overrideLabel = hasLabelOverride
? sanitizeLabel(event?.label, buildSignalLabel(signalKey))
: undefined;
const nextLeads = leads.map((lead) => ({
id: lead.id,
name: lead.name,
base: lead.base,
signals: { ...lead.signals },
}));
const target = nextLeads[targetIndex];
const currentCount = sanitizeCountValue(target.signals[signalKey], 0);
let nextCount = currentCount;
if (override !== undefined) {
nextCount = override;
} else if (delta !== 0) {
nextCount = sanitizeCountValue(currentCount + delta, currentCount);
}
const sanitizedNext = sanitizeCountValue(nextCount, 0);
let leadChanged = false;
if (sanitizedNext <= 0) {
if (signalKey in target.signals) {
delete target.signals[signalKey];
leadChanged = true;
}
} else if (Math.abs(sanitizedNext - currentCount) > 0.001) {
target.signals[signalKey] = sanitizedNext;
leadChanged = true;
}
const nextWeights = weights.map((entry) => ({ ...entry }));
const weightIndex = nextWeights.findIndex((entry) =>
entry.signal === signalKey
);
let weightsChanged = false;
const resolvedWeight = proposedWeight ??
nextWeights[weightIndex]?.weight ??
fallbackWeight;
if (weightIndex >= 0) {
const existing = nextWeights[weightIndex];
const nextWeight = sanitizeWeightValue(resolvedWeight, fallbackWeight);
const nextLabel = hasLabelOverride
? overrideLabel ?? existing.label
: existing.label;
if (
Math.abs(nextWeight - existing.weight) > 0.001 ||
nextLabel !== existing.label
) {
nextWeights[weightIndex] = {
signal: signalKey,
label: nextLabel,
weight: nextWeight,
};
weightsChanged = true;
}
} else {
nextWeights.push({
signal: signalKey,
label: overrideLabel ?? buildSignalLabel(signalKey),
weight: sanitizeWeightValue(resolvedWeight, fallbackWeight),
});
weightsChanged = true;
}
if (!leadChanged && !weightsChanged) {
context.lastMutation.set(`${leadId}>${signalKey} noop`);
return;
}
const leadPayload = nextLeads.map((lead) => ({
id: lead.id,
name: lead.name,
base: lead.base,
signals: { ...lead.signals },
}));
context.leads.set(leadPayload);
nextWeights.sort((left, right) => left.label.localeCompare(right.label));
context.weights.set(nextWeights.map((entry) => ({
signal: entry.signal,
label: entry.label,
weight: entry.weight,
})));
const descriptor: string[] = [];
if (override !== undefined) {
descriptor.push(`=${formatDecimal(sanitizedNext)}`);
} else if (leadChanged && delta !== 0) {
const prefix = delta > 0 ? "+" : "";
descriptor.push(`${prefix}${formatDecimal(delta)}`);
}
if (weightsChanged && proposedWeight !== undefined) {
descriptor.push(`w=${formatDecimal(proposedWeight)}`);
}
const message = `${leadId}>${signalKey} ${
descriptor.length > 0 ? descriptor.join(" ") : "updated"
}`;
const history = Array.isArray(context.history.get())
? [...(context.history.get() ?? [])]
: [];
history.push(message);
if (history.length > 10) {
history.splice(0, history.length - 10);
}
context.history.set(history);
context.lastMutation.set(message);
context.sequence.set(context.sequence.get() + 1);
},
);
export const leadScoring = recipe(
"Lead Scoring",
({ leads, signalWeights, defaultWeight }) => {
const history = cell([]);
const lastMutation = cell("none");
const sequence = cell(0);
const sanitizedDefaultWeight = lift((value: number | undefined) =>
sanitizeWeightValue(value, 1)
)(defaultWeight);
const sanitizedLeads = lift((value: LeadInput[] | undefined) =>
sanitizeLeadList(value)
)(leads);
const sanitizedWeights = lift((
input: {
weights: SignalWeightInput[] | undefined;
fallback: number;
},
) => sanitizeSignalWeights(input.weights, input.fallback))({
weights: signalWeights,
fallback: sanitizedDefaultWeight,
});
const weightRecord = lift(
(list: SignalWeightState[] | undefined) =>
computeWeightRecord(Array.isArray(list) ? list : []),
)(sanitizedWeights);
const leadSummaries = lift((
input: {
leads: LeadState[] | undefined;
weightMap: Record;
fallback: number;
},
) => {
const leadList = Array.isArray(input.leads) ? input.leads : [];
return computeLeadSummaries(
leadList,
input.weightMap,
input.fallback,
);
})({
leads: sanitizedLeads,
weightMap: weightRecord,
fallback: sanitizedDefaultWeight,
});
const signalSummary = lift((list: LeadScoreSummary[] | undefined) =>
aggregateSignalTotals(Array.isArray(list) ? list : [])
)(leadSummaries);
const scoreByLead = lift((list: LeadScoreSummary[] | undefined) => {
const record: Record = {};
for (const entry of list ?? []) {
record[entry.id] = entry.score;
}
return record;
})(leadSummaries);
const totalScore = lift((list: LeadScoreSummary[] | undefined) => {
let total = 0;
for (const entry of list ?? []) {
total = roundTwo(total + entry.score);
}
return total;
})(leadSummaries);
const signalTotals = derive(signalSummary, (list) => {
const record: Record = {};
for (const entry of list) {
record[entry.signal] = entry.totalCount;
}
return record;
});
const weightedSignalTotals = derive(signalSummary, (list) => {
const record: Record = {};
for (const entry of list) {
record[entry.signal] = entry.weightedTotal;
}
return record;
});
const leadCount = lift((list: LeadScoreSummary[] | undefined) =>
Array.isArray(list) ? list.length : 0
)(leadSummaries);
const signalCount = lift((list: SignalAggregate[] | undefined) =>
Array.isArray(list) ? list.length : 0
)(signalSummary);
const topLeadName = derive(
leadSummaries,
(list) => list.length > 0 ? list[0].name : "none",
);
const topLeadScore = derive(
leadSummaries,
(list) => list.length > 0 ? list[0].score : 0,
);
const topScoreLabel = lift((value: number | undefined) =>
formatDecimal(typeof value === "number" ? value : 0)
)(topLeadScore);
const summaryLabel =
str`${leadCount} leads scored; top ${topLeadName} ${topScoreLabel} across ${signalCount} signals`;
const lastMutationView = lift((value: string | undefined) => {
if (typeof value === "string" && value.trim().length > 0) {
return value;
}
return "none";
})(lastMutation);
const historyView = lift((entries: string[] | undefined) =>
Array.isArray(entries) ? [...entries] : []
)(history);
const mutationControls = {
applySignal: applySignalMutation({
leads,
weights: signalWeights,
defaultWeight: sanitizedDefaultWeight,
history,
lastMutation,
sequence,
}),
};
return {
leads: sanitizedLeads,
signalWeights: sanitizedWeights,
leaderboard: leadSummaries,
scoreByLead,
totalScore,
signalSummary,
signalTotals,
weightedSignalTotals,
leadCount,
signalCount,
topLead: topLeadName,
topScore: topLeadScore,
summaryLabel,
lastMutation: lastMutationView,
history: historyView,
controls: mutationControls,
};
},
);
export type {
LeadInput,
LeadScoreSummary,
LeadScoringArgs,
SignalAggregate,
SignalWeightInput,
};