# The content engine — posts that structurally cannot lie ## Phase 0 — architecture `src/services/content/contentEngine.js`. Three mechanisms make Truth Law structural rather than careful: 1. **Copy is token-substituted.** Every factual claim is a `{token}` resolved against pulled facts. An unbacked token **refuses to render** — there is no code path producing a plausible default. 2. **The fact contract is asserted first.** A template declares required fields; they are checked *before any string is built*. 3. **Card and copy share one fact object.** They cannot diverge. **No live model writes factual claims.** The voice is in the template, the facts are pulled. A voice-polish port is reserved and deliberately unwired — an LLM that can rewrite a sentence can rewrite a number. **Read-only on every source.** Zero writes to serving, model or ledger tables, so zero effect on the accrual clock. ### The Truth-Law proof — 18 tests | the guard | what it prevents | |---|---| | unbacked token refuses | `{edge}` rendering as `undefined` or an empty hole | | card tokens gated too | a caption that's honest beside a card that isn't | | `render()` throws directly | a caller bypassing the gate | | `null` never renders as `"null"` | absence dressed as data | | **`0` IS present** | *"0 cleared B+"* is our most honest post — deleting it would be `Number(null)===0` in reverse | | `NaN`/`Infinity` absent | arithmetic failures are not facts | | contract gap names the field | a silent half-post | | pull failure skips | a post built on a dead source | ## Phase 1 — three templates, real output **HOT HITTERS** (from the repaired full-season log, not a ten-game slice): > Jahmai Jones is hitting 60% over his last 10. His season number is 26%. > That gap is the whole point. Everybody else is guessing at it. **THE HONESTY FLEX** — the differentiator, and every number is ours: > WE GRADED 2140 PROPS TONIGHT. 70 CLEARED B+. > That's 3%. The other 42% we can't separate from the baseline, and we say so on the card instead of calling them leans. > We do not issue A+, A, A-. No band of this model has ever hit at a rate that would justify one. > Everybody else's card is all A's. Ask them what their A actually hits. **STREAK LIST** — verified from settled outcomes only: > Nathan Church has a 7-game hit streak. Live, verified off settled results only. > Every game in these ran to a final. We don't count a pending night to make a number look better. ### The bug the engine caught in itself The first run emitted *"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 snapshot window, so **every** player had fewer than 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: an `absent()` variant may now **decline to speak**. The template separates *no candidates at all* (SKIP with a reason) from *candidates judged, none hot* (honest absence). Both cases are locked by test. Source corrected to `mlbStatsAdapter.fullLog` — the same log the repaired champion reads. ## Phase 2 — the card `cardRenderer.js`, SVG rather than canvas: it is text, so it diffs in review and its numbers are **greppable** — which matters when the entire claim is that the numbers are real. A card whose contents can't be inspected without opening an image is a poor fit for a Truth-Law product. Brand: VYND white + R green `#00D4A0`, slashed-Y, scanline field, mono throughout. The card never formats its own facts — every string arrives already rendered and gate-checked, so caption and card cannot disagree. A test asserts the pulled number appears in the emitted SVG. ## Phase 3 — posting-ready, and extending it ``` SUPABASE_URL=... node scripts/generate-content.js -> .content-out/2026-08-07/hot_hitters.txt + .svg -> .content-out/2026-08-07/honesty_flex.txt + .svg -> .content-out/2026-08-07/streak_list.txt + .svg ``` Kev posts; the engine generates. ### Adding template N+1 — registry entry only, no engine change ```js registerTemplate({ id, sport, requires: ['dotted.paths'], pull: async (deps) => facts, // the ONLY place data enters copy: () => 'text with {tokens}', card: () => ({ title, subtitle, lines }), absent: (gaps, facts) => ({ copy, card }) // or { skip: 'reason' } }); ``` **Queued (stubs, not built):** hot takes · daily honest reads · *"grades we DIDN'T give"* · cross-sport streak variants (the streak template is already sport-agnostic — it takes settled outcomes and a noun, so NFL TD streaks or NBA made-three streaks need only that sport's settled data). ## Isolation Read-only throughout. `p_win`, the model and the serving path are untouched; the eligible-date clock is unaffected. **0 eligible dates today, unchanged.**