Files
vyndr/specs/CONTENT-ENGINE.md
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

112 lines
4.9 KiB
Markdown

# 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.**