Files
worldmonitor/server
Elie Habib 66ca645571 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.
2026-04-18 00:01:57 +04:00
..