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/<date>/. 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) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1sivYNqY2TS5ftykmHBU9
4.9 KiB
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:
- 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. - The fact contract is asserted first. A template declares required fields; they are checked before any string is built.
- 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
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.