Files
vyndr/src/services/mediaEngine.js
T
builtbykev 17fb981f99 P0 fix: content/ not in image crashed API boot; harden garnish + preflight
ROOT CAUSE: the Dockerfile copied src/poller/scripts/supabase but NOT
content/. mediaEngine.js read content/stark-lines.json with an unguarded
module-load readFileSync; ENOENT in the image threw at require time, and
via app.js → routes/desk → deskService → mediaEngine that crashed the
ENTIRE API at boot. The Coolify healthcheck rolled back to the last
healthy image (4d2b27d), so every deploy since 219167e silently served a
14-hour-old build — S11 live tracking, S6 API code, the settlement boot
line, and SNAPSHOT_EXPECTED_INTERVAL were all merged but NOT running.

FIX (one train):
1. Dockerfile COPYs content/ into the runner image.
2. mediaEngine: stark-lines.json is OPTIONAL (garnish, never load-bearing)
   — loadStark() try/catch → {} → posts render without the Stark kicker,
   never a crash. Belt AND suspenders with #1.
3. src/preflight.js (§A4): boot prints '[preflight] OK' or 'DEGRADED'
   naming exactly what content/env is missing — before the healthcheck
   can fail silently. Run first in server.js.
4. Full fragility sweep: mediaEngine was the ONLY unguarded module-load
   file read; coachSignals (config/coaches.json) was already lazy +
   try/catch + copied. No others.

Verified: requiring app.js + deskService + mediaEngine with
stark-lines.json ABSENT now boots clean (reproduced the exact prod
failure). 2757 -> 2763 tests (tests/unit/bootResilience.test.js).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 13:42:18 -04:00

276 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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('Slates 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 tonights 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 },
};