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:
Kev
2026-07-11 14:31:35 -04:00
parent d242b11b4b
commit 0e7871ac0e
12 changed files with 912 additions and 0 deletions
+126
View File
@@ -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 } };