← back to Small Business Builder
src/lib/audit-grade.js
55 lines
// Single source of truth for the audit grade. Used by:
// - /wa/:slug result page (analysis only)
// - /builds index page (analysis + build row signals)
//
// Signals are additive; cap at 100. The rubric rewards real-world quality
// signals — public URL, live pm2 process, recent git commit, full meta — so the
// page isn't dominated by a single letter when synthesized analyses uniformly
// have only title+tagline.
export function computeGrade({ analysis = null, build = null } = {}) {
const meta = analysis?.meta_json || {};
const summary = analysis?.summary_json || {};
let score = 0;
const reasons = [];
// --- analysis-side signals ----------------------------------------------
if (summary.title || meta.title) { score += 10; reasons.push('title +10'); }
const tagline = summary.tagline || meta.tagline || '';
if (tagline.length >= 40) { score += 15; reasons.push('tagline≥40 +15'); }
else if (tagline.length > 0) { score += 8; reasons.push('tagline +8'); }
if (Array.isArray(meta.palette) && meta.palette.length >= 2) { score += 10; reasons.push('palette +10'); }
if (meta.hero_image) { score += 10; reasons.push('hero +10'); }
if (analysis?.ig_handle) { score += 5; reasons.push('ig +5'); }
if (analysis?.vertical && analysis.vertical !== 'generic') { score += 10; reasons.push('vertical +10'); }
if (analysis && !meta.iframe_blocked) { score += 5; reasons.push('embedable +5'); }
// --- live HTML audit (only when audit-public-sites.mjs has run) ---------
// Fold real cheerio-derived score into the grade. Worth up to +20 because
// signals like missing viewport / no h1 / bad title length are objective.
const live = summary.live_audit?.grade?.score;
if (Number.isFinite(live)) {
const liveBoost = Math.round(live * 0.2); // 0-20
score += liveBoost;
reasons.push(`live-audit ${live} → +${liveBoost}`);
}
// --- build-side signals (only available on /builds) ---------------------
if (build?.url) { score += 10; reasons.push('public-url +10'); }
if (build?.pm2?.status === 'online') { score += 10; reasons.push('pm2-live +10'); }
if (build?.has_git) { score += 5; reasons.push('git +5'); }
if (build?.description_source === 'memory') { score += 5; reasons.push('memory-doc +5'); }
// recent activity: <30d=+10, <180d=+5
if (build?.last_at) {
const days = (Date.now() - Date.parse(build.last_at)) / 86400000;
if (days < 30) { score += 10; reasons.push('recent +10'); }
else if (days < 180) { score += 5; reasons.push('semi-recent +5'); }
}
if (build?.port) { score += 3; reasons.push('port +3'); }
if (score > 100) score = 100;
const letter = score >= 80 ? 'A' : score >= 65 ? 'B' : score >= 45 ? 'C' : score >= 25 ? 'D' : 'F';
const color = score >= 80 ? '#22c55e' : score >= 65 ? '#84cc16' : score >= 45 ? '#facc15' : score >= 25 ? '#f97316' : '#dc2626';
return { letter, score, color, reasons };
}