From 74aa75945e83355b9be9ef7719e822e60cba314b Mon Sep 17 00:00:00 2001 From: Kev Date: Fri, 7 Aug 2026 16:34:57 -0400 Subject: [PATCH] Content engine: posts that structurally cannot lie MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHASE 0 — contentEngine makes Truth Law structural, not careful. Copy is token-substituted and an unbacked {token} REFUSES to render -- there is no code path that produces a plausible default. The fact contract is asserted before any string is built. Card and copy render from ONE fact object, so a caption and a card cannot disagree. No live model writes factual claims: the voice is in the template, the facts are pulled, and the voice-polish port is deliberately unwired, because an LLM that can rewrite a sentence can rewrite a number. 18 tests carry the proof. The one that matters most: ZERO IS PRESENT. "0 cleared B+" is our most honest possible post, and treating 0 as missing would be the Number(null)===0 breach wearing its opposite coat -- it would silently delete exactly the post the brand is built on. PHASE 1 — three templates, generating real posts from tonight's data: hot hitters off the repaired full-season log, the honesty flex off the real servedGrade distribution (2,140 graded / 70 cleared B+ / 42% not separable / A unissuable), and streaks verified from settled outcomes only. THE ENGINE CAUGHT A BUG IN ITSELF, and it is the sharpest lesson here. The first run published "No hitter is meaningfully hot tonight -- we could dress up a middling week as a streak. We don't." That was FALSE: the box-score cache spans only the settled window, every player had under 20 games, and the pool was empty. A broken pull was publishing as considered editorial judgement -- the fourth appearance of this class tonight and the first where our OWN HONESTY COPY was the disguise. Fixed structurally rather than by patching the number: an absent() variant may now DECLINE to speak, and the template separates "no candidates at all" (SKIP with a reason) from "candidates judged, none hot" (honest absence). Both locked by test. Source corrected to mlbStatsAdapter.fullLog, the same log the repaired champion reads. PHASE 2 — cardRenderer emits SVG rather than canvas: it is text, so it diffs in review and its numbers are greppable, which matters when the whole claim is that the numbers are real. VYND white + R green, slashed-Y, scanlines, mono. The card never formats its own facts -- every string arrives pre-rendered and gate-checked. PHASE 3 — scripts/generate-content.js writes copy + card per template to .content-out//. Template N+1 is a registry entry: requires, pull, copy, card, absent. Queued as stubs, not built: hot takes, daily reads, "grades we DIDN'T give", cross-sport streak variants (the streak template is already sport-agnostic -- settled outcomes and a noun). FULLY ISOLATED: read-only on every source, zero writes to serving, model or ledger tables. Serving fingerprint verified unchanged. The accrual clock is untouched at 0 eligible dates. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9 --- .gitignore | 2 + scripts/generate-content.js | 137 ++++++++++++++ specs/CONTENT-ENGINE.md | 111 +++++++++++ src/services/content/cardRenderer.js | 83 +++++++++ src/services/content/contentEngine.js | 172 ++++++++++++++++++ src/services/content/templates/honestyFlex.js | 73 ++++++++ src/services/content/templates/hotHitters.js | 94 ++++++++++ src/services/content/templates/streakList.js | 65 +++++++ tests/unit/contentEngine.test.js | 164 +++++++++++++++++ 9 files changed, 901 insertions(+) create mode 100644 scripts/generate-content.js create mode 100644 specs/CONTENT-ENGINE.md create mode 100644 src/services/content/cardRenderer.js create mode 100644 src/services/content/contentEngine.js create mode 100644 src/services/content/templates/honestyFlex.js create mode 100644 src/services/content/templates/hotHitters.js create mode 100644 src/services/content/templates/streakList.js create mode 100644 tests/unit/contentEngine.test.js diff --git a/.gitignore b/.gitignore index 598fa2f..5c918c5 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ out/ .vercel/ .seq-cache/ + +.content-out/ diff --git a/scripts/generate-content.js b/scripts/generate-content.js new file mode 100644 index 0000000..5454196 --- /dev/null +++ b/scripts/generate-content.js @@ -0,0 +1,137 @@ +#!/usr/bin/env node +'use strict'; + +/** + * generate-content — tonight's posts, from tonight's real data. + * + * READ-ONLY on every source. This writes nothing to any serving, model or + * ledger table, so it has zero effect on the repaired-champion accrual clock. + * + * SUPABASE_URL=... node scripts/generate-content.js + * -> .content-out//