---
path: /domains/veritas/articles
surface: customer
domain: veritas
auth: signed-in
source: apps/oshun/web/src/app/domains/veritas/articles/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. 2026-06-27
  Codex addendum — added granular Playwright coverage in
  apps/oshun/web/e2e/veritas-article-analyzer.spec.ts for input/filter/close,
  sample analysis, claim-source expansion, compare switching, offline URL
  simulation, reduced motion, and standalone mobile overflow; updated component
  hooks and mobile grid behaviour. 2026-06-30 Codex addendum — added and covered
  analyzer entry links from `/domains/veritas` shell tools and the `/veritas`
  broadsheet subnav; corrected the stale standalone `/domains` dashboard
  assumption.'
---

# Article Analyzer · Veritas

## Purpose

The Veritas Article Analyzer — a full-screen overlay that ingests an article URL
(or sample article) and produces a multi-section breakdown: readability,
sentiment, named entities, claim extraction, source quality, and bias. Side
panel supports a compare-against-another-article view.

## Entry points

- **`/domains/veritas` shell surface** — More verification tools exposes an
  "Article Analyzer" link (`data-veritas-secondary-tool-link="articles"`) to
  `/domains/veritas/articles`
- **`/veritas` broadsheet subnav** — "Article analyzer"
  (`data-lilith-subnav-item-id="articles"`) links to `/domains/veritas/articles`
- **Direct URL / bookmark** — yes (auth required)

There is no standalone `/domains` dashboard route in the current app; domain
switcher paths land on concrete domain surfaces such as `/domains/veritas`.

## Layout regions

`page.tsx` is a `'use client'` component that renders an `sr-only` `<h1>` then
mounts `<VeritasArticleAnalyzer onClose={() => router.back()} />`. The analyzer
is a fixed-position overlay (`position: fixed; inset: 0; zIndex: 40`).

- **Sticky header**: domain logo / `Article Analyzer` title, tablist for
  `input | analysis | compare` (only visible when `view !== 'input'`), Close
  button (X icon, accessible name "Close", ≥44×44 target)
- **Body** (scrollable):
  - **Input view** (`view === 'input'`):
    - URL input card: 28px headline "Article Analyzer", lede about what it
      analyzes, link icon + text input (`placeholder="Paste article URL..."`),
      primary `Analyze` button (disabled when input empty or analyzing;
      "Analyzing..." pulse text on submit, ~1.8s simulated delay)
    - Sample Articles section: category chip filter (`all` + unique categories
      from `SAMPLE_ARTICLES`); grid of story overview cards
      (`auto-fit, minmax(min(100%, 380px), 1fr)`); each card is a keyboard
      reachable button and shows sentiment colour indicator and claim
      verification status
  - **Analysis view** (`view === 'analysis'`, requires `selectedArticle`):
    - Multi-section analysis grouped by collapsible panels: `readability`,
      `sentiment`, `entities`, `claims`, `source`, `bias` (all expanded by
      default per `expandedSections` initial state)
  - **Compare view** (`view === 'compare'`, requires `compareArticle`): two
    article columns laid side-by-side on wide viewports and stacked on phones
- **Footer**: none beyond the overlay body padding

## States

- [x] **Loading (initial)** — the overlay renders synchronously; no loading
      skeleton for the input view
- [x] **Analyzing** — `Analyze` button shows "Analyzing..." with the
      `oshun-aa-pulse` keyframe; URL input remains visible
- [x] **Empty input** — `Analyze` button is `disabled` and rendered at 50%
      opacity; cursor `not-allowed`
- [x] **Populated (sample selected)** — clicking a sample card transitions to
      `analysis` view via `handleSelectSample`
- [x] **Populated (URL submitted)** — after the 1.8s timeout, a random
      `SAMPLE_ARTICLES` entry is picked, its `url` overridden with the submitted
      URL, and the view switches to `analysis`
- [x] **Compare view with no second article** — `compare` tab is disabled when
      `compareArticle` is unset
- [x] **Filter: no matches** — N/A through the shipped UI because category chips
      are derived from categories present in `SAMPLE_ARTICLES`; the test
      verifies a real category filter and the resulting count
- [x] **Offline** — no network calls in code (uses `SAMPLE_ARTICLES`
      simulation); offline is effectively a no-op
- [x] **Standalone PWA** — full-screen overlay renders cleanly; verify safe area
      at the top
- [x] **Reduced motion** — pulse and scan-line keyframes are injected via
      `<style>`; verify `prefers-reduced-motion` honoured

## Interactions

### Header

- [x] **Close** (X icon button, top-right of header)
  - Function: invokes `onClose` → `router.back()`
  - Keyboard: Enter/Space activates
  - Screen reader: accessible name is "Close"
  - Touch target: verified ≥ 44×44 CSS px

### Header tablist (when not in input view)

- [x] **input** (button)
- [x] **analysis** (button)
  - Disabled when `selectedArticle === null` (opacity 0.4)
- [x] **compare** (button)
  - Disabled when `compareArticle === null` (opacity 0.4)

### Input view — URL form

- [x] **URL input** (`<input type="text">`)
  - Function: bound to `urlInput` state
  - Keyboard: Enter triggers `handleAnalyze`
  - Placeholder: `Paste article URL...`
  - `inputRef` autofocuses when the input view is active
- [x] **Analyze** (primary button)
  - Function: `handleAnalyze` → 1.8s `setTimeout` → switch to analysis view with
    a random `SAMPLE_ARTICLES` entry whose `url` is overridden
  - Disabled when input empty or already analyzing

### Input view — Sample articles

- [x] **Category chip** (per category, including `all`)
  - Function: `setCategoryFilter(cat)`; capitalises label
  - Active style: accent border + 15% accent background
- [x] **Sample article card** (per `filteredArticles`)
  - Function: `handleSelectSample(article)` → switches to analysis view
  - Visual indicators: sentiment colour (`SENTIMENT_COLORS`), claim status
    (`CLAIM_STATUS_CONFIG`), bias position chip

### Analysis view — Sections

- [x] **Section header** (expand/collapse, per `expandedSections` set)
  - Function: `toggleSection(id)` adds/removes from `expandedSections`
  - All sections start expanded
  - Sections: readability, sentiment, entities, claims, source, bias

### Analysis view — section content (sampled by code)

- [x] **Readability gauge** (`ReadabilityGauge`) — colour-coded score (>=70
      ok, >=50 warn, >=30 alert, else alert) — visual chart with score and label
- [x] **Sentiment label badge** (`bdg(...)`) — pill driven by
      `SENTIMENT_COLORS[label]`
- [x] **Named entities** — chips with `ENTITY_CONFIG` icon + colour by
      `EntityCategory` (person, organization, location, date, money, event,
      product, legislation, statistic)
- [x] **Claims list** — each claim shows `CLAIM_STATUS_CONFIG` badge (verified /
      disputed / unverified / false / misleading) and an evidence list
- [x] **Bias position bar** — `BIAS_POSITION` markers at 0/25/50/75/100 for left
      → right; current article's bias rendered as accent point

### Analysis view → Compare

- [x] **"Compare against…" affordance** (button) — `startCompare(article)` sets
      `compareArticle` and switches view to `compare`. Verify the visible label
      and which card surfaces it

### Compare view

- [x] Two article columns side by side; fields diffed include header/source,
      readability, sentiment, bias, claim summary, and source reliability
- [x] **Back to analysis** (button) — `setView('analysis')`

## Data & contracts

- **Reads**: `SAMPLE_ARTICLES` from `@/lib/veritas/veritas-extended-simulation`
  — fixture-only; the URL input does not make a real network call
- **Writes**: none externally; local state mutations only
- **Realtime**: none
- **Caching**: not applicable (no network); analyzer is client-only
- **Auth/role check**: shell middleware enforces signed-in (verify rule for
  `/domains/veritas/*`)
- **Trust helpers**: `formatVeritasSignalBand`, `mapStatusToVeritasVerdict`,
  `resolveWebVeritasTrustRule` from `./veritasTrustLabels`
- **Random source**: `webRandomUnitInterval` from `@/lib/random` (used to pick a
  fake "result" article)
- **Page metadata**: not set (no `Metadata` export in this client-side page);
  the screen reader `<h1>` reads "Article Analyzer · Veritas · OSHUN"

## Current E2E evidence

- `apps/oshun/web/e2e/veritas-article-analyzer.spec.ts`
  - Entry paths: opens the analyzer from `/domains/veritas` via the More
    verification tools link, closes back through browser history, then opens the
    analyzer from the `/veritas` broadsheet subnav.
  - Input state: autofocus, empty disabled analyze button, category filter,
    reduced-motion override, close button accessible name / 44px target, and
    keyboard close returning to the previous shell route.
  - Analysis state: canonical sample card selection, selected article contract,
    tab active/disabled state, all core sections, section collapse/restore,
    claim-source expansion, and source support flag.
  - Compare state: compare action, compare tab enablement, default second
    article, switching comparison source, back to analysis, and new-analysis
    return.
  - Failure modes: browser-offline URL submission still completes through the
    local simulation with the submitted URL preserved; standalone PWA launch at
    390px verifies input, analysis, and compare views do not horizontally
    overflow.

## Cross-references

- Component:
  `apps/oshun/web/src/components/domains/veritas/VeritasArticleAnalyzer.tsx`
- Constants: `SENTIMENT_COLORS`, `ENTITY_CONFIG`, `CLAIM_STATUS_CONFIG`,
  `BIAS_POSITION` (lines 80–113)
- Shared design system: `ClaimListItem`, `SourceListItem` from
  `@/design-system/components/*`, `tokens` from `@/design-system/tokens`,
  `LV as L`, `SERIF_L` from `@/design-system/lilith`
- Sibling tools (similar overlay shape):
  - [`domains-veritas-bias.md`](./domains-veritas-bias.md)
  - [`domains-veritas-claims.md`](./domains-veritas-claims.md)
  - [`domains-veritas-fact-check.md`](./domains-veritas-fact-check.md)
  - [`domains-veritas-headlines.md`](./domains-veritas-headlines.md)
  - [`domains-veritas-knowledge-graph.md`](./domains-veritas-knowledge-graph.md)
  - [`domains-veritas-newsletter.md`](./domains-veritas-newsletter.md)
  - [`domains-veritas-nlp.md`](./domains-veritas-nlp.md)
  - [`domains-veritas-research.md`](./domains-veritas-research.md)
- Customer-facing Veritas hub: [`veritas.md`](./veritas.md)
- Feature spec: [`V1/features.md`](../../../V1/features.md#veritas)

## Open questions / known gaps

- [ ] URL submission is simulated — `handleAnalyze` picks a random
      `SAMPLE_ARTICLES` entry, not a real fetch. Verify whether V1 wires this to
      a real article-analysis endpoint
- [ ] No telemetry calls (no `trackVeritas*` imports in this file); verify the
      design intent
