'use strict'; /** * deskService — assembles Kev's daily copy-paste arsenal (Session 63 / A1-S4). * * Pulls the day's REAL pipeline data (snapshots, streaks+lens, ledger * aggregate + settled rows) and fills the VOICE v1.1 templates. Output is * the /desk pack: every format as text + pre-segmented tweets (≤280) + * 2 copy variants (with/without the Stark line) + the DATA BRIEF (structured * numbers Kev pastes into claude.ai for freeform writing). * * NOTHING auto-posts. This service only assembles. */ const media = require('./mediaEngine'); const SPORTS = ['mlb', 'wnba', 'nba', 'soccer']; function variants(text) { if (!text) return []; // Variant B: the same post without its Stark line (pure numbers). The // Stark lines are the only non-data lines, all sourced from the library. const starkSet = new Set(Object.values(media.__internals.STARK).flat()); const stripped = text.split('\n').filter((l) => !starkSet.has(l)).join('\n'); const out = [{ label: 'wire', text, tweets: media.segmentThread(text) }]; if (stripped !== text) out.push({ label: 'numbers only', text: stripped, tweets: media.segmentThread(stripped) }); return out; } /** * Assemble the pack. deps injectable: cacheGet, loadRosterLogs, * computeStreaks/computeFormHeat/applyLens, ledger (getModelAggregate + sb * reads), now. */ async function assembleDeskPack(opts = {}) { const deps = { cacheGet: opts.cacheGet || require('../utils/redis').cacheGet, loadRosterLogs: opts.loadRosterLogs || require('./rosterLogs').loadRosterLogs, streaks: opts.streaks || require('./streaksService'), lens: opts.lens || require('./streakLens'), ledger: opts.ledger || require('./ledgerService'), now: opts.now || (() => new Date().toISOString()), }; const nowIso = deps.now(); // 1. Snapshots → counts, loudest signal, signals, archetype watch, dispatches. const counts = {}; let allGrades = []; for (const sp of SPORTS) { const snap = await deps.cacheGet(`snapshot:${sp}:latest`); const grades = snap && Array.isArray(snap.grades) ? snap.grades : []; counts[sp] = grades.length; allGrades = allGrades.concat(grades.map((g) => ({ ...g, sport: sp }))); } const graded = allGrades.filter((g) => g.grade && !g.outcome); graded.sort((a, b) => (Number(b.confidence) || 0) - (Number(a.confidence) || 0)); const loudest = graded[0] || null; // 2. Streaks through the lens (MLB primary; extendable). let streakRows = []; try { const roster = await deps.loadRosterLogs('mlb'); const rows = [ ...deps.streaks.computeStreaks(roster, 'mlb', { limit: 6 }), ...deps.streaks.computeFormHeat(roster, 'mlb', { limit: 3 }), ]; const sched = await deps.cacheGet(`schedule:mlb:${nowIso.slice(0, 10)}`); streakRows = deps.lens.applyLens(rows, { scheduleGames: Array.isArray(sched) ? sched : [], pitcherGames: [] }); } catch { /* streak section degrades to absent */ } // 3. Ledger — aggregate + yesterday's settled rows for the Settle/Receipts. const aggregate = await deps.ledger.getModelAggregate({}); let settledRows = []; try { if (opts.fetchSettled) settledRows = await opts.fetchSettled(); else { const { getSupabaseServiceClient } = require('../utils/supabase'); if (deps.ledger.__internals.isConfigured()) { const sb = getSupabaseServiceClient(); const { data } = await sb.from('ledger_entries') .select('player_name, stat, line, side, grade, graded_at, locked_odds, closing_line, closing_odds, clv, clv_result, outcome, actual_value, settled_at') .is('user_id', null).not('outcome', 'is', null) .order('settled_at', { ascending: false }).limit(30); settledRows = data || []; } } } catch { /* receipts degrade to absent */ } const misses = settledRows.filter((r) => r.outcome === 'miss'); // 4. Fill the formats. Absent data → absent format, never filler. const dispatches = graded .filter((g) => g.movement && (g.movement.kind === 'steam' || g.movement.kind === 'value')) .slice(0, 4) .map((g) => media.lineDispatch(g, nowIso)) .filter(Boolean); const pack = { generated_at: nowIso, formats: { morning_wire: variants(media.morningWire({ dateIso: nowIso, counts, loudest })), signals: graded.slice(0, 3).map((g) => variants(media.signal(g))).filter((v) => v.length), streak_watch: variants(media.streakWatch(streakRows, nowIso)), settle: variants(media.theSettle({ aggregate, misses, dateIso: nowIso })), receipts: settledRows.slice(0, 3).map((r) => variants(media.receipt(r))).filter((v) => v.length), archetype_watch: variants(media.archetypeWatch(graded, nowIso)), line_dispatches: dispatches.map((d) => variants(d)), }, // 5. DATA BRIEF — the day's structured numbers for freeform writing. data_brief: { date: nowIso, slate_counts: counts, top_signals: graded.slice(0, 8).map((g) => ({ player: g.player || g.player_name, sport: g.sport, stat: g.stat_type || g.stat, line: g.line, side: g.direction, grade: g.grade, confidence: g.confidence, archetype: g.archetype || null, odds: (g.gradedAt && g.gradedAt.odds) || null, movement: g.movement || null, })), streaks: streakRows.slice(0, 8).map((r) => ({ player: r.player, team: r.team, read: r.lens && r.lens.read })), record: aggregate, settled_today: settledRows.slice(0, 20), }, }; return pack; } module.exports = { assembleDeskPack, __internals: { variants, SPORTS } };