---
path: /veritas/topic
surface: customer
domain: veritas
auth: signed-in
source: apps/oshun/web/src/app/veritas/topic/page.tsx
status: walked
last_walked:
  '2026-05-29 automated runtime walk (Playwright headless) — render, /v1 data
  (2xx), console/page-errors, expected content, screenshot verified; live
  screen-reader, touch, offline, and telemetry-delivery checks pending a manual
  AT pass. Evidence: WALKTHROUGH/results/runtime-sweep-2026-05-29.md; content
  re-verified 2026-06-03 against current source; 2026-06-29 Playwright re-walk
  added row/cell-level topic anchors and direct route assertions'
---

# Veritas · topic hub

## Purpose

A topic hub: what is settled with high confidence, what is open / contested /
moving, what the reader has read so far on this topic, and what gaps Lilith has
named. A separate column lists primary sources for the topic and the reader's
"follow" subscriptions. Thin server component awaits `getTopicHub()` and renders
`VeritasTopic`.

## Entry points

- **Sub-nav "Topic hubs" tab** on `/veritas` — verify this is the target
- **Linked from `/veritas`** evidence rail — when a claim belongs to a topic
  with a hub (verify)
- **Linked from `/explore`** — discovery of grounded topics (verify)
- **Direct URL / bookmark** — yes (auth required)
- **Internal namespace alternate** — `/domains/veritas/topics` and
  `/domains/veritas/topics/[topicId]` render a different (workspace-style) view;
  see those files

## Layout regions

`page.tsx` awaits `getTopicHub()` → `<VeritasTopic>`. The component mounts
`LWebShell` with `LCustomerNav active="library"`.

- **Header**: Lilith customer nav (Library tab active)
- **Masthead (`VrMast`)**:
  `Veritas · the topic | energy · norway · winter 2024–25 | <storyCount> stories · <sourceCount> sources · <openDisputeCount> open disputes`,
  display title "The topic, in _one place_.", italic lede
- **Two-column grid (1.4fr / 1fr)**:
  - **Left — Settled / Open / What you've read**:
    - `VrHead`: `I · Settled, broadly · What we believe with high confidence.`
    - Per `data.settled[i]`: solid-border card with italic statement (16px),
      `conf. <row.confidence>` code on the right, eyebrow `<row.sourceCount>`
      below
    - `VrHead`: `II · Open · contested · moving · What is not yet decided.`
    - Per `data.open[i]`: dashed-border card with italic statement, italic
      subline, and medium-confidence `whenLabel` code
    - `VrHead`:
      `III · What you've read · From this topic, on Veritas. · "14 of 47 stories · last 30 days"`
      (right text is hardcoded — verify)
    - **Reading heatmap**: 7-column CSS grid of square cells; cell colour
      derived from `data.readingHistory.readPattern[i]` value (accent /
      `#d99777` / paper / `L.rule2`) — count is `data.readingHistory.totalCells`
    - Heatmap legend: "Each cell, one day. Solid · read; hollow · skipped."
  - **Right — Primary sources / Gaps / Follow**:
    - `VrHead`: `— · Primary sources · What we go to first.`
    - Per `data.primarySources[i]`: italic name + description, `Nx cited` code,
      `VrBand` chip
    - `VrHead`: `— · Gaps Lilith named · Where the topic is thin.`
    - Paper-bg card with italic copy from `data.gapsLines[]` (one line per gap,
      separated by `<br />`)
    - `VrHead`: `— · Follow · Tell me when this moves.`
    - Per `data.follows[i]`: italic label + `● <on>` accent code (looks like a
      status pill — verify whether it should be a toggle)

## States

- [ ] **Loading** — `getTopicHub()` awaited server-side; no client skeleton
- [x] **Populated** — fixture renders all sections; verified through
      `data-veritas-topic-page`, settled/open/source/follow row anchors, gaps
      line anchors, and the reading-history cell grid
- [ ] **No open items** — `data.open[]` empty; the section still renders header
      but maps over an empty array (verify graceful behaviour)
- [ ] **No reading history** — `data.readingHistory.readPattern[]` of all zeros
      yields all-accent cells (verify the colour ramp; value 0 maps to
      `L.accent` per the ternary)
- [ ] **No follows** — `data.follows[]` empty; verify rendering
- [ ] **Error (recoverable)** — `getTopicHub()` rejects → nearest `error.tsx`
- [ ] **Offline** — cached HTML; nothing client-side here
- [ ] **Standalone PWA** — 1.4fr/1fr grid; verify collapse on narrow screens
- [x] **Empty** — N/A; route always loads one topic
- [ ] **Gated** — none coded

## Interactions

### Section I — Settled

- [x] **Settled card** — non-interactive; Playwright asserts every fixture row
      through `data-veritas-topic-settled-row` attributes for statement,
      confidence, and source-count; verify whether each statement should link to
      its canonical story / claim
- [ ] **`conf. <N>` code** — text only

### Section II — Open

- [x] **Open card** (dashed border) — non-interactive; Playwright asserts row
      attributes for statement and `whenLabel`
- [x] **`whenLabel` code** — text only; e.g., "decision expected by …"

### Section III — Reading heatmap

- [x] **Cell** (per day) — non-interactive; Playwright asserts 28
      `data-veritas-topic-reading-cell` nodes and the first five fixture values.
      Verify whether hovering should reveal what was read that day (no tooltip
      in code)
- [x] **Legend** — text only

### Right column — Primary sources

- [x] **Source row** (per source)
  - Function: not a link in code; Playwright asserts the static row contract via
    `data-veritas-topic-primary-source-row`, name, band, and cited-count attrs.
    Verify whether each row should target `/veritas/source` for that source
  - `VrBand` chip — non-interactive

### Right column — Gaps

- [x] **Gaps card** — non-interactive; multiple lines separated by `<br />`;
      Playwright asserts `data-veritas-topic-gap-line` count and card copy

### Right column — Follow

- [x] **Follow row** (per subscription)
  - Function: appears to be a status pill (`● on`); no toggle in code;
    Playwright asserts `data-veritas-topic-follow-row` state attrs and confirms
    the polished row has no nested button. Verify whether it should become a
    button that flips the subscription
  - The internal namespace alternate (`VeritasTopicHubWorkspace`) does have a
    real `Follow topic` toggle backed by the Veritas store

## Data & contracts

- **Reads**: `getTopicHub()` from `@/lib/lilith-data/veritas-depth` — returns
  `TopicHubData` ({ storyCount, sourceCount, openDisputeCount, settled[],
  open[], readingHistory.{totalCells, readPattern}, primarySources[].{name,
  description, citedCount, band}, gapsLines[], follows[].{label, on} })
- **Writes**: none in this view; the internal-namespace workspace does write via
  the Veritas store (`followTopic`, `unfollowTopic`, `updateTopicAlert`)
- **Realtime**: none in this view; consider whether "follow" should be realtime
- **Caching**: server fetch; revalidation per fixture policy; note that
  `readingHistory` is per-user
- **Auth/role check**: shell middleware (verify); per-user cache key required
  because of reading history
- **Page metadata**: `title: "Veritas · topic hub"`, description "What is
  settled, what is still moving, what you have read so far, and what gaps
  remain.", `alternates.canonical: /veritas/topic`

## E2E coverage

- `apps/oshun/web/e2e/veritas-evidence-trail.spec.ts` — step 8 now opens the
  shipped `/veritas/topic` route and verifies the masthead counts, settled/open
  row attrs, 28-cell reading-history grid, primary-source rows, gaps lines, and
  static follow-status rows against real dev web+BFF infra

## Cross-references

- Component: `apps/oshun/web/src/components/lilith/veritas.tsx` — `VeritasTopic`
  (lines 1353–1597)
- Data: `apps/oshun/web/src/lib/lilith-data/veritas-depth.ts` (`getTopicHub`)
- Internal namespace counterpart:
  - [`domains-veritas-topics.md`](./domains-veritas-topics.md)
  - [`domains-veritas-topics-topicId.md`](./domains-veritas-topics-topicId.md)
- Sibling routes:
  - [`veritas.md`](./veritas.md), [`veritas-claim.md`](./veritas-claim.md),
    [`veritas-counterclaims.md`](./veritas-counterclaims.md),
    [`veritas-source.md`](./veritas-source.md),
    [`veritas-story.md`](./veritas-story.md)
- Feature spec: [`V1/features.md`](../../../V1/features.md#veritas)

## Open questions / known gaps

- [ ] The "14 of 47 stories · last 30 days" right-side text on Section III is
      hardcoded in the JSX, not derived from `data` — confirm intent
- [ ] Follow rows look like toggles but are static in code; cross-reference
      `VeritasTopicHubWorkspace` (which has the real toggle) and decide whether
      this surface should align
- [ ] No dynamic route segment in the path (`/veritas/topic`); the fixture
      describes one topic. Confirm the production plan
- [ ] Reading-history cell colours: `v < 2` maps to `L.accent`, which means
      "unread" (value 0) renders the same colour as the most-read cell; verify
      the colour ramp polarity
- [ ] `LCustomerNav active="library"` — consistent with the `/veritas` hub and
      the other depth views, which all use Library (no Explore inconsistency)
