74aa75945e
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
112 lines
4.9 KiB
Markdown
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.**
|