---
path: /veritas/retraction
surface: customer
domain: veritas
auth: signed-in
source: apps/oshun/web/src/app/veritas/retraction/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; per-user BFF 404/no-leak
  verified 2026-06-27 in veritas-retraction-cascade.spec.ts; per-user
  acknowledgement POST and same-reader reload hidden state verified 2026-06-30
  in veritas-retraction-cascade.spec.ts'
---

# Veritas · soft correction

## Purpose

A private letter shown to a single reader on their next visit when a claim they
previously read has been retracted or corrected. The promise is "no notification
at midnight" — Veritas surfaces the change in-app, never names the reader
outside this notice, and never pushes. Thin server component awaits
`getRetraction()` and renders `VeritasRetraction` only when the authenticated
BFF has a pending notice for that reader.

## Entry points

- **On next visit after a relevant retraction** — automatic surface; the
  triggering logic lives elsewhere (verify which BFF/Sophia signal flags a read
  claim as retracted)
- **Linked from `/veritas/story`** — the timeline says "Veritas notifies you
  personally for the claims you have read on the days you read them. The notice
  is private to you. See the retraction board ›" (verify the link target)
- **Direct URL / bookmark** — yes (auth required), but
  `robots: { index: false, follow: false }` is set so the page is not indexed
- **Email link** — V1/features.md says retractions are surfaced privately on
  next visit; not via push (verify whether email is even part of the channel)

## Layout regions

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

- **Header**: Lilith customer nav (Library tab active)
- **Masthead (`VrMast`)**:
  `Veritas · the soft correction | for <recipientName> · <date read> · private | a thing you read became false`,
  display title "A small _correction_, for you.", italic lede
- **Two-column grid (1fr / 1fr)**:
  - **Left — Letter card** (accent border, paper background):
    - Floating `private` code label in the top-right corner
    - `<data.recipientName> —` italic greeting
    - Multi-paragraph letter (19px serif, lh 1.7) addressing the reader by name,
      naming the date they read, showing the old (struck-through) value and the
      new (italic accent) value, naming the reason, and explaining that the
      corrected piece will surface the correction at the top on re-read
    - Signed `— Lilith, on behalf of Veritas`
  - **Right column — Detail + Promise + Actions**:
    - `VrHead`: `I · The change, in detail · Before · after.`
    - 2-column grid: "read on <date>" (struck-through, medium-confidence colour)
      | "now reads" (accent border, paper background)
    - `VrHead`: `II · The promise · What this notice does — and doesn't.`
    - Italic list of `data.promise[]` lines; the rendering strips any
      `*emphasis*` markers (regex `*([^*]+)*` → group)
    - **Action row** (`VeritasRetractionActions`, flex, gap 10):
      - **Read once · don't keep** active button posts to
        `/api/veritas/retractions/personal-letter/acknowledgement`, which
        forwards to the authenticated BFF
        `/v1/veritas/retractions/personal-letter/acknowledgement`; success
        renders an acknowledgement status and the same reader sees 404 on reload
      - **Re-open the piece** (`href="/veritas/story?origin=retraction"`, flex
        1), verified 2026-06-24

## States

- [ ] **Loading** — `getRetraction()` awaited server-side; no client skeleton
- [x] **Populated** — authenticated BFF personal-letter payload renders the
      letter, the before/after card, the promise list, and the action row
- [ ] **First view** — the reader has not yet seen this letter (no `read once`
      write event from a prior visit)
- [x] **Returning view** — after "Read once · don't keep" posts the
      acknowledgement, the BFF personal-letter read returns
      `404 veritas_retraction_notice_not_found` for the same reader and the
      private letter does not re-appear on reload
- [ ] **Error (recoverable)** — `getRetraction()` rejects → nearest `error.tsx`
- [ ] **Offline** — cached HTML can render the letter; "Read once · don't keep"
      fails closed with a network-unavailable acknowledgement status when the
      POST cannot reach the same-origin API route, while "Re-open the piece" is
      a normal route link whose offline behaviour depends on whether
      `/veritas/story` is already cached
- [ ] **Standalone PWA** — the 1fr/1fr grid collapses on narrow screens via grid
      behaviour; verify
- [x] **Empty** — when the authenticated BFF returns
      `404 veritas_retraction_notice_not_found`, Next renders the route as a 404
      instead of falling back to another reader's letter
- [x] **Gated** — `robots: { index: false, follow: false }` prevents search
      indexing, and the BFF gates the route per-user; a different user does not
      see another reader's letter

## Interactions

### Letter card (left)

- [ ] **Letter body** — non-interactive serif paragraphs
- [ ] **Old value strikethrough** — visual; verify SR text announces "was X, now
      Y" (currently relies on `text-decoration: line-through` only)
- [ ] **Floating `private` chip** — non-interactive

### Before/after card (right)

- [ ] **"read on <date>" block** — struck-through copy from `data.before`;
      non-interactive
- [ ] **"now reads" block** — accent border, copy from `data.after`;
      non-interactive

### Promise list

- [ ] **Italic line** — non-interactive; the render strips `*emphasis*` markers
      via `p.replace(/\*([^*]+)\*/g, (_m, w) => w)`. Verify why the markers are
      stripped (markdown emphasis from fixture? if so, consider `<em>` rendering
      instead)

### Action row

- [x] **Read once · don't keep** (`VeritasRetractionActions` button)
  - Function: POSTs to the same-origin web API proxy
    `/api/veritas/retractions/personal-letter/acknowledgement`, which forwards
    the authenticated customer bearer to the BFF acknowledgement route. The BFF
    stores a per-user acknowledgement, returns `state: "acknowledged"`, and the
    next personal-letter read for that reader returns 404.
  - UI states: `data-veritas-retraction-acknowledgement-state` exposes `idle` /
    `saving` / `acknowledged` / `error`; success renders
    `data-veritas-retraction-acknowledgement-status="acknowledged"`.
  - Telemetry: no analytics event yet; the BFF acknowledgement response is the
    durable post-condition.
- [x] **Re-open the piece** (`LBtn kind="primary" size="lg"`)
  - Function: links to `/veritas/story?origin=retraction`, the corrected story
    timeline with retraction notices. Verified 2026-06-24 in
    `veritas-retraction-cascade.spec.ts`.
  - Telemetry: verify event fires (`veritas_retraction_reopened`?)

## Data & contracts

- **Reads**: `getRetraction()` from `@/lib/lilith-data/veritas-depth` calls the
  authenticated BFF route `/v1/veritas/retractions/personal-letter` and returns
  `RetractionData` ({ recipientName, letter.{dateRead, oldValue, newValue,
  reason}, before, after, promise[] }) or `null` for no pending notice
- **Writes**: "Read once · don't keep" writes a per-user acknowledgement through
  the BFF route `/v1/veritas/retractions/personal-letter/acknowledgement`; the
  write is idempotent and hides the same reader's pending personal-letter read
  on reload. "Re-open the piece" is navigation only and does not write.
- **Realtime**: none
- **Caching**: server fetch per user (must not be CDN-cached since the letter is
  private and personalised)
- **Auth/role check**: shell middleware enforces signed-in; the BFF route
  requires Veritas scope and scopes `getRetraction()` to the requesting reader
- **Page metadata**: `title: "Veritas · soft correction"`, description "When a
  claim you read becomes false: a private letter, on your next visit. No
  notification at midnight.", `alternates.canonical: /veritas/retraction`,
  `robots: { index: false, follow: false }`

## Cross-references

- Component: `apps/oshun/web/src/components/lilith/veritas.tsx` —
  `VeritasRetraction` (lines 1771–1934)
- Data: `apps/oshun/web/src/lib/lilith-data/veritas-depth.ts` (`getRetraction`)
- Story timeline reference: `VeritasTimeline` (lines 1053–1066) names the
  retraction notification model; this view is what it links to
- Sibling routes:
  - [`veritas.md`](./veritas.md), [`veritas-story.md`](./veritas-story.md),
    [`veritas-provenance.md`](./veritas-provenance.md)
- Feature spec: [`V1/features.md`](../../../V1/features.md#veritas)
- Cross-domain: privacy commitments / "private to you" are part of the
  Lilith/Psyche tone policy; verify with
  [`V1/features.md#lilith-persona-policy`](../../../V1/features.md#lilith-persona-policy)

## Open questions / known gaps

- [x] Durable acknowledgement for "Read once · don't keep" now writes through
      the BFF and hides the same reader's private letter on reload; verified in
      `veritas-retraction-cascade.spec.ts`
- [x] Confirm the route is per-user gated at the BFF (`getRetraction()` returns
      a 404 for users without a pending retraction, not someone else's letter)
- [x] Verify whether the "Read once · don't keep" semantics means a hard delete
      vs a soft acknowledge — current implementation is a soft per-user
      acknowledgement: the pending private letter no longer re-appears, while
      the underlying cascade record remains available to audit/history systems
- [ ] No keyboard shortcut documented for dismiss (Esc); verify the shell
      provides one
- [ ] The promise list strips `*emphasis*` markers — consider whether surfacing
      emphasis as `<em>` would convey the intended typographic hierarchy
- [ ] `LCustomerNav active="library"` — consistent with the `/veritas` hub and
      the other depth views, which all use Library (no Explore inconsistency)
