← 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 };
}