Session 29: Content generation templates — slate threads, POTD, recaps, matchup previews (1660 tests)
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
'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 },
|
||||
};
|
||||
Reference in New Issue
Block a user