'use strict'; /** * Content formatter (Session 29). * * Transforms structured content objects (from contentTemplateService) into * platform-ready PLAIN TEXT โ€” suitable for X threads / Telegram. Image * formatting is a separate design-layer concern. Pure + defensive: a * missing field renders as a sensible blank, never "undefined". */ function fmtOdds(o) { const n = Number(o); if (!Number.isFinite(n)) return String(o ?? ''); return n > 0 ? `+${n}` : `${n}`; } function fmtTime(iso) { if (!iso) return ''; try { return new Date(iso).toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' }); } catch { return String(iso); } } function formatPickPost(p) { const lines = [`${p.player} ยท ${p.stat} ${p.side || ''} ${p.line ?? ''}`.replace(/\s+/g, ' ').trim()]; lines.push(`Grade: ${p.grade}${p.confidence ? ` ยท ${p.confidence}% confidence` : ''}`); if (p.edge) lines.push(`Edge: ${p.edge > 0 ? '+' : ''}${p.edge}%`); if (p.analysis) lines.push(p.analysis); return lines.join('\n'); } function formatGameHighlight(p) { const parts = [`${p.game}${p.time ? ` ยท ${fmtTime(p.time)}` : ''}`]; if (p.bestAwayML) parts.push(`Best away ML: ${fmtOdds(p.bestAwayML.odds)} (${p.bestAwayML.book})`); if (p.bestHomeML) parts.push(`Best home ML: ${fmtOdds(p.bestHomeML.odds)} (${p.bestHomeML.book})`); if (p.total != null) parts.push(`O/U ${p.total}`); if (p.bookCount) parts.push(`${p.bookCount} books`); return parts.join('\n'); } function formatMovers(p) { const head = '๐Ÿ“ˆ Biggest line moves:'; const rows = (p.movers || []).map((m) => { const d = m.delta > 0 ? `+${m.delta}` : `${m.delta}`; return `${m.player} ${String(m.stat || '').replace(/_/g, ' ')}: ${m.opening ?? '?'} โ†’ ${m.current ?? '?'} (${d})${m.sharpSignal ? ' โšก' : ''}`; }); return [head, ...rows].join('\n'); } function formatSchedule(p) { return (p.games || []) .map((g) => `${g.away || '?'} @ ${g.home || '?'}${g.time ? ` ยท ${fmtTime(g.time)}` : ''}`) .join('\n'); } function formatPost(post) { switch (post.role) { case 'hook': return post.text || ''; case 'cta': return post.text || ''; case 'pick': return formatPickPost(post); case 'game_highlight': return formatGameHighlight(post); case 'movers': return formatMovers(post); case 'schedule': return formatSchedule(post); default: return ''; } } /** Returns an array of post-ready strings (one per thread post). */ function formatSlateThread(thread) { if (!thread || !Array.isArray(thread.posts)) return []; return thread.posts.map(formatPost); } /** Single-block POTD text. */ function formatPOTD(potd) { if (!potd || potd.available === false) return 'No standout pick today โ€” check the full slate at vyndr.app.'; if (potd.dataLevel === 'lines') { return `๐ŸŽฏ GAME OF THE DAY\n${potd.game}${potd.time ? ` ยท ${fmtTime(potd.time)}` : ''}\n${potd.total != null ? `O/U ${potd.total}` : ''}${potd.bestHomeML ? `\nBest home ML ${fmtOdds(potd.bestHomeML.odds)} (${potd.bestHomeML.book})` : ''}`.trim(); } return `๐ŸŽฏ PROP OF THE DAY\n${formatPickPost(potd)}`; } /** Recap text block. */ function formatRecap(recap) { if (!recap || recap.available === false) return 'No graded results in yet today.'; const { record, winRate } = recap; const lines = [`๐Ÿ“Š VYNDR RECAP โ€” ${recap.date}`]; lines.push(`Record: ${record.wins}-${record.losses}${record.pushes ? `-${record.pushes}` : ''}${winRate != null ? ` (${Math.round(winRate * 100)}%)` : ''}`); if (recap.topHits && recap.topHits.length) { lines.push('Top hits:'); for (const h of recap.topHits) lines.push(`โœ… ${h.player} ${h.stat} ${h.side || ''} ${h.line ?? ''}`.replace(/\s+/g, ' ').trim()); } if (recap.metrics && recap.metrics.brierScore != null) lines.push(`Brier: ${recap.metrics.brierScore}`); return lines.join('\n'); } module.exports = { formatSlateThread, formatPOTD, formatRecap, __internals: { formatPost, formatPickPost, formatGameHighlight, formatMovers, formatSchedule }, };