← back to Rentv Adintel
lib/scoring.js
57 lines
'use strict';
/**
* Advertising Opportunity Score (spec §13).
* Ranks RENTV *sales opportunity* — NOT personal worth, NOT a secret ad-spend
* estimate. Every factor is transparent and the weights are overridable by an
* admin. Do not relabel the output as an ad-spend estimate.
*/
// Default weights — must sum to 1.0. Admin-overridable (§13).
const DEFAULT_WEIGHTS = Object.freeze({
verifiedAdvertising: 0.22,
verifiedConferenceSpendSignal: 0.15,
recency: 0.12,
repeatActivity: 0.1,
californiaFit: 0.1,
arizonaFit: 0.05,
categoryFit: 0.08,
rentvAudienceFit: 0.08,
contactCompleteness: 0.04,
evidenceQuality: 0.06,
});
const FACTOR_KEYS = Object.freeze(Object.keys(DEFAULT_WEIGHTS));
const clampScore = (value) => Math.max(0, Math.min(100, Number(value) || 0));
/**
* @param {Object} input each factor 0-100
* @param {Object} [weights] optional weight overrides (partial ok)
* @returns {number} integer 0-100
*/
function calculateAdvertiserOpportunityScore(input, weights) {
const w = { ...DEFAULT_WEIGHTS, ...(weights || {}) };
let total = 0;
for (const k of FACTOR_KEYS) total += clampScore(input[k]) * w[k];
return Math.round(total);
}
/** Returns per-factor contribution so the UI can "Show Me Why" (§25). */
function explainScore(input, weights) {
const w = { ...DEFAULT_WEIGHTS, ...(weights || {}) };
const parts = FACTOR_KEYS.map((k) => {
const value = clampScore(input[k]);
const weight = w[k];
return { factor: k, value, weight, contribution: Math.round(value * weight * 100) / 100 };
});
return { score: calculateAdvertiserOpportunityScore(input, weights), factors: parts, weights: w };
}
module.exports = {
DEFAULT_WEIGHTS,
FACTOR_KEYS,
clampScore,
calculateAdvertiserOpportunityScore,
explainScore,
};