Context. surface customer · domain veritas · route /domains/veritas/articles · auth signed-in · source apps/oshun/web/src/app/domains/veritas/articles/page.tsx
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.
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/veritasshell surface — More verification tools exposes an "Article Analyzer" link (data-veritas-secondary-tool-link="articles") to/domains/veritas/articles/veritasbroadsheet 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 Analyzertitle, tablist forinput | analysis | compare(only visible whenview !== '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..."), primaryAnalyzebutton (disabled when input empty or analyzing; "Analyzing..." pulse text on submit, ~1.8s simulated delay) - Sample Articles section: category chip filter (
all+ unique categories fromSAMPLE_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
- URL input card: 28px headline "Article Analyzer", lede about what it
analyzes, link icon + text input (
- Analysis view (
view === 'analysis', requiresselectedArticle):- Multi-section analysis grouped by collapsible panels:
readability,sentiment,entities,claims,source,bias(all expanded by default perexpandedSectionsinitial state)
- Multi-section analysis grouped by collapsible panels:
- Compare view (
view === 'compare', requirescompareArticle): two article columns laid side-by-side on wide viewports and stacked on phones
- Input view (
- Footer: none beyond the overlay body padding
States#
- Loading (initial) — the overlay renders synchronously; no loading skeleton for the input view
- Analyzing —
Analyzebutton shows "Analyzing..." with theoshun-aa-pulsekeyframe; URL input remains visible - Empty input —
Analyzebutton isdisabledand rendered at 50% opacity; cursornot-allowed - Populated (sample selected) — clicking a sample card transitions to
analysisview viahandleSelectSample - Populated (URL submitted) — after the 1.8s timeout, a random
SAMPLE_ARTICLESentry is picked, itsurloverridden with the submitted URL, and the view switches toanalysis - Compare view with no second article —
comparetab is disabled whencompareArticleis unset - 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 - Offline — no network calls in code (uses
SAMPLE_ARTICLESsimulation); offline is effectively a no-op - Standalone PWA — full-screen overlay renders cleanly; verify safe area at the top
- Reduced motion — pulse and scan-line keyframes are injected via
<style>; verifyprefers-reduced-motionhonoured
Interactions#
Header#
- 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
- Function: invokes
Header tablist (when not in input view)#
- input (button)
- analysis (button)
- Disabled when
selectedArticle === null(opacity 0.4)
- Disabled when
- compare (button)
- Disabled when
compareArticle === null(opacity 0.4)
- Disabled when
Input view — URL form#
- URL input (
<input type="text">)- Function: bound to
urlInputstate - Keyboard: Enter triggers
handleAnalyze - Placeholder:
Paste article URL... inputRefautofocuses when the input view is active
- Function: bound to
- Analyze (primary button)
- Function:
handleAnalyze→ 1.8ssetTimeout→ switch to analysis view with a randomSAMPLE_ARTICLESentry whoseurlis overridden - Disabled when input empty or already analyzing
- Function:
Input view — Sample articles#
- Category chip (per category, including
all)- Function:
setCategoryFilter(cat); capitalises label - Active style: accent border + 15% accent background
- Function:
- 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
- Function:
Analysis view — Sections#
- Section header (expand/collapse, per
expandedSectionsset)- Function:
toggleSection(id)adds/removes fromexpandedSections - All sections start expanded
- Sections: readability, sentiment, entities, claims, source, bias
- Function:
Analysis view — section content (sampled by code)#
- Readability gauge (
ReadabilityGauge) — colour-coded score (>=70 ok, >=50 warn, >=30 alert, else alert) — visual chart with score and label - Sentiment label badge (
bdg(...)) — pill driven bySENTIMENT_COLORS[label] - Named entities — chips with
ENTITY_CONFIGicon + colour byEntityCategory(person, organization, location, date, money, event, product, legislation, statistic) - Claims list — each claim shows
CLAIM_STATUS_CONFIGbadge (verified / disputed / unverified / false / misleading) and an evidence list - Bias position bar —
BIAS_POSITIONmarkers at 0/25/50/75/100 for left → right; current article's bias rendered as accent point
Analysis view → Compare#
- "Compare against…" affordance (button) —
startCompare(article)setscompareArticleand switches view tocompare. Verify the visible label and which card surfaces it
Compare view#
- Two article columns side by side; fields diffed include header/source, readability, sentiment, bias, claim summary, and source reliability
- Back to analysis (button) —
setView('analysis')
Data & contracts#
- Reads:
SAMPLE_ARTICLESfrom@/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,resolveWebVeritasTrustRulefrom./veritasTrustLabels - Random source:
webRandomUnitIntervalfrom@/lib/random(used to pick a fake "result" article) - Page metadata: not set (no
Metadataexport 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/veritasvia the More verification tools link, closes back through browser history, then opens the analyzer from the/veritasbroadsheet 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.
- Entry paths: opens the analyzer from
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,SourceListItemfrom@/design-system/components/*,tokensfrom@/design-system/tokens,LV as L,SERIF_Lfrom@/design-system/lilith - Sibling tools (similar overlay shape):
- Customer-facing Veritas hub:
veritas.md - Feature spec:
V1/features.md
Open questions / known gaps#
- URL submission is simulated —
handleAnalyzepicks a randomSAMPLE_ARTICLESentry, 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