feat(brief): WorldMonitor Brief magazine renderer + envelope contract (Phase 1) (#3150)

* feat(brief): add WorldMonitor Brief magazine renderer + envelope contract

Phase 1 of the WorldMonitor Brief plan (docs/plans/2026-04-17-003-feat-
worldmonitor-brief-magazine-plan.md). Establishes the integration
boundary between the future per-user composer and every consumer
surface (hosted edge route, dashboard panel, email teaser, carousel,
Tauri reader).

- shared/brief-envelope.{d.ts,js}: BriefEnvelope + BRIEF_ENVELOPE_VERSION
- shared/render-brief-magazine.{d.ts,js}: pure (envelope) -> HTML
- tests/brief-magazine-render.test.mjs: 28 shape + chrome + leak tests

Page sequence is derived from data (no hardcoded counts). Threads
split into 03a/03b when more than six; Signals page is omitted when
signals is empty; story palette alternates light/dark by index
parity. Forbidden-field guard asserts importanceScore / primaryLink /
pubDate / model + provider names never appear in rendered HTML.

No runtime impact: purely additive, no consumers yet.

* fix(brief): address code review findings on PR #3150

Addresses all seven review items from todos/205..211.

P1 (merge blockers):

- todo 205: forbidden-field test no longer matches free-text content
  like 'openai' / 'claude' / 'gemini' (false-positives on legitimate
  stories). Narrowed to JSON-key structural tokens
  ('"importanceScore":', '"_seed":', etc.) and added a sentinel-
  poisoning test that injects values into non-data envelope fields and
  asserts they never appear in output.

- todo 206: drop 'moderate' from BriefThreatLevel union (synonym of
  'medium'). Four-value ladder: critical | high | medium | low. Added
  THREAT_LABELS map + HIGHLIGHTED_LEVELS set so display label and
  highlight rule live together instead of inline char-case + hardcoded
  comparison.

- todo 207: replace the minimal validator with assertBriefEnvelope()
  that walks every required field (user.name/tz, date YYYY-MM-DD shape,
  digest.greeting/lead/numbers.{clusters,multiSource,surfaced}, threads
  array + per-element shape, signals array of strings, per-story
  required fields + threatLevel enum). Throws with field-path message
  on first miss. Adds nine negative tests covering common omissions.

- todo 208: envelope carries version guard. assertBriefEnvelope throws
  when envelope.version !== BRIEF_ENVELOPE_VERSION with a message
  naming both observed and expected versions.

P2 (should-fix, now included):

- todo 209: drop the _seed wrapper and make BriefEnvelope a flat
  { version, issuedAt, data } shape. A per-user brief is not a global
  seed; reusing _seed invited mis-application of seed invariants
  (SEED_META pairing, TTL rotation). Locked down before Phase 3
  composer bakes the shape in.

- todo 210: move renderer from shared/render-brief-magazine.{js,d.ts}
  to server/_shared/brief-render.{js,d.ts}. The 740-line template
  doesn't belong on the shared/ mirror hot path (Vercel+Railway). Keep
  only the envelope contract in shared/. Import path updated in tests.

- todo 211: logo SVG now defined once per document as a <symbol> and
  referenced via <use> at each placement (~8 refs/doc). Drops ~7KB
  from rendered output (~26% total size reduction on small inputs).

Tests pass (26/26), typecheck clean, lint clean, mirror check (169/169)
unaffected.

* fix(brief): enforce closed-key contract + surfaced-count invariant

Addresses two additional P1 review findings on PR #3150.

1. Validator rejects extra keys at every level (envelope root, data,
   user, digest, numbers, each thread, each story). Previously the
   forbidden-field rule was documented in the .d.ts and proven only at
   render time via a sentinel test — a producer could still persist
   importanceScore, primaryLink, pubDate, briefModel, _seed, fetchedAt,
   etc. into the Redis-resident envelope and every downstream consumer
   (edge route, dashboard panel preview, email teaser, carousel) would
   accept them. The validator is the only place the invariant can live.

2. Validator now asserts digest.numbers.surfaced === stories.length.
   The renderer uses both values — surfaced on the at-a-glance page,
   stories.length for the cover blurb and page count — so allowing
   them to disagree produced a self-contradictory brief that the old
   validator silently passed.

Tests: 30/30 (was 26). New negative tests cover the strict-keys
rejection at root / data / numbers / story[i] levels and the surfaced
mismatch. The earlier sentinel-poisoning test is superseded — the
strict-keys check catches the same class of bug earlier and harder.
This commit is contained in:
Elie Habib
2026-04-18 00:01:57 +04:00
committed by GitHub
parent 7693a4fa4f
commit 66ca645571
5 changed files with 1502 additions and 0 deletions

3
server/_shared/brief-render.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
import type { BriefEnvelope } from '../../shared/brief-envelope.js';
export function renderBriefMagazine(envelope: BriefEnvelope): string;

View File

@@ -0,0 +1,951 @@
// Deterministic renderer for the WorldMonitor Brief magazine.
//
// Pure function: (BriefEnvelope) -> HTML string. No I/O, no LLM calls,
// no network, no time-dependent output. The composer writes the
// envelope once; any consumer (edge route, dashboard panel preview,
// Tauri window) re-renders the same HTML at read time.
//
// The page sequence is derived from the data, not hardcoded:
// 1. Dark cover
// 2. Digest · 01 Greeting (always)
// 3. Digest · 02 At A Glance (always)
// 4. Digest · 03 On The Desk (one page if threads.length <= 6;
// else split into 03a + 03b)
// 5. Digest · 04 Signals (omitted when signals.length === 0)
// 6. Stories (one page per story, alternating
// light/dark by index parity)
// 7. Dark back cover
//
// Source references:
// - Visual prototype: .claude/worktrees/zany-chasing-boole/digest-magazine.html
// - Brainstorm: docs/brainstorms/2026-04-17-worldmonitor-brief-magazine-requirements.md
// - Plan: docs/plans/2026-04-17-003-feat-worldmonitor-brief-magazine-plan.md
import { BRIEF_ENVELOPE_VERSION } from '../../shared/brief-envelope.js';
/**
* @typedef {import('../../shared/brief-envelope.js').BriefEnvelope} BriefEnvelope
* @typedef {import('../../shared/brief-envelope.js').BriefData} BriefData
* @typedef {import('../../shared/brief-envelope.js').BriefStory} BriefStory
* @typedef {import('../../shared/brief-envelope.js').BriefThread} BriefThread
* @typedef {import('../../shared/brief-envelope.js').BriefThreatLevel} BriefThreatLevel
*/
// ── Constants ────────────────────────────────────────────────────────────────
const FONTS_HREF =
'https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,700;0,900;1,400&family=Source+Serif+4:ital,wght@0,400;0,600;1,400&family=IBM+Plex+Mono:wght@400;500;600&display=swap';
const MAX_THREADS_PER_PAGE = 6;
const DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/;
/** @type {Record<BriefThreatLevel, string>} */
const THREAT_LABELS = {
critical: 'Critical',
high: 'High',
medium: 'Medium',
low: 'Low',
};
/** @type {Set<BriefThreatLevel>} */
const HIGHLIGHTED_LEVELS = new Set(['critical', 'high']);
const VALID_THREAT_LEVELS = new Set(
/** @type {BriefThreatLevel[]} */ (['critical', 'high', 'medium', 'low']),
);
// ── HTML escaping ────────────────────────────────────────────────────────────
const HTML_ESCAPE_MAP = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
const HTML_ESCAPE_RE = /[&<>"']/;
const HTML_ESCAPE_RE_G = /[&<>"']/g;
/**
* Text-context HTML escape. Do not use for raw attribute-value
* interpolation without extending the map.
* @param {string} str
*/
function escapeHtml(str) {
const s = String(str);
if (!HTML_ESCAPE_RE.test(s)) return s;
return s.replace(HTML_ESCAPE_RE_G, (ch) => HTML_ESCAPE_MAP[ch]);
}
/** @param {number} n */
function pad2(n) {
return String(n).padStart(2, '0');
}
// ── Envelope validation ──────────────────────────────────────────────────────
/** @param {unknown} v */
function isObject(v) {
return typeof v === 'object' && v !== null;
}
/** @param {unknown} v */
function isNonEmptyString(v) {
return typeof v === 'string' && v.length > 0;
}
/** @param {unknown} v */
function isFiniteNumber(v) {
return typeof v === 'number' && Number.isFinite(v);
}
// Closed key sets for each object in the contract. The validator
// rejects extra keys at every level — a producer cannot smuggle
// importanceScore, primaryLink, pubDate, briefModel, fetchedAt or any
// other forbidden upstream field into a persisted envelope. The
// renderer already refuses to interpolate unknown fields (and that is
// covered by the sentinel-poisoning test), but unknown fields resident
// in Redis still pollute every future consumer (edge route, dashboard
// panel preview, carousel, email teaser). Locking the contract at
// write time is the only place this invariant can live.
const ALLOWED_ENVELOPE_KEYS = new Set(['version', 'issuedAt', 'data']);
const ALLOWED_DATA_KEYS = new Set(['user', 'issue', 'date', 'dateLong', 'digest', 'stories']);
const ALLOWED_USER_KEYS = new Set(['name', 'tz']);
const ALLOWED_DIGEST_KEYS = new Set(['greeting', 'lead', 'numbers', 'threads', 'signals']);
const ALLOWED_NUMBERS_KEYS = new Set(['clusters', 'multiSource', 'surfaced']);
const ALLOWED_THREAD_KEYS = new Set(['tag', 'teaser']);
const ALLOWED_STORY_KEYS = new Set([
'category',
'country',
'threatLevel',
'headline',
'description',
'source',
'whyMatters',
]);
/**
* @param {Record<string, unknown>} obj
* @param {Set<string>} allowed
* @param {string} path
*/
function assertNoExtraKeys(obj, allowed, path) {
for (const key of Object.keys(obj)) {
if (!allowed.has(key)) {
throw new Error(
`${path} has unexpected key ${JSON.stringify(key)}; allowed keys: ${[...allowed].join(', ')}`,
);
}
}
}
/**
* Throws a descriptive error on the first missing, mis-typed, or
* unexpected field. Runs before any HTML interpolation so the renderer
* can assume the typed shape after this returns. The renderer is a
* shared module with multiple independent producers (Railway composer,
* tests, future dev-only fixtures) — a strict runtime contract matters
* more than the declaration-file types alone.
*
* Also enforces the cross-field invariant that
* `digest.numbers.surfaced === stories.length`. The renderer uses both
* values (surfaced prints on the "at a glance" page; stories.length
* drives cover blurb and page count) — allowing them to disagree would
* produce a self-contradictory brief.
*
* @param {unknown} envelope
* @returns {asserts envelope is BriefEnvelope}
*/
function assertBriefEnvelope(envelope) {
if (!isObject(envelope)) {
throw new Error('renderBriefMagazine: envelope must be an object');
}
const env = /** @type {Record<string, unknown>} */ (envelope);
assertNoExtraKeys(env, ALLOWED_ENVELOPE_KEYS, 'envelope');
if (env.version !== BRIEF_ENVELOPE_VERSION) {
throw new Error(
`renderBriefMagazine: envelope.version=${JSON.stringify(env.version)} does not match renderer version=${BRIEF_ENVELOPE_VERSION}. Deploy a matching renderer before producing envelopes at this version.`,
);
}
if (!isFiniteNumber(env.issuedAt)) {
throw new Error('renderBriefMagazine: envelope.issuedAt must be a finite number');
}
if (!isObject(env.data)) {
throw new Error('renderBriefMagazine: envelope.data is required');
}
const data = /** @type {Record<string, unknown>} */ (env.data);
assertNoExtraKeys(data, ALLOWED_DATA_KEYS, 'envelope.data');
if (!isObject(data.user)) throw new Error('envelope.data.user is required');
const user = /** @type {Record<string, unknown>} */ (data.user);
assertNoExtraKeys(user, ALLOWED_USER_KEYS, 'envelope.data.user');
if (!isNonEmptyString(user.name)) throw new Error('envelope.data.user.name must be a non-empty string');
if (!isNonEmptyString(user.tz)) throw new Error('envelope.data.user.tz must be a non-empty string');
if (!isNonEmptyString(data.issue)) throw new Error('envelope.data.issue must be a non-empty string');
if (!isNonEmptyString(data.date)) throw new Error('envelope.data.date must be a non-empty string');
if (!DATE_REGEX.test(/** @type {string} */ (data.date))) {
throw new Error('envelope.data.date must match YYYY-MM-DD');
}
if (!isNonEmptyString(data.dateLong)) throw new Error('envelope.data.dateLong must be a non-empty string');
if (!isObject(data.digest)) throw new Error('envelope.data.digest is required');
const digest = /** @type {Record<string, unknown>} */ (data.digest);
assertNoExtraKeys(digest, ALLOWED_DIGEST_KEYS, 'envelope.data.digest');
if (!isNonEmptyString(digest.greeting)) throw new Error('envelope.data.digest.greeting must be a non-empty string');
if (!isNonEmptyString(digest.lead)) throw new Error('envelope.data.digest.lead must be a non-empty string');
if (!isObject(digest.numbers)) throw new Error('envelope.data.digest.numbers is required');
const numbers = /** @type {Record<string, unknown>} */ (digest.numbers);
assertNoExtraKeys(numbers, ALLOWED_NUMBERS_KEYS, 'envelope.data.digest.numbers');
for (const key of /** @type {const} */ (['clusters', 'multiSource', 'surfaced'])) {
if (!isFiniteNumber(numbers[key])) {
throw new Error(`envelope.data.digest.numbers.${key} must be a finite number`);
}
}
if (!Array.isArray(digest.threads)) {
throw new Error('envelope.data.digest.threads must be an array');
}
digest.threads.forEach((t, i) => {
if (!isObject(t)) throw new Error(`envelope.data.digest.threads[${i}] must be an object`);
const th = /** @type {Record<string, unknown>} */ (t);
assertNoExtraKeys(th, ALLOWED_THREAD_KEYS, `envelope.data.digest.threads[${i}]`);
if (!isNonEmptyString(th.tag)) throw new Error(`envelope.data.digest.threads[${i}].tag must be a non-empty string`);
if (!isNonEmptyString(th.teaser)) throw new Error(`envelope.data.digest.threads[${i}].teaser must be a non-empty string`);
});
if (!Array.isArray(digest.signals)) {
throw new Error('envelope.data.digest.signals must be an array');
}
digest.signals.forEach((s, i) => {
if (!isNonEmptyString(s)) throw new Error(`envelope.data.digest.signals[${i}] must be a non-empty string`);
});
if (!Array.isArray(data.stories) || data.stories.length === 0) {
throw new Error('envelope.data.stories must be a non-empty array');
}
data.stories.forEach((s, i) => {
if (!isObject(s)) throw new Error(`envelope.data.stories[${i}] must be an object`);
const st = /** @type {Record<string, unknown>} */ (s);
assertNoExtraKeys(st, ALLOWED_STORY_KEYS, `envelope.data.stories[${i}]`);
for (const field of /** @type {const} */ (['category', 'country', 'headline', 'description', 'source', 'whyMatters'])) {
if (!isNonEmptyString(st[field])) {
throw new Error(`envelope.data.stories[${i}].${field} must be a non-empty string`);
}
}
if (typeof st.threatLevel !== 'string' || !VALID_THREAT_LEVELS.has(/** @type {BriefThreatLevel} */ (st.threatLevel))) {
throw new Error(
`envelope.data.stories[${i}].threatLevel must be one of critical|high|medium|low (got ${JSON.stringify(st.threatLevel)})`,
);
}
});
// Cross-field invariant: surfaced count must match the actual number
// of stories surfaced to this reader. Enforced here so cover copy
// ("N threads") and the at-a-glance stat can never disagree.
if (numbers.surfaced !== data.stories.length) {
throw new Error(
`envelope.data.digest.numbers.surfaced=${numbers.surfaced} must equal envelope.data.stories.length=${data.stories.length}`,
);
}
}
// ── Logo symbol + references ─────────────────────────────────────────────────
/**
* The full logo SVG is emitted ONCE per document inside an invisible
* <svg><defs><symbol id="wm-logo-core"> block. Every placement then
* references the symbol via `<use>` at the desired size. Saves ~7 KB on
* a 12-story brief vs. repeating the full SVG per placement.
*
* Stroke width is baked into the symbol (medium weight). Visual variance
* across placements (cover 48px vs story 28px) reads identically at
* display size; sub-pixel stroke differences are not perceptible.
*/
const LOGO_SYMBOL = (
'<svg aria-hidden="true" style="display:none;position:absolute;width:0;height:0" focusable="false">' +
'<defs>' +
'<symbol id="wm-logo-core" viewBox="0 0 64 64">' +
'<circle cx="32" cy="32" r="28"/>' +
'<ellipse cx="32" cy="32" rx="5" ry="28"/>' +
'<ellipse cx="32" cy="32" rx="14" ry="28"/>' +
'<ellipse cx="32" cy="32" rx="22" ry="28"/>' +
'<ellipse cx="32" cy="32" rx="28" ry="5"/>' +
'<ellipse cx="32" cy="32" rx="28" ry="14"/>' +
'<path class="wm-ekg" d="M 6 32 L 20 32 L 24 24 L 30 40 L 36 22 L 42 38 L 46 32 L 56 32"/>' +
'<circle class="wm-ekg-dot" cx="57" cy="32" r="1.8"/>' +
'</symbol>' +
'</defs>' +
'</svg>'
);
/**
* @param {{ size: number; color?: string }} opts
*/
function logoRef({ size, color }) {
// color is sourced ONLY from a closed enum of theme strings at the
// call sites in this file. Never interpolate envelope-derived content
// into a style= attribute via this helper.
const styleAttr = color ? ` style="color: ${color};"` : '';
return (
`<svg class="wm-logo" width="${size}" height="${size}" viewBox="0 0 64 64" ` +
`aria-label="WorldMonitor"${styleAttr}>` +
'<use href="#wm-logo-core"/>' +
'</svg>'
);
}
// ── Running head (shared across digest pages) ────────────────────────────────
/** @param {string} dateShort @param {string} label */
function digestRunningHead(dateShort, label) {
return (
'<div class="running-head">' +
'<span class="mono left">' +
logoRef({ size: 22 }) +
` · WorldMonitor Brief · ${escapeHtml(dateShort)} ·` +
'</span>' +
`<span class="mono">${escapeHtml(label)}</span>` +
'</div>'
);
}
// ── Page renderers ───────────────────────────────────────────────────────────
/**
* @param {{ dateLong: string; issue: string; storyCount: number; pageIndex: number; totalPages: number }} opts
*/
function renderCover({ dateLong, issue, storyCount, pageIndex, totalPages }) {
const blurb =
storyCount === 1
? 'One thread that shaped the world today.'
: `${storyCount} threads that shaped the world today.`;
return (
'<section class="page cover">' +
'<div class="meta-top">' +
'<span class="brand">' +
logoRef({ size: 48 }) +
'<span class="mono">WorldMonitor</span>' +
'</span>' +
`<span class="mono">Issue № ${escapeHtml(issue)}</span>` +
'</div>' +
'<div class="hero">' +
`<div class="kicker">${escapeHtml(dateLong)}</div>` +
'<h1>WorldMonitor<br/>Brief.</h1>' +
`<p class="blurb">${escapeHtml(blurb)}</p>` +
'</div>' +
'<div class="meta-bottom">' +
'<span class="mono">Good evening</span>' +
'<span class="mono">Swipe / ↔ to begin</span>' +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/**
* @param {{ greeting: string; lead: string; dateShort: string; pageIndex: number; totalPages: number }} opts
*/
function renderDigestGreeting({ greeting, lead, dateShort, pageIndex, totalPages }) {
return (
'<section class="page digest">' +
digestRunningHead(dateShort, 'Digest / 01') +
'<div class="body">' +
'<div class="label mono">At The Top Of The Hour</div>' +
`<h2>${escapeHtml(greeting)}</h2>` +
`<blockquote>${escapeHtml(lead)}</blockquote>` +
'<hr class="rule" />' +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/**
* @param {{ numbers: import('../../shared/brief-envelope.js').BriefNumbers; date: string; dateShort: string; pageIndex: number; totalPages: number }} opts
*/
function renderDigestNumbers({ numbers, date, dateShort, pageIndex, totalPages }) {
const rows = [
{ n: numbers.clusters, label: 'story clusters ingested in the last 24 hours' },
{ n: numbers.multiSource, label: 'multi-source confirmed events' },
{ n: numbers.surfaced, label: 'threads surfaced in this brief' },
]
.map(
(row) =>
'<div class="stat-row">' +
`<div class="stat-num">${pad2(row.n)}</div>` +
`<div class="stat-label">${escapeHtml(row.label)}</div>` +
'</div>',
)
.join('');
return (
'<section class="page digest">' +
digestRunningHead(dateShort, 'Digest / 02 — At A Glance') +
'<div class="body">' +
'<div class="label mono">The Numbers Today</div>' +
`<div class="stats">${rows}</div>` +
`<div class="footer-caption mono">Signal Window · ${escapeHtml(date)}</div>` +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/**
* @param {{ threads: BriefThread[]; dateShort: string; label: string; heading: string; includeEndMarker: boolean; pageIndex: number; totalPages: number }} opts
*/
function renderDigestThreadsPage({
threads,
dateShort,
label,
heading,
includeEndMarker,
pageIndex,
totalPages,
}) {
const rows = threads
.map(
(t) =>
'<p class="thread">' +
`<span class="tag">${escapeHtml(t.tag)} —</span>` +
`${escapeHtml(t.teaser)}` +
'</p>',
)
.join('');
const endMarker = includeEndMarker
? '<div class="end-marker"><hr /><span class="mono">Stories follow →</span></div>'
: '';
return (
'<section class="page digest">' +
digestRunningHead(dateShort, label) +
'<div class="body">' +
'<div class="label mono">Today\u2019s Threads</div>' +
`<h2>${escapeHtml(heading)}</h2>` +
`<div class="threads">${rows}</div>` +
endMarker +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/**
* @param {{ signals: string[]; dateShort: string; pageIndex: number; totalPages: number }} opts
*/
function renderDigestSignals({ signals, dateShort, pageIndex, totalPages }) {
const paragraphs = signals
.map((s) => `<p class="signal">${escapeHtml(s)}</p>`)
.join('');
return (
'<section class="page digest">' +
digestRunningHead(dateShort, 'Digest / 04 — Signals') +
'<div class="body">' +
'<div class="label mono">Signals To Watch</div>' +
'<h2>What would change the story.</h2>' +
`<div class="signals">${paragraphs}</div>` +
'<div class="end-marker"><hr /><span class="mono">End of digest · Stories follow →</span></div>' +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/**
* @param {{ story: BriefStory; rank: number; palette: 'light' | 'dark'; pageIndex: number; totalPages: number }} opts
*/
function renderStoryPage({ story, rank, palette, pageIndex, totalPages }) {
const threatClass = HIGHLIGHTED_LEVELS.has(story.threatLevel) ? ' crit' : '';
const threatLabel = THREAT_LABELS[story.threatLevel];
return (
`<section class="page story ${palette}">` +
'<div class="left">' +
`<div class="rank-ghost">${pad2(rank)}</div>` +
'<div class="left-content">' +
'<div class="tag-row">' +
`<span class="tag">${escapeHtml(story.category)}</span>` +
`<span class="tag">${escapeHtml(story.country)}</span>` +
`<span class="tag${threatClass}">${escapeHtml(threatLabel)}</span>` +
'</div>' +
`<h3>${escapeHtml(story.headline)}</h3>` +
`<p class="desc">${escapeHtml(story.description)}</p>` +
`<div class="source">Source · ${escapeHtml(story.source)}</div>` +
'</div>' +
'</div>' +
'<div class="right">' +
'<div class="callout">' +
'<div class="label">Why this is important</div>' +
`<p class="note">${escapeHtml(story.whyMatters)}</p>` +
'</div>' +
'</div>' +
'<div class="logo-chrome">' +
logoRef({ size: 28 }) +
'<span class="mono">WorldMonitor Brief</span>' +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
/** @param {{ tz: string; pageIndex: number; totalPages: number }} opts */
function renderBackCover({ tz, pageIndex, totalPages }) {
return (
'<section class="page cover back">' +
'<div class="hero">' +
'<div class="centered-logo">' +
logoRef({ size: 80, color: 'var(--bone)' }) +
'</div>' +
'<div class="kicker">Thank you for reading</div>' +
'<h1>End of<br/>Transmission.</h1>' +
'</div>' +
'<div class="meta-bottom">' +
'<span class="mono">worldmonitor.app</span>' +
`<span class="mono">Next brief · 08:00 ${escapeHtml(tz)}</span>` +
'</div>' +
`<div class="page-number mono">${pad2(pageIndex)} / ${pad2(totalPages)}</div>` +
'</section>'
);
}
// ── Shell (document + CSS + JS) ──────────────────────────────────────────────
const STYLE_BLOCK = `<style>
:root {
--ink: #0a0a0a;
--bone: #f2ede4;
--cream: #f1e9d8;
--cream-ink: #1a1612;
--sienna: #8b3a1f;
--paper: #fafafa;
--paper-ink: #0a0a0a;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100vw; height: 100vh; overflow: hidden;
background: #000;
font-family: 'Source Serif 4', Georgia, serif;
-webkit-font-smoothing: antialiased;
}
.deck {
width: 100vw; height: 100vh; display: flex;
transition: transform 620ms cubic-bezier(0.77, 0, 0.175, 1);
will-change: transform;
}
.page {
flex: 0 0 100vw; width: 100vw; height: 100vh;
padding: 6vh 6vw 10vh;
position: relative; overflow: hidden;
display: flex; flex-direction: column;
}
.mono {
font-family: 'IBM Plex Mono', monospace;
font-weight: 500; letter-spacing: 0.18em;
text-transform: uppercase; font-size: max(11px, 0.85vw);
}
.wm-logo { display: block; fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
.wm-logo .wm-ekg { stroke-width: 2.4; }
.wm-logo .wm-ekg-dot { fill: currentColor; stroke: none; }
.logo-chrome {
position: absolute; bottom: 5vh; left: 6vw;
display: flex; align-items: center; gap: 0.8vw; opacity: 0.7;
}
.cover { background: var(--ink); color: var(--bone); }
.cover .meta-top, .cover .meta-bottom {
display: flex; justify-content: space-between; align-items: center; opacity: 0.75;
}
.cover .meta-top .brand { display: flex; align-items: center; gap: 1vw; }
.cover .hero {
flex: 1; display: flex; flex-direction: column; justify-content: center;
}
.cover .hero h1 {
font-family: 'Playfair Display', serif; font-weight: 900;
font-size: 10vw; line-height: 0.92; letter-spacing: -0.03em;
margin-bottom: 6vh;
}
.cover .hero .kicker {
font-family: 'IBM Plex Mono', monospace;
font-size: max(13px, 1.1vw); letter-spacing: 0.3em;
text-transform: uppercase; opacity: 0.75; margin-bottom: 4vh;
}
.cover .hero .blurb {
font-family: 'Source Serif 4', serif; font-style: italic;
font-size: max(18px, 1.7vw); max-width: 48ch; opacity: 0.82; line-height: 1.4;
}
.cover.back { align-items: center; justify-content: center; text-align: center; }
.cover.back .hero { align-items: center; flex: 0; }
.cover.back .centered-logo { margin-bottom: 5vh; opacity: 0.9; }
.cover.back .hero h1 { font-size: 8vw; }
.cover.back .meta-bottom {
width: 100%; position: absolute; bottom: 6vh; left: 0; padding: 0 6vw;
}
.digest { background: var(--cream); color: var(--cream-ink); }
.digest .running-head {
display: flex; justify-content: space-between; align-items: center;
padding-bottom: 2vh; border-bottom: 1px solid rgba(26, 22, 18, 0.18);
}
.digest .running-head .left {
display: flex; align-items: center; gap: 0.8vw;
color: var(--sienna); font-weight: 600;
}
.digest .body {
flex: 1; display: flex; flex-direction: column;
justify-content: center; padding-top: 4vh;
}
.digest .label { color: var(--sienna); margin-bottom: 5vh; }
.digest h2 {
font-family: 'Playfair Display', serif; font-weight: 900;
font-size: 7vw; line-height: 0.98; letter-spacing: -0.02em;
margin-bottom: 6vh; max-width: 18ch;
}
.digest blockquote {
font-family: 'Source Serif 4', serif; font-style: italic;
font-size: 2vw; line-height: 1.38; max-width: 32ch;
margin-bottom: 5vh; padding-left: 2vw;
border-left: 3px solid var(--sienna);
}
.digest .rule {
border: none; height: 2px; background: var(--sienna);
width: 8vw; margin-top: 5vh;
}
.digest .stats { display: flex; flex-direction: column; gap: 3vh; }
.digest .stat-row {
display: grid; grid-template-columns: 22vw 1fr;
align-items: baseline; gap: 3vw;
padding-bottom: 3vh; border-bottom: 1px solid rgba(26, 22, 18, 0.14);
}
.digest .stat-row:last-child { border-bottom: none; }
.digest .stat-num {
font-family: 'Playfair Display', serif; font-weight: 900;
font-size: 11vw; line-height: 0.9; color: var(--cream-ink);
}
.digest .stat-label {
font-family: 'Source Serif 4', serif; font-style: italic;
font-size: max(18px, 1.7vw); line-height: 1.3;
color: var(--cream-ink); opacity: 0.85;
}
.digest .footer-caption { margin-top: 4vh; color: var(--sienna); opacity: 0.85; }
.digest .threads { display: flex; flex-direction: column; gap: 3.2vh; max-width: 62ch; }
.digest .thread {
font-family: 'Source Serif 4', serif;
font-size: max(17px, 1.55vw); line-height: 1.45;
color: var(--cream-ink);
}
.digest .thread .tag {
font-family: 'IBM Plex Mono', monospace; font-weight: 600;
letter-spacing: 0.2em; color: var(--sienna); margin-right: 0.6em;
}
.digest .signals { display: flex; flex-direction: column; gap: 3.5vh; max-width: 60ch; }
.digest .signal {
font-family: 'Source Serif 4', serif;
font-size: max(18px, 1.65vw); line-height: 1.45;
color: var(--cream-ink); padding-left: 2vw;
border-left: 2px solid var(--sienna);
}
.digest .end-marker {
margin-top: 5vh; display: flex; align-items: center; gap: 1.5vw;
}
.digest .end-marker hr {
flex: 0 0 10vw; border: none; height: 2px; background: var(--sienna);
}
.digest .end-marker .mono { color: var(--sienna); }
.story { display: grid; grid-template-columns: 55fr 45fr; gap: 4vw; }
.story.light { background: var(--paper); color: var(--paper-ink); }
.story.dark { background: var(--ink); color: var(--bone); }
.story .left {
display: flex; flex-direction: column; justify-content: center;
position: relative; padding-right: 2vw;
}
.story .rank-ghost {
font-family: 'Playfair Display', serif; font-weight: 900;
font-size: 38vw; line-height: 0.8;
position: absolute; top: 50%; left: -1vw;
transform: translateY(-50%); opacity: 0.07;
pointer-events: none; letter-spacing: -0.04em;
}
.story.dark .rank-ghost { opacity: 0.1; }
.story .left-content { position: relative; z-index: 2; }
.story .tag-row {
display: flex; gap: 1.2vw; margin-bottom: 4vh; flex-wrap: wrap;
}
.story .tag {
font-family: 'IBM Plex Mono', monospace;
font-size: max(11px, 0.85vw); font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase;
padding: 0.5em 1em; border: 1px solid currentColor; opacity: 0.82;
}
.story .tag.crit { background: currentColor; color: var(--paper); }
.story.dark .tag.crit { background: var(--bone); color: var(--ink); border-color: var(--bone); }
.story h3 {
font-family: 'Playfair Display', serif; font-weight: 900;
font-size: 5vw; line-height: 0.98; letter-spacing: -0.02em;
margin-bottom: 5vh; max-width: 18ch;
}
.story .desc {
font-family: 'Source Serif 4', serif;
font-size: max(17px, 1.55vw); line-height: 1.45;
max-width: 40ch; margin-bottom: 4vh; opacity: 0.88;
}
.story.dark .desc { opacity: 0.85; }
.story .source {
font-family: 'IBM Plex Mono', monospace;
font-size: max(11px, 0.9vw); letter-spacing: 0.2em;
text-transform: uppercase; opacity: 0.6;
}
.story .right { display: flex; flex-direction: column; justify-content: center; }
.story .callout {
background: rgba(0, 0, 0, 0.05);
border-left: 4px solid currentColor;
padding: 5vh 3vw 5vh 3vw;
}
.story.dark .callout {
background: rgba(242, 237, 228, 0.06);
border-left-color: var(--bone);
}
.story .callout .label {
font-family: 'IBM Plex Mono', monospace;
font-size: max(11px, 0.85vw); font-weight: 600;
letter-spacing: 0.22em; text-transform: uppercase;
margin-bottom: 3vh; opacity: 0.75;
}
.story .callout .note {
font-family: 'Source Serif 4', serif;
font-size: max(17px, 1.55vw); line-height: 1.5; opacity: 0.82;
}
.nav-dots {
position: fixed; bottom: 3.5vh; left: 50%;
transform: translateX(-50%);
display: flex; gap: 0.9vw; z-index: 20;
padding: 0.9vh 1.4vw;
background: rgba(20, 20, 20, 0.55);
backdrop-filter: blur(8px); border-radius: 999px;
}
.nav-dots button {
width: 9px; height: 9px; border-radius: 50%; border: none;
background: rgba(255, 255, 255, 0.3);
cursor: pointer; padding: 0;
transition: all 220ms ease;
}
.nav-dots button.digest-dot { background: rgba(139, 58, 31, 0.55); }
.nav-dots button.active {
background: rgba(255, 255, 255, 0.95);
width: 26px; border-radius: 5px;
}
.nav-dots button.active.digest-dot { background: var(--sienna); }
.hint {
position: fixed; bottom: 3.5vh; right: 3vw;
font-family: 'IBM Plex Mono', monospace;
font-size: 10px; letter-spacing: 0.2em;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.5);
z-index: 20; mix-blend-mode: difference;
}
.page-number {
position: absolute; top: 5vh; right: 4vw;
font-family: 'IBM Plex Mono', monospace;
font-size: max(11px, 0.85vw);
letter-spacing: 0.2em; opacity: 0.55;
}
</style>`;
const NAV_SCRIPT = `<script>
(function() {
var deck = document.getElementById('deck');
if (!deck) return;
var pages = deck.querySelectorAll('.page');
var dotsContainer = document.getElementById('navDots');
var total = pages.length;
var current = 0;
var wheelLock = false;
var touchStartX = 0;
// digest-indexes attribute is a server-built JSON number array.
var digestIndexes = new Set(JSON.parse(deck.dataset.digestIndexes || '[]'));
for (var i = 0; i < total; i++) {
var b = document.createElement('button');
b.setAttribute('aria-label', 'Go to page ' + (i + 1));
if (digestIndexes.has(i)) b.classList.add('digest-dot');
(function(idx) { b.addEventListener('click', function() { go(idx); }); })(i);
dotsContainer.appendChild(b);
}
var dots = dotsContainer.querySelectorAll('button');
function render() {
deck.style.transform = 'translateX(-' + (current * 100) + 'vw)';
for (var i = 0; i < dots.length; i++) {
if (i === current) dots[i].classList.add('active');
else dots[i].classList.remove('active');
}
}
function go(i) { current = Math.max(0, Math.min(total - 1, i)); render(); }
function next() { go(current + 1); }
function prev() { go(current - 1); }
window.addEventListener('keydown', function(e) {
if (e.key === 'ArrowRight' || e.key === 'PageDown' || e.key === ' ') { e.preventDefault(); next(); }
else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); prev(); }
else if (e.key === 'Home') { e.preventDefault(); go(0); }
else if (e.key === 'End') { e.preventDefault(); go(total - 1); }
});
window.addEventListener('wheel', function(e) {
if (wheelLock) return;
var delta = Math.abs(e.deltaY) > Math.abs(e.deltaX) ? e.deltaY : e.deltaX;
if (Math.abs(delta) < 12) return;
wheelLock = true;
if (delta > 0) next(); else prev();
setTimeout(function() { wheelLock = false; }, 620);
}, { passive: true });
window.addEventListener('touchstart', function(e) { touchStartX = e.touches[0].clientX; }, { passive: true });
window.addEventListener('touchend', function(e) {
var dx = e.changedTouches[0].clientX - touchStartX;
if (Math.abs(dx) < 50) return;
if (dx < 0) next(); else prev();
}, { passive: true });
render();
})();
</script>`;
// ── Main entry ───────────────────────────────────────────────────────────────
/**
* @param {BriefEnvelope} envelope
* @returns {string}
*/
export function renderBriefMagazine(envelope) {
assertBriefEnvelope(envelope);
const { user, issue, date, dateLong, digest, stories } = envelope.data;
const [, month, day] = date.split('-');
const dateShort = `${day}.${month}`;
const threads = digest.threads;
const hasSignals = digest.signals.length > 0;
const splitThreads = threads.length > MAX_THREADS_PER_PAGE;
// Total page count is fully data-derived, computed up front, so every
// page renderer knows its position without a two-pass build.
const totalPages =
1 // cover
+ 1 // digest 01 greeting
+ 1 // digest 02 numbers
+ (splitThreads ? 2 : 1) // digest 03 on the desk (split if needed)
+ (hasSignals ? 1 : 0) // digest 04 signals (conditional)
+ stories.length
+ 1; // back cover
/** @type {string[]} */
const pagesHtml = [];
/** @type {number[]} */
const digestIndexes = [];
let p = 0;
pagesHtml.push(
renderCover({
dateLong,
issue,
storyCount: stories.length,
pageIndex: ++p,
totalPages,
}),
);
digestIndexes.push(p);
pagesHtml.push(
renderDigestGreeting({
greeting: digest.greeting,
lead: digest.lead,
dateShort,
pageIndex: ++p,
totalPages,
}),
);
digestIndexes.push(p);
pagesHtml.push(
renderDigestNumbers({
numbers: digest.numbers,
date,
dateShort,
pageIndex: ++p,
totalPages,
}),
);
const threadsPages = splitThreads
? [threads.slice(0, Math.ceil(threads.length / 2)), threads.slice(Math.ceil(threads.length / 2))]
: [threads];
threadsPages.forEach((slice, i) => {
const label = threadsPages.length === 1
? 'Digest / 03 — On The Desk'
: `Digest / 03${i === 0 ? 'a' : 'b'} — On The Desk`;
const heading = i === 0 ? 'What the desk is watching.' : '\u2026 continued.';
digestIndexes.push(p);
pagesHtml.push(
renderDigestThreadsPage({
threads: slice,
dateShort,
label,
heading,
includeEndMarker: i === threadsPages.length - 1 && !hasSignals,
pageIndex: ++p,
totalPages,
}),
);
});
if (hasSignals) {
digestIndexes.push(p);
pagesHtml.push(
renderDigestSignals({
signals: digest.signals,
dateShort,
pageIndex: ++p,
totalPages,
}),
);
}
stories.forEach((story, i) => {
pagesHtml.push(
renderStoryPage({
story,
rank: i + 1,
palette: i % 2 === 0 ? 'light' : 'dark',
pageIndex: ++p,
totalPages,
}),
);
});
pagesHtml.push(
renderBackCover({
tz: user.tz,
pageIndex: ++p,
totalPages,
}),
);
const title = `WorldMonitor Brief · ${escapeHtml(dateLong)}`;
return (
'<!DOCTYPE html>' +
'<html lang="en">' +
'<head>' +
'<meta charset="UTF-8" />' +
'<meta name="viewport" content="width=device-width, initial-scale=1.0" />' +
`<title>${title}</title>` +
'<link rel="preconnect" href="https://fonts.googleapis.com">' +
'<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>' +
`<link href="${FONTS_HREF}" rel="stylesheet">` +
STYLE_BLOCK +
'</head>' +
'<body>' +
LOGO_SYMBOL +
`<div class="deck" id="deck" data-digest-indexes='${JSON.stringify(digestIndexes)}'>` +
pagesHtml.join('') +
'</div>' +
'<div class="nav-dots" id="navDots"></div>' +
'<div class="hint">← → / swipe / scroll</div>' +
NAV_SCRIPT +
'</body>' +
'</html>'
);
}

103
shared/brief-envelope.d.ts vendored Normal file
View File

@@ -0,0 +1,103 @@
// Type declarations for shared/brief-envelope.js.
//
// The envelope is the integration boundary between the per-user brief
// composer (Railway worker, future Phase 3) and every consumer surface:
// the hosted magazine edge route, the dashboard panel preview RPC, the
// email teaser renderer, the carousel renderer, and the Tauri in-app
// reader. All consumers read the same brief:{userId}:{issueDate} Redis
// key and bind to this contract.
//
// Intentionally NOT wrapped in the seed-envelope `_seed` frame. A brief
// is 1 producer -> 1 user -> 1 read (7-day TTL), not a global public
// seed; reusing `_seed` here invites code that mis-applies seed
// invariants (staleness gating, api/health.js SEED_META pairing, etc.)
// to per-user keys. The version constant lives on the envelope root.
//
// Forbidden fields: importanceScore, primaryLink, pubDate, and any AI
// model / provider / cache timestamp strings must NOT appear in
// BriefEnvelope.data. They exist upstream in news:insights:v1 but are
// stripped at compose time. See PR #3143 for the notify-endpoint fix
// that established this rule.
export const BRIEF_ENVELOPE_VERSION: 1;
/**
* Severity ladder. Four values, no synonyms. `critical` and `high`
* render with the highlight treatment; `medium` and `low` render
* plain. See HIGHLIGHTED_LEVELS in the renderer.
*/
export type BriefThreatLevel = 'critical' | 'high' | 'medium' | 'low';
export interface BriefUser {
/** Display name used in the greeting and back-cover chrome. */
name: string;
/** IANA timezone string, e.g. "UTC", "Europe/Paris". */
tz: string;
}
export interface BriefNumbers {
/** Total story clusters ingested globally in the last 24h. */
clusters: number;
/** Multi-source confirmed events globally in the last 24h. */
multiSource: number;
/** Stories surfaced in THIS user's brief. Must equal stories.length. */
surfaced: number;
}
export interface BriefThread {
/** Short editorial label, e.g. "Energy", "Diplomacy". */
tag: string;
/** One-sentence teaser, no trailing period required. */
teaser: string;
}
export interface BriefDigest {
/** e.g. "Good evening." — time-of-day aware in user.tz. */
greeting: string;
/** Executive summary paragraph — italic pull-quote in the magazine. */
lead: string;
numbers: BriefNumbers;
/** Threads to watch today. Renderer splits into 03a/03b when > 6. */
threads: BriefThread[];
/** Signals-to-watch. The "04 · Signals" page is omitted when empty. */
signals: string[];
}
export interface BriefStory {
/** Editorial category label. */
category: string;
/** ISO-2 country code (or composite like "IL / LB"). */
country: string;
threatLevel: BriefThreatLevel;
headline: string;
description: string;
/** Publication/wire attribution only (no importance score, no URL). */
source: string;
/** Per-user LLM-generated rationale. */
whyMatters: string;
}
export interface BriefData {
user: BriefUser;
/** Short issue code, e.g. "17.04". */
issue: string;
/** ISO date "YYYY-MM-DD" in user.tz. */
date: string;
/** Long-form human date, e.g. "17 April 2026". */
dateLong: string;
digest: BriefDigest;
stories: BriefStory[];
}
/**
* Canonical envelope stored at brief:{userId}:{issueDate} in Redis.
* Renderer + future composer + future consumers must all pin to
* `version === BRIEF_ENVELOPE_VERSION` at runtime — see the consumer
* drift incident (PR #3139) for why.
*/
export interface BriefEnvelope {
version: typeof BRIEF_ENVELOPE_VERSION;
/** Unix ms when the envelope was composed. Informational only. */
issuedAt: number;
data: BriefData;
}

26
shared/brief-envelope.js Normal file
View File

@@ -0,0 +1,26 @@
// Runtime surface for shared/brief-envelope.d.ts.
//
// The envelope is a pure data contract — no behaviour to export beyond
// the schema version constant. Types live in the sibling .d.ts and flow
// through JSDoc @typedef pointers below so .mjs consumers get editor
// hints without a build step.
/**
* @typedef {import('./brief-envelope.js').BriefEnvelope} BriefEnvelope
* @typedef {import('./brief-envelope.js').BriefData} BriefData
* @typedef {import('./brief-envelope.js').BriefStory} BriefStory
* @typedef {import('./brief-envelope.js').BriefDigest} BriefDigest
* @typedef {import('./brief-envelope.js').BriefThread} BriefThread
* @typedef {import('./brief-envelope.js').BriefThreatLevel} BriefThreatLevel
*/
/**
* Schema version stamped on every Redis-resident brief. Bump when any
* shape in brief-envelope.d.ts changes in a way that existing consumers
* cannot ignore. Envelope-version drift is the primary failure mode for
* this pipeline (see the seed-envelope-consumer-drift incident, PR
* #3139) — coordinate every producer + consumer update in the same PR.
*
* @type {1}
*/
export const BRIEF_ENVELOPE_VERSION = 1;

View File

@@ -0,0 +1,419 @@
// Shape tests for the deterministic brief magazine renderer.
//
// The renderer is pure — same envelope in, same HTML out. These tests
// pin down the page-sequence rules that the rest of the pipeline
// (edge route, dashboard panel, email teaser, carousel, Tauri reader)
// depends on. If one of these breaks, every consumer gets confused.
//
// The forbidden-field guard protects the invariant that the renderer
// only ever interpolates `envelope.data.*` fields. We prove this two
// ways: (1) assert known field-name TOKENS (JSON keys like
// `"importanceScore":`) never appear in the output, and (2) inject
// sentinels into non-`data` locations of the envelope and assert the
// sentinels are absent. The earlier version of this test matched bare
// substrings like "openai" / "claude" / "gemini", which false-fails
// on any legitimate story covering those companies.
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
import { renderBriefMagazine } from '../server/_shared/brief-render.js';
import { BRIEF_ENVELOPE_VERSION } from '../shared/brief-envelope.js';
/**
* @typedef {import('../shared/brief-envelope.js').BriefEnvelope} BriefEnvelope
* @typedef {import('../shared/brief-envelope.js').BriefStory} BriefStory
* @typedef {import('../shared/brief-envelope.js').BriefThread} BriefThread
*/
// ── Fixtures ─────────────────────────────────────────────────────────────────
/** @returns {BriefStory} */
function story(overrides = {}) {
return {
category: 'Energy',
country: 'IR',
threatLevel: 'high',
headline: 'Iran declares Strait of Hormuz open. Oil drops more than 9%.',
description:
'Tehran publicly reopened the Strait of Hormuz to commercial shipping today.',
source: 'Multiple wires',
whyMatters:
'Hormuz is roughly a fifth of global seaborne oil — a 9% move in a single session is a repricing, not a wobble.',
...overrides,
};
}
/** @returns {BriefThread} */
function thread(tag, teaser) {
return { tag, teaser };
}
/**
* @param {Partial<import('../shared/brief-envelope.js').BriefData>} overrides
* @returns {BriefEnvelope}
*/
function envelope(overrides = {}) {
const data = {
user: { name: 'Elie', tz: 'UTC' },
issue: '17.04',
date: '2026-04-17',
dateLong: '17 April 2026',
digest: {
greeting: 'Good evening.',
lead: 'The most impactful development today is the reopening of the Strait of Hormuz.',
numbers: { clusters: 278, multiSource: 21, surfaced: 4 },
threads: [
thread('Energy', 'Iran reopens the Strait of Hormuz.'),
thread('Diplomacy', 'Israel\u2013Lebanon ceasefire takes effect.'),
thread('Maritime', 'US military expands posture against Iran-linked shipping.'),
thread('Humanitarian', 'A record year at sea for Rohingya refugees.'),
],
signals: [
'Adherence to the Israel\u2013Lebanon ceasefire in the first 72 hours.',
'Long-term stability of commercial shipping through Hormuz.',
],
},
stories: [
story(),
story({ country: 'IL', category: 'Diplomacy' }),
story({ country: 'US', category: 'Maritime', threatLevel: 'critical' }),
story({ country: 'MM', category: 'Humanitarian' }),
],
...overrides,
};
return {
version: BRIEF_ENVELOPE_VERSION,
issuedAt: 1_700_000_000_000,
data,
};
}
// ── Helpers ──────────────────────────────────────────────────────────────────
/** @param {string} html */
function pageCount(html) {
const matches = html.match(/<section class="page/g);
return matches ? matches.length : 0;
}
// ── Tests ────────────────────────────────────────────────────────────────────
describe('renderBriefMagazine — page sequence', () => {
it('default case: cover + 4 digest pages + N stories + back cover = N + 6', () => {
const env = envelope();
const html = renderBriefMagazine(env);
assert.equal(pageCount(html), env.data.stories.length + 6);
});
it('omits the Signals page when signals is empty', () => {
const env = envelope({
digest: {
...envelope().data.digest,
signals: [],
},
});
const html = renderBriefMagazine(env);
// cover + greeting + numbers + threads + N stories + back = N + 5
assert.equal(pageCount(html), env.data.stories.length + 5);
assert.ok(!html.includes('Digest / 04'), 'Signals page label should not appear');
assert.ok(!html.includes('Signals To Watch'), 'Signals heading should not appear');
});
it('splits On The Desk into 03a + 03b when threads.length > 6', () => {
const manyThreads = Array.from({ length: 8 }, (_, i) =>
thread(`Tag${i}`, `Teaser number ${i}.`),
);
const env = envelope({
digest: { ...envelope().data.digest, threads: manyThreads },
});
const html = renderBriefMagazine(env);
assert.ok(html.includes('Digest / 03a'), 'must emit 03a label');
assert.ok(html.includes('Digest / 03b'), 'must emit 03b label');
assert.ok(!html.includes('Digest / 03 \u2014 On The Desk'), 'must not emit the single-page label');
// cover + greeting + numbers + 03a + 03b + signals + N stories + back = N + 7
assert.equal(pageCount(html), env.data.stories.length + 7);
});
it('splits On The Desk even when signals is empty (still two threads pages)', () => {
const manyThreads = Array.from({ length: 10 }, (_, i) =>
thread(`Tag${i}`, `Teaser ${i}.`),
);
const env = envelope({
digest: { ...envelope().data.digest, threads: manyThreads, signals: [] },
});
const html = renderBriefMagazine(env);
// cover + greeting + numbers + 03a + 03b + N stories + back = N + 6
assert.equal(pageCount(html), env.data.stories.length + 6);
assert.ok(html.includes('03a'));
assert.ok(html.includes('03b'));
});
it('alternates story palette starting with light', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const storyMatches = [...html.matchAll(/<section class="page story (light|dark)"/g)];
assert.equal(storyMatches.length, env.data.stories.length);
storyMatches.forEach((m, i) => {
const expected = i % 2 === 0 ? 'light' : 'dark';
assert.equal(m[1], expected, `story ${i + 1} palette`);
});
});
it('zero-pads the surfaced stat and story rank numbers', () => {
const env = envelope({
digest: { ...envelope().data.digest, numbers: { clusters: 5, multiSource: 2, surfaced: 4 } },
});
const html = renderBriefMagazine(env);
assert.ok(html.includes('<div class="stat-num">04</div>'));
assert.ok(html.includes('<div class="rank-ghost">01</div>'));
assert.ok(html.includes('<div class="rank-ghost">04</div>'));
});
});
describe('renderBriefMagazine — chrome invariants', () => {
it('logo symbol is emitted exactly once; all placements reference it via <use>', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const symbolDefs = html.match(/<symbol id="wm-logo-core"/g) || [];
assert.equal(symbolDefs.length, 1, 'exactly one symbol definition');
// 1 cover + 4 digest pages + N story chromes + 1 back cover = N + 6 logo references
const useRefs = html.match(/<use href="#wm-logo-core"\s*\/>/g) || [];
const expected = 1 + 4 + env.data.stories.length + 1;
assert.equal(useRefs.length, expected);
// Every reference still carries the aria label for a11y.
const ariaLabels = html.match(/aria-label="WorldMonitor"/g) || [];
assert.equal(ariaLabels.length, expected);
});
it('every page is full-bleed (100vw / 100vh declared in the shared stylesheet)', () => {
const html = renderBriefMagazine(envelope());
assert.ok(/\.page\s*\{[^}]*flex:\s*0\s*0\s*100vw/.test(html));
assert.ok(/\.page\s*\{[^}]*height:\s*100vh/.test(html));
});
it('emits the dot-navigation container and digest-index dataset', () => {
const html = renderBriefMagazine(envelope());
assert.ok(html.includes('id="navDots"'));
const m = html.match(/data-digest-indexes='(\[[^']+\])'/);
assert.ok(m, 'deck must expose digest indexes to nav script');
const arr = JSON.parse(m[1]);
assert.ok(Array.isArray(arr));
assert.equal(arr.length, 4, 'default envelope has 4 digest pages');
assert.ok(arr.every((n) => typeof n === 'number'), 'digest indexes are numbers only');
});
it('each story page has a three-tag row (category, country, threat level)', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const tagRows = html.match(/<div class="tag-row">([\s\S]*?)<\/div>\s*<h3/g) || [];
assert.equal(tagRows.length, env.data.stories.length);
for (const row of tagRows) {
const tags = row.match(/<span class="tag[^"]*">/g) || [];
assert.equal(tags.length, 3, `expected 3 tags, got ${tags.length} in ${row}`);
}
});
it('page numbers are 1-indexed and count up to the total', () => {
const env = envelope();
const html = renderBriefMagazine(env);
const total = pageCount(html);
const nums = [...html.matchAll(/<div class="page-number mono">(\d{2}) \/ (\d{2})<\/div>/g)];
assert.equal(nums.length, total);
nums.forEach((m, i) => {
assert.equal(Number(m[1]), i + 1);
assert.equal(Number(m[2]), total);
});
});
it('applies .crit highlight to critical and high threat levels only', () => {
const env = envelope({
stories: [
story({ threatLevel: 'critical' }),
story({ threatLevel: 'high' }),
story({ threatLevel: 'medium' }),
story({ threatLevel: 'low' }),
],
});
const html = renderBriefMagazine(env);
// "Critical" and "High" tags get the .crit class; "Medium" and "Low" do not.
assert.ok(html.includes('<span class="tag crit">Critical</span>'));
assert.ok(html.includes('<span class="tag crit">High</span>'));
assert.ok(html.includes('<span class="tag">Medium</span>'));
assert.ok(html.includes('<span class="tag">Low</span>'));
});
});
describe('renderBriefMagazine — envelope internals never leak into HTML', () => {
// Structural invariant: the renderer only reads `envelope.data.*`.
// We verify this two ways: (1) field-name tokens that only appear in
// upstream seed data (importanceScore, etc.) never leak; (2) sentinel
// values injected into non-data envelope locations are absent from
// the output.
it('does not emit upstream seed field-name tokens as JSON keys or bare names', () => {
const env = envelope();
const html = renderBriefMagazine(env);
// Field-name tokens — these are structural keys that would only
// appear if the renderer accidentally interpolated an envelope
// object (e.g. JSON.stringify(envelope)). Free-text content
// cannot plausibly emit `"importanceScore":` or `_seed`.
const forbiddenKeys = [
'"importanceScore"',
'"primaryLink"',
'"pubDate"',
'"generatedAt"',
'"briefModel"',
'"briefProvider"',
'"fetchedAt"',
'"recordCount"',
'"_seed"',
];
for (const token of forbiddenKeys) {
assert.ok(!html.includes(token), `forbidden token ${token} appeared in HTML`);
}
});
it('validator rejects extension fields on envelope root (importanceScore, _seed, etc.)', () => {
// Stricter than "renderer does not interpolate them". Forbidden
// fields must be impossible to PERSIST in the envelope at all —
// the renderer runs after they are already written to Redis, so
// the only place the invariant can live is the validator at
// write + read time.
const env = /** @type {any} */ ({
...envelope(),
importanceScore: 999,
primaryLink: 'https://example.com',
pubDate: 123,
_seed: { version: 1, fetchedAt: 0 },
});
assert.throws(() => renderBriefMagazine(env), /envelope has unexpected key/);
});
it('HTML-escapes user-provided content (no raw angle brackets from stories)', () => {
const env = envelope({
stories: [
story({
headline: 'Something with <script>alert(1)</script> in it',
whyMatters: 'Why matters with <img src=x> attempt',
}),
...envelope().data.stories.slice(1),
],
});
const html = renderBriefMagazine(env);
assert.ok(!html.includes('<script>alert(1)</script>'));
assert.ok(!html.includes('<img src=x>'));
assert.ok(html.includes('&lt;script&gt;alert(1)&lt;/script&gt;'));
});
});
describe('renderBriefMagazine — envelope validation', () => {
it('throws when envelope is not an object', () => {
assert.throws(() => renderBriefMagazine(/** @type {any} */ (null)), /must be an object/);
assert.throws(() => renderBriefMagazine(/** @type {any} */ ('string')), /must be an object/);
});
it('throws when version does not match BRIEF_ENVELOPE_VERSION', () => {
const env = /** @type {any} */ ({ ...envelope(), version: 99 });
assert.throws(
() => renderBriefMagazine(env),
/version.*does not match renderer version/,
);
});
it('throws when issuedAt is missing or non-finite', () => {
const env = /** @type {any} */ ({ ...envelope() });
delete env.issuedAt;
assert.throws(() => renderBriefMagazine(env), /issuedAt/);
});
it('throws when envelope.data is missing', () => {
const env = /** @type {any} */ ({ version: BRIEF_ENVELOPE_VERSION, issuedAt: 0 });
assert.throws(() => renderBriefMagazine(env), /envelope\.data is required/);
});
it('throws when envelope.data.date is not YYYY-MM-DD', () => {
const env = envelope();
env.data.date = '04/17/2026';
assert.throws(() => renderBriefMagazine(env), /YYYY-MM-DD/);
});
it('throws when digest.signals is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.signals;
assert.throws(() => renderBriefMagazine(env), /digest\.signals must be an array/);
});
it('throws when digest.threads is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.threads;
assert.throws(() => renderBriefMagazine(env), /digest\.threads must be an array/);
});
it('throws when digest.numbers.clusters is missing', () => {
const env = /** @type {any} */ (envelope());
delete env.data.digest.numbers.clusters;
assert.throws(() => renderBriefMagazine(env), /digest\.numbers\.clusters/);
});
it('throws when a story has an invalid threatLevel', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).threatLevel = 'moderate';
assert.throws(
() => renderBriefMagazine(env),
/threatLevel must be one of critical\|high\|medium\|low/,
);
});
it('throws when stories is empty', () => {
const env = envelope({ stories: [] });
assert.throws(() => renderBriefMagazine(env), /stories must be a non-empty array/);
});
it('throws when a story carries an extension field (importanceScore, etc.)', () => {
const env = envelope();
/** @type {any} */ (env.data.stories[0]).importanceScore = 999;
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.stories\[0\] has unexpected key "importanceScore"/,
);
});
it('throws when envelope.data carries an extra key', () => {
const env = /** @type {any} */ (envelope());
env.data.primaryLink = 'https://leak.example/story';
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data has unexpected key "primaryLink"/,
);
});
it('throws when digest.numbers carries an extra key', () => {
const env = /** @type {any} */ (envelope());
env.data.digest.numbers.fetchedAt = Date.now();
assert.throws(
() => renderBriefMagazine(env),
/envelope\.data\.digest\.numbers has unexpected key "fetchedAt"/,
);
});
it('throws when digest.numbers.surfaced does not equal stories.length', () => {
// Cover copy ("N threads that shaped the world today") and the
// at-a-glance stat both surface this count; the validator must
// keep them in lockstep so no brief can ship a self-contradictory
// number.
const env = envelope();
env.data.digest.numbers.surfaced = 99;
assert.throws(
() => renderBriefMagazine(env),
/surfaced=99 must equal.*stories\.length=4/,
);
});
});
describe('BRIEF_ENVELOPE_VERSION', () => {
it('is the literal 1 (bump requires cross-producer coordination)', () => {
assert.equal(BRIEF_ENVELOPE_VERSION, 1);
});
});