S4 (a1): the media engine — VOICE templates, /desk, Ghost drafts
4a VOICE v1.1 committed (board start); lint is EXECUTABLE — banned list + no-exclamation law enforced in the engine (throws in test, drops in prod) and locked by tests. Curly-apostrophe variants covered. 4b mediaEngine: deterministic templates (MORNING WIRE, SIGNAL, STREAK WATCH, THE SETTLE, RECEIPTS, ARCHETYPE WATCH, LINE DISPATCH) filled ONLY from snapshot/ledger/streaks JSON. Record percentages never render under n>=20 (counts + 'Record building' below). Stark layer = curated committed library (content/stark-lines.json), day-rotated selection — selected, never generated. 4c /desk (founder-only: requireAuth + DESK_OWNERS email allowlist, deny-by-default): all formats as text + <=280-char pre-segmented tweets with per-tweet copy buttons + char counts, wire/numbers-only variants, DATA BRIEF block (structured day numbers) with copy-for-claude.ai. ntfy ping after the day's first snapshot: 'Desk pack ready'. 4d ghostPublisher: DRAFTS ONLY (status:'draft' test-locked), env-gated no-op, HS256 JWT via node crypto (zero new deps). POST /api/internal/ghost/drafts saves slate preview + settle drafts. Nothing anywhere auto-posts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
'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 } };
|
||||
Reference in New Issue
Block a user