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
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* THE TRUTH-LAW PROOF.
|
||||
*
|
||||
* The claim this engine makes is that a post cannot contain a number that was
|
||||
* not pulled. These tests are that claim, made falsifiable. If any of them can
|
||||
* be made to pass while a fabricated string escapes, the moat is decorative.
|
||||
*/
|
||||
|
||||
const engine = require('../../src/services/content/contentEngine');
|
||||
const { toSvg } = require('../../src/services/content/cardRenderer');
|
||||
|
||||
const base = {
|
||||
id: 'test_tpl', sport: 'mlb', requires: ['n'],
|
||||
pull: async () => ({ n: 3, name: 'Real Player' }),
|
||||
copy: () => 'we graded {n} props',
|
||||
card: () => ({ title: 'T', lines: [{ text: '{n} props', style: 'stat' }] }),
|
||||
};
|
||||
const reg = (over = {}) => engine.registerTemplate({ ...base, ...over, id: over.id || `t_${Math.random()}` });
|
||||
|
||||
describe('a template CANNOT render an unbacked claim', () => {
|
||||
it('refuses a token with no pulled fact', async () => {
|
||||
// The failure this prevents: a template author writes {edge} and the engine
|
||||
// helpfully renders "undefined" or, worse, an empty string that reads fine.
|
||||
const t = reg({ copy: () => 'edge is {edge_that_was_never_pulled}%' });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.skipped).toBe(true);
|
||||
expect(out.reason).toMatch(/TRUTH LAW: unbacked token/);
|
||||
});
|
||||
|
||||
it('refuses an unbacked token on the CARD too, not just the copy', async () => {
|
||||
const t = reg({ card: () => ({ title: 'T', lines: [{ text: '{ghost_stat}', style: 'stat' }] }) });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.reason).toMatch(/unbacked token/);
|
||||
});
|
||||
|
||||
it('render() throws directly — the gate is not bypassable by a caller', () => {
|
||||
expect(() => engine.render('{nope}', { yes: 1 })).toThrow(/TRUTH LAW/);
|
||||
});
|
||||
|
||||
it('a null fact is ABSENT, not rendered as "null"', async () => {
|
||||
const t = reg({ pull: async () => ({ n: null }), copy: () => '{n} props' });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(false);
|
||||
expect(String(out.copy || '')).not.toMatch(/null/);
|
||||
});
|
||||
|
||||
it('an empty string is absent — a hole in a sentence is a lie by omission', () => {
|
||||
expect(engine.isPresent('')).toBe(false);
|
||||
expect(engine.isPresent(' ')).toBe(false);
|
||||
});
|
||||
|
||||
it('ZERO is PRESENT — "0 cleared B+" is a real and important claim', () => {
|
||||
// Treating 0 as missing is the Number(null) === 0 breach wearing its
|
||||
// opposite coat, and it would silently delete our most honest post.
|
||||
expect(engine.isPresent(0)).toBe(true);
|
||||
expect(engine.render('{n} cleared', { n: 0 })).toBe('0 cleared');
|
||||
});
|
||||
|
||||
it('NaN and Infinity are absent — they are arithmetic failures, not facts', () => {
|
||||
expect(engine.isPresent(NaN)).toBe(false);
|
||||
expect(engine.isPresent(Infinity)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the fact contract is checked BEFORE any string is built', () => {
|
||||
it('a contract gap skips with the missing field named', async () => {
|
||||
const t = reg({ requires: ['n', 'missing_field'] });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.reason).toMatch(/fact-contract gap: missing_field/);
|
||||
});
|
||||
|
||||
it('a gap can emit an HONEST ABSENCE instead of nothing', async () => {
|
||||
const t = reg({
|
||||
requires: ['n', 'absent_thing'],
|
||||
absent: () => ({ copy: 'nothing tonight, and we say so', card: { title: 'NONE' } }),
|
||||
});
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(true);
|
||||
expect(out.honest_absence).toBe(true);
|
||||
expect(out.copy).toMatch(/nothing tonight/);
|
||||
});
|
||||
|
||||
it('a failing pull skips rather than rendering a half-post', async () => {
|
||||
const t = reg({ pull: async () => { throw new Error('source down'); } });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.reason).toMatch(/pull failed: source down/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('copy and card cannot disagree', () => {
|
||||
it('both render from ONE fact object', async () => {
|
||||
const t = reg({
|
||||
pull: async () => ({ n: 7 }),
|
||||
copy: () => 'we graded {n}',
|
||||
card: () => ({ title: 'T', lines: [{ text: '{n} graded', style: 'stat' }] }),
|
||||
});
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(out.copy).toMatch(/7/);
|
||||
expect(out.card.lines[0].text).toMatch(/7/);
|
||||
});
|
||||
|
||||
it('the card SVG contains the same pulled number', async () => {
|
||||
const t = reg({ pull: async () => ({ n: 42 }), copy: () => '{n}', card: () => ({ title: 'T', lines: [{ text: '{n} graded', style: 'stat' }] }) });
|
||||
const out = await engine.generate(t.id, {});
|
||||
expect(toSvg(out.card)).toMatch(/42 graded/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('the real templates obey the contract', () => {
|
||||
const hot = require('../../src/services/content/templates/hotHitters');
|
||||
const flex = require('../../src/services/content/templates/honestyFlex');
|
||||
const streak = require('../../src/services/content/templates/streakList');
|
||||
|
||||
it.each([[hot], [flex], [streak]])('every token in %s is declarable', (t) => {
|
||||
const tokens = new Set([
|
||||
...engine.tokensIn(t.copy({})),
|
||||
...engine.tokensIn(JSON.stringify(t.card({}))),
|
||||
]);
|
||||
expect(tokens.size).toBeGreaterThan(0);
|
||||
// Each template must ship an absent-variant, or a thin night silently
|
||||
// produces nothing and the flywheel stops without anyone noticing.
|
||||
expect(typeof t.absent).toBe('function');
|
||||
});
|
||||
|
||||
it('hot hitters refuses a hitter with too little history', async () => {
|
||||
engine.registerTemplate(hot);
|
||||
const out = await engine.generate('hot_hitters', {
|
||||
date: '2026-08-07',
|
||||
hitterForm: async () => ([{ name: 'Rookie', season_games: 4, recent_rate: 0.9, season_rate: 0.2 }]),
|
||||
});
|
||||
// 4 games is not a season, so nobody QUALIFIES -- which is a source problem,
|
||||
// not a quiet night. It must SKIP rather than publish honest-absence copy.
|
||||
expect(out.ok).toBe(false);
|
||||
expect(out.reason).toMatch(/source problem, not a quiet night/);
|
||||
});
|
||||
|
||||
it('hot hitters DOES emit honest absence when a real pool has nobody hot', async () => {
|
||||
// The distinction that matters: candidates existed, were judged, none hot.
|
||||
engine.registerTemplate(hot);
|
||||
const out = await engine.generate('hot_hitters', {
|
||||
date: '2026-08-07',
|
||||
hitterForm: async () => Array.from({ length: 30 }, (_, i) => ({
|
||||
name: `P${i}`, season_games: 90, recent_rate: 0.30, season_rate: 0.40,
|
||||
})),
|
||||
});
|
||||
expect(out.honest_absence).toBe(true);
|
||||
expect(out.copy).toMatch(/No hitter is meaningfully hot/);
|
||||
});
|
||||
|
||||
it('streaks refuse a run not verified from settled results', async () => {
|
||||
engine.registerTemplate(streak);
|
||||
const out = await engine.generate('streak_list', {
|
||||
date: '2026-08-07',
|
||||
settledStreaks: async () => ([{ name: 'X', streak: 9, verified_from_settled: false }]),
|
||||
});
|
||||
expect(out.honest_absence).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user