0e7871ac0e
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>
127 lines
5.5 KiB
JavaScript
127 lines
5.5 KiB
JavaScript
'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 } };
|