Files
vyndr/specs/CONTENT-ENGINE.md
T
builtbykev 74aa75945e Content engine: posts that structurally cannot lie
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
2026-08-07 16:34:57 -04:00

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:

  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

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.