'use strict'; /** * mediaEngine — VOICE v1.1 template engine (Session 63 / A1-S4). * * Deterministic templates for the wire formats (MORNING WIRE, SIGNAL, * STREAK WATCH, THE SETTLE, RECEIPTS, ARCHETYPE WATCH, LINE DISPATCH), * filled ONLY from pipeline JSON (snapshot/ledger/streaks). Templates never * compose numbers — the Data Semantics Rule extends to marketing. Record * claims obey the product's n≥20 gate. Stark-layer lines are SELECTED from * the curated committed library (content/stark-lines.json), never generated. * * Every emitted string passes lintVoice() — the banned list + the * no-exclamation law are enforced at build time by tests and at runtime by * assembly (a template that fails lint throws in dev, drops the line in prod). * * NOTHING here posts anywhere. Output goes to /desk for Kev to publish. */ const fs = require('fs'); const path = require('path'); // Session 65 (P0 deploy fix) — the Stark-layer library is a GARNISH, never // load-bearing. This used to be an unguarded module-load readFileSync; when // the file wasn't in the container image it threw at require time, which // (via app.js → routes/desk → deskService → here) crashed the WHOLE API at // boot and silently rolled the deploy back for 14 hours. A witty kicker is // not allowed to take down the record. Missing/corrupt file → empty library // → every format posts WITHOUT the Stark line (starkLine returns null), // which is a valid, still-VOICE-compliant post. Never throws. const STARK_PATH = path.join(__dirname, '..', '..', 'content', 'stark-lines.json'); function loadStark() { try { return JSON.parse(fs.readFileSync(STARK_PATH, 'utf8')); } catch (e) { console.warn(`[mediaEngine] stark-lines.json unavailable (${e.code || e.message}); posting without the Stark layer.`); return {}; } } const STARK = loadStark(); // ---- VOICE lint ----------------------------------------------------------- const BANNED_PATTERNS = [ /!/, // no exclamation points, ever /\block(s|ed)?\b/i, /🔒/, /🔥/, /💰/, /🚀/, /💯/, /we['’]?re so back/i, /it['’]?s so over/i, /\btail\b/i, /\bfade\b/i, // as calls to action — banned outright /can['’]?t lose/i, /free money/i, /guarantee/i, /RT if/i, /who['’]?s tailing/i, /\btailing\b/i, /\beating\b/i, /\bcooking\b/i, /\bGOAT\b/, ]; /** Returns [] when clean, else the matched patterns (as strings). */ function lintVoice(text) { const hits = []; for (const re of BANNED_PATTERNS) { if (re.test(String(text || ''))) hits.push(re.source); } return hits; } function assertVoice(text, context) { const hits = lintVoice(text); if (hits.length > 0) { const msg = `[mediaEngine] VOICE lint failed (${context}): ${hits.join(', ')}`; if (process.env.NODE_ENV === 'test') throw new Error(msg); console.warn(msg); return false; } return true; } // ---- helpers --------------------------------------------------------------- const sideCh = (dir) => (String(dir || 'over').toLowerCase() === 'under' ? 'u' : 'o'); function etTime(iso, withDay = false) { const d = iso ? new Date(iso) : new Date(); if (Number.isNaN(d.getTime())) return ''; const opts = withDay ? { timeZone: 'America/New_York', weekday: 'short', month: 'short', day: 'numeric' } : { timeZone: 'America/New_York', hour: 'numeric', minute: '2-digit' }; return new Intl.DateTimeFormat('en-US', opts).format(d); } /** Deterministic stark selection: day-of-year rotation, never random * (Data Semantics: reproducible output for the same inputs + date). */ function starkLine(category, dateIso) { const lines = STARK[category] || []; if (lines.length === 0) return null; const d = dateIso ? new Date(dateIso) : new Date(); const doy = Math.floor((d.getTime() - Date.UTC(d.getUTCFullYear(), 0, 0)) / 86_400_000); return lines[doy % lines.length]; } /** Segment a post into ≤280-char tweets on line boundaries. */ function segmentThread(text, limit = 280) { const lines = String(text || '').split('\n'); const tweets = []; let cur = ''; for (const line of lines) { const candidate = cur ? `${cur}\n${line}` : line; if (candidate.length > limit && cur) { tweets.push(cur); cur = line; } else { cur = candidate; } } if (cur.trim()) tweets.push(cur); return tweets; } // ---- formats --------------------------------------------------------------- /** THE MORNING WIRE. data = { dateIso, counts: {mlb, wnba, ...}, loudest: grade row|null } */ function morningWire(data = {}) { const parts = Object.entries(data.counts || {}) .filter(([, n]) => n > 0) .map(([sp, n]) => `${n} ${sp.toUpperCase()}.`); const lines = [`THE WIRE — ${etTime(data.dateIso, true)}`]; if (parts.length > 0) lines.push(parts.join(' ')); if (data.loudest && data.loudest.grade) { const l = data.loudest; const arch = l.archetype ? `a ${l.archetype} read` : `an ${l.grade} read`; lines.push(`Loudest signal: ${arch} — ${l.player} ${l.stat_type || l.stat} ${sideCh(l.direction)}${l.line} · ${l.grade}.`); } const stark = starkLine('morning', data.dateIso); if (stark) lines.push(stark); lines.push('Slate’s graded. 3 free reads a day.', 'vyndr.app'); const text = lines.join('\n'); assertVoice(text, 'morningWire'); return text; } /** SIGNAL. g = one graded snapshot row (must carry gradedAt). */ function signal(g = {}) { if (!g.player || !g.grade) return null; const lines = [ `${g.player} — ${humanStat(g.stat_type || g.stat)} ${sideCh(g.direction)}${g.line} · ${g.grade}`, ]; if (g.archetype) lines.push(`${g.archetype} read.`); const odds = g.gradedAt && g.gradedAt.odds != null ? ` at ${g.gradedAt.odds}` : ''; const ts = g.gradedAt && g.gradedAt.timestamp ? `Graded ${etTime(g.gradedAt.timestamp)} ET${odds}.` : null; if (ts) lines.push(ts); const text = lines.join('\n'); assertVoice(text, 'signal'); return text; } /** STREAK WATCH. rows = lens-applied streak rows. */ function streakWatch(rows = [], dateIso) { const real = rows.filter((r) => r && r.lens && r.lens.read).slice(0, 4); if (real.length === 0) return null; const lines = [`STREAK WATCH — ${etTime(dateIso, true)}`]; for (const r of real) lines.push(`${r.player}: ${r.lens.read}.`); const stark = starkLine('streak', dateIso); if (stark) lines.push(stark); lines.push('vyndr.app/explore'); const text = lines.join('\n'); assertVoice(text, 'streakWatch'); return text; } /** * THE SETTLE. agg = ledger model aggregate; misses = settled miss rows. * Record percentages ONLY at n≥20 (the aggregate already nulls them) — * below that, counts only, honestly labeled. */ function theSettle({ aggregate, misses = [], dateIso } = {}) { const a = aggregate || {}; const lines = [`THE SETTLE — ${etTime(dateIso, true)}`]; const tierA = a.by_tier && a.by_tier['A']; if (tierA && tierA.settled > 0) lines.push(`A-tier: ${tierA.hits}-${tierA.misses}`); if (a.settled > 0) lines.push(`All grades: ${a.hits}-${a.misses}${a.pushes ? ` (${a.pushes} push)` : ''}`); else lines.push('Nothing settled today.'); if (a.hit_pct == null && a.settled > 0) { lines.push(`Record building — ${a.settled} settled, ${a.pending || 0} pending. Percentages post at 20.`); } const missRows = misses.filter((m) => m && m.player_name).slice(0, 6); if (missRows.length > 0) { lines.push(`Misses, by name: ${missRows.map((m) => `${m.player_name} ${humanStat(m.stat)} ${sideCh(m.side)}${m.line} ❌`).join(' · ')}`); } const stark = starkLine(missRows.length > 0 ? 'settle_losing' : 'settle_winning', dateIso); if (stark) lines.push(stark); lines.push('vyndr.app/ledger'); const text = lines.join('\n'); assertVoice(text, 'theSettle'); return text; } /** RECEIPTS. row = settled ledger row with lock + close + outcome. */ function receipt(row = {}) { if (!row.player_name || !row.outcome) return null; const s = sideCh(row.side); const lines = [ `Posted ${etTime(row.graded_at)} ET: ${row.player_name} ${humanStat(row.stat)} ${s}${row.line} · ${row.grade}${row.locked_odds ? ` · ${row.locked_odds}` : ''}`, ]; if (row.closing_line != null && row.clv_result) { const clvNote = row.clv_result === 'beat' ? 'Market chased the read.' : row.clv_result === 'faded' ? 'Market went the other way.' : 'Close held.'; lines.push(`Close: ${s}${row.closing_line}${row.closing_odds ? ` at ${row.closing_odds}` : ''}. ${clvNote}`); } const mark = row.outcome === 'hit' ? '✅' : row.outcome === 'miss' ? '❌' : 'push'; lines.push(`Result: ${row.actual_value != null ? `${row.actual_value} ` : ''}${mark}`); if (row.clv_result === 'beat' && row.outcome === 'hit') { const stark = starkLine('steam', row.settled_at); if (stark) lines.push(stark); } const text = lines.join('\n'); assertVoice(text, 'receipt'); return text; } /** ARCHETYPE WATCH. grades = snapshot rows. */ function archetypeWatch(grades = [], dateIso) { const byArch = {}; for (const g of grades) { if (!g.archetype || !g.grade) continue; byArch[g.archetype] = byArch[g.archetype] || []; byArch[g.archetype].push(g); } const top = Object.entries(byArch).sort((a, b) => b[1].length - a[1].length)[0]; if (!top || top[1].length < 2) return null; const [arch, rows] = top; const lines = [ `${arch} watch: ${rows.length} on tonight’s slate.`, ...rows.slice(0, 4).map((g) => `${g.player} ${humanStat(g.stat_type || g.stat)} ${sideCh(g.direction)}${g.line} · ${g.grade}`), 'vyndr.app', ]; const text = lines.join('\n'); assertVoice(text, 'archetypeWatch'); return text; } /** LINE DISPATCH. move = intraday movement row on a graded prop. */ function lineDispatch(g = {}, nowIso) { const m = g.movement; if (!m || m.delta == null) return null; const s = sideCh(g.direction); const locked = g.gradedAt && g.gradedAt.line != null ? g.gradedAt.line : g.line; const arrow = m.currentLine > locked ? '▲' : '▼'; const kind = m.kind === 'steam' ? 'Steam.' : m.kind === 'value' ? 'Value — the read holds at the new number.' : 'Line moved.'; const lines = [ `${g.player} ${humanStat(g.stat_type || g.stat)}: ${s}${locked} → ${s}${m.currentLine} ${arrow}`, `${kind} ${etTime(nowIso)} ET.`, ]; const text = lines.join('\n'); assertVoice(text, 'lineDispatch'); return text; } const STAT_HUMAN = { total_bases: 'TB', home_runs: 'HR', hits: 'Hits', rbi: 'RBI', runs: 'Runs', strikeouts: 'Ks', stolen_bases: 'SB', earned_runs: 'ER', innings_pitched: 'IP', points: 'PTS', rebounds: 'REB', assists: 'AST', threes: '3PT', outs: 'Outs', doubles: '2B', }; function humanStat(stat) { const k = String(stat || '').toLowerCase(); return STAT_HUMAN[k] || k.replace(/_/g, ' '); } module.exports = { lintVoice, segmentThread, starkLine, morningWire, signal, streakWatch, theSettle, receipt, archetypeWatch, lineDispatch, __internals: { BANNED_PATTERNS, etTime, humanStat, assertVoice, STARK }, };