---
path: /arete/offering
surface: customer
domain: arete
auth: signed-in
source: apps/oshun/web/src/app/arete/offering/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; save/send recoverable 5xx
  failure and retry re-walked against live BFF 2026-06-27; score selection,
  material reorder, recipient/occasion selection, dynamic masthead summary, and
  scene preview copy re-walked in browser 2026-06-27; cue-privacy promise reveal
  and BFF-backed C2PA receipt envelope re-walked 2026-06-27; offering-surface
  coach framework preview to real BFF plus forced 503/retry re-walked 2026-06-27'
---

# Arete · the Living Offering

## Purpose

"A 4–8 minute composition for one person — intention, scene, score, sharing,
cue-privacy promise." A composition surface for assembling photos, voice notes,
and a quiet score into a short personal gift, with an explicit cue-privacy
promise about working-copy destruction. Renders
`<AreteOffering data={await getAreteOffering()} />`.

## Entry points

- **Direct URL / bookmark** — yes (signed-in)
- **From `/atelier/*`** — atelier composition tools likely reference offering
  (no explicit link verified in source)
- **From notification** — "your offering is ready to send" deep link
- **From a chest / library surface** — described in the privacy section as "all
  from your chest" (no explicit link in this page)

## Layout regions

`page.tsx` is a thin server component: `await getAreteOffering()` →
`<AreteOffering>`. Component lives at
`apps/oshun/web/src/components/lilith/arete.tsx` line 1794 — an `LWebShell` with
`LCustomerNav active="today"`.

- **Header**: `LCustomerNav active="today"`
- **Masthead** (`AreteOfferingWorkspace`): left "Arete · the living offering",
  mid `[data-arete-offering-summary]` seeded as "renata's offering · for J · 14
  may" and updated from the live recipient/occasion inputs, right "6 min ·
  private until you choose", big "An intention, _composed_ into a small scene.",
  lede about the 4–8 minute composition concept
- **Main, left (1.4fr)**:
  - I · The intention — accent-bordered paper card with a real textarea
    (`[data-arete-offering-intention-input]`) seeded with the current intention
    prose
  - I.B · Coach lens — compact framework selector
    (`general / cbt / act / stoic / reflective`) with
    `[data-arete-offering-coach-framework-option]`, plus "Preview coach score"
    calling the real coach BFF for the current intention
  - II · The scene · preview — `aspectRatio: '16/9'` simulated video card with
    gradient backdrop, `[data-arete-offering-scene-label]` derived from
    `sceneTimeline[0]`, `[data-arete-offering-scene-preview-line]` derived from
    the current intention prose, and hardcoded play button glyph "▶"
  - Scene timeline strip — `data.sceneTimeline` rows (numbered scenes, with
    labels), waveform SVG, scrub head indicator at 12%
  - III · Score — 3-column grid of `data.scoreOptions` button cards (title +
    sub + selected chip); client state is seeded from the fixture's selected
    option and click updates the active score
- **Aside, right (1fr)**:
  - IV · Materials — 3x3 grid of color-swatch buttons driven by
    `data.toneSwatch` (9 cells, cell 5 initially selected with paper-color inset
    border); click selects a swatch, drag/drop or Move earlier / Move later
    reorders the selected swatch locally; caption "9 of 47 photos and 3 of 12
    voice notes selected. Drag or move to reorder."
  - V · Sharing — editable recipient and occasion inputs seeded from
    `data.recipient` / `data.occasionDate`, "watch once · on a single device",
    then `data.sharingRules` rows (k/v pairs)
  - Cue-privacy promise card — accent-bordered paper card; "Read the full
    promise" reveals the full cue-privacy detail, and "See the C2PA receipt"
    POSTs the current recipient/occasion/intention/score to the BFF-backed
    receipt envelope without echoing raw intention prose
  - Footer action row: "Save draft" (ghost lg) + "Send to {recipient} ·
    {occasion}" (primary lg), fed by the current sharing controls and guarded by
    the Lilith safety-assessment preflight

## States

- [ ] **Loading** — server-awaited; no `loading.tsx` at this route
- [ ] **Populated (default fixture)** — scene timeline, 3 score options, 9
      swatch cells, sharing rules, action footer all render
- [ ] **Scene timeline empty** — empty `sceneTimeline` would render no scene
      blocks (only waveform); no empty-state coded
- [x] **Score selected** — score buttons expose `aria-pressed` and
      `[data-arete-offering-score-selected]`; initial state is seeded from the
      selected fixture option with a first-option fallback, and click updates
      the active chip locally
- [x] **Material reorder** — swatch tiles expose selected state and color attrs;
      Move earlier / Move later and drag/drop reorder the selected material
      locally
- [x] **Editable intention** — the default prose renders in a textarea; typed
      prose updates the scene preview line, is sent with Save draft / Send, and
      is returned as a private member-scoped `intentionPreview`
- [x] **Coach score preview** — the framework selector starts on `reflective`,
      can switch to `stoic / cbt / act / general`, and "Preview coach score"
      POSTs `{ framework, userMessage }` to `/v1/arete/coach/responses`; a 2xx
      response renders `[data-arete-offering-coach-response]` plus suggestions,
      while a 5xx renders `[data-arete-offering-coach-error]` with retry enabled
- [x] **Different recipient / occasion** — Section V exposes real recipient and
      occasion inputs; changes update `[data-arete-offering-summary]`,
      `[data-arete-offering-actions]`, the send button label, the
      `/v1/arete/offerings/keep` POST body, `/sent` readback, and the
      `/arete/offerings` gallery row
- [x] **Cue-privacy receipt** — "See the C2PA receipt" POSTs to
      `/v1/arete/offerings/receipt`, renders
      `[data-arete-offering-receipt-state="receipt-issued"]`, C2PA manifest id,
      cue-privacy hash, and private-intent hash, and does not render raw
      intention prose in the receipt panel
- [ ] **Route render error (recoverable)** — server-awaited route failures rely
      on the parent error boundary
- [x] **Save/send write error (recoverable)** — `AreteOfferingActions` catches a
      non-2xx `/v1/arete/offerings/keep` response, renders
      `[data-arete-offering-error][role="alert"]`, leaves the action controls
      enabled, and can retry the same browser-entered intention into a live BFF
      `draft-kept` or `delivered` response
- [x] **Crisis pre-screen blocked** — before keep/send, `AreteOfferingActions`
      calls `/v1/lilith/adapter/safety-assessment?role=safety`; a blocking
      response replaces the action buttons with
      `[data-arete-offering-safety-interrupt]`, 988 crisis-resource copy, and a
      `/profile/safety` link, with no `/v1/arete/offerings/keep` write
- [x] **Safety preflight error (fail closed)** — a non-2xx safety-assessment
      response renders `[data-arete-offering-error][role="alert"]`, leaves the
      action controls enabled, and still makes no `/v1/arete/offerings/keep`
      write
- [ ] **Offline** — no explicit handling
- [ ] **Gated** — N/A; metadata does not mark robots no-index
- [ ] **Standalone PWA** — renders cleanly within `LWebShell`

## Interactions

### Scene preview card

- [ ] **Play glyph "▶"** — static centered circular glyph
  - Function: no `onClick` — purely visual placeholder
  - Screen reader: no `aria-label` on the glyph
- [x] **Preview label / line** — `[data-arete-offering-scene-label]` is derived
      from the first scene timeline label and
      `[data-arete-offering-scene-preview-line]` updates from the current
      intention textarea

### Scene timeline

- [ ] **Scene block** (one per `data.sceneTimeline`)
  - Function: numbered+labelled timeline block; static
  - Function: scrub indicator at 12% is hardcoded, not interactive

### Score option button (one per `data.scoreOptions`)

- [x] **Score button** — title + sub + visible "● selected" chip on the active
      option
  - Function: click sets the active score; root grid exposes
    `[data-arete-offering-score-selected]`
  - Screen reader: `aria-pressed` reflects the active score

### Coach preview panel

- [x] **Framework selector** — five segmented buttons expose
      `[data-arete-offering-coach-framework-option]` and `aria-pressed`
  - Function: click updates
    `[data-arete-offering-coach-panel][data-arete-offering-coach-framework]` and
    clears stale preview/error state
- [x] **Preview coach score** — button calls the real BFF
      `/v1/arete/coach/responses` with the selected framework and current
      intention textarea value
  - Function: successful responses render `[data-arete-offering-coach-response]`
    with `[data-arete-offering-coach-response-framework]` and one chip per
    `[data-arete-offering-coach-suggestion]`
  - Recoverable error: non-2xx clears stale coach copy, renders
    `[data-arete-offering-coach-error]`, and keeps the preview button enabled
    for retry

### Materials swatches (3x3)

- [x] **Swatch tile** — colored button with index "01"…"09" label; the selected
      tile has a paper-color inset border and `aria-pressed`
  - Function: click selects a tile; drag/drop reorders a dragged tile; Move
    earlier / Move later moves the currently selected tile and updates
    `[data-arete-offering-material-selected]`
  - Current scope: local composition state only; no persisted material order

### Sharing controls

- [x] **Recipient input** — controlled text field seeded from `data.recipient`
  - Function: updates `[data-arete-offering-summary]`,
    `[data-arete-offering-recipient-value]`, and the BFF-backed action island's
    `data-arete-offering-action-recipient`
- [x] **Occasion input** — controlled text field seeded from `data.occasionDate`
  - Function: updates `[data-arete-offering-summary]`,
    `[data-arete-offering-occasion-value]`, and the BFF-backed action island's
    `data-arete-offering-action-occasion`
  - Current scope: natural-language occasion text, not an ISO date picker

### Privacy / footer actions

- [x] **"Read the full promise"** (ghost button)
  - Function: toggles `[data-arete-offering-promise-detail]` with the explicit
    private-intention, no-training, source-cue redaction promise
- [x] **"See the C2PA receipt"** (ghost button)
  - Function: POSTs `/v1/arete/offerings/receipt` with the current
    `{ recipient, occasion, intention, score }`; UI renders receipt id, C2PA
    manifest id, cue-privacy hash, private-intent hash, and selected score
  - Privacy: BFF hashes the private intention and the UI does not render the raw
    intention prose in the receipt panel
- [x] **"Save draft"** (button inside `AreteOfferingActions`)
  - Function: POSTs `/v1/lilith/adapter/safety-assessment?role=safety` with the
    current intention first; clean assessments then POST
    `/v1/arete/offerings/keep` with
    `{ action: 'draft', recipient, occasion, intention }`; UI renders
    `data-arete-offering-state="draft-kept"` and the private intention preview
  - Crisis path: renders `[data-arete-offering-safety-interrupt]` plus support
    resources and no keep write
  - Recoverable error: a non-2xx keep response renders
    `[data-arete-offering-error]` and leaves the button enabled for a real BFF
    retry
- [x] **"Send to {recipient} · {occasion}"** (button inside
      `AreteOfferingActions`)
  - Function: POSTs `/v1/lilith/adapter/safety-assessment?role=safety` with the
    current intention first; clean assessments then POST
    `/v1/arete/offerings/keep` with
    `{ action: 'send', recipient, occasion, intention }`; UI renders
    `data-arete-offering-state="delivered"` and the private intention preview
  - Crisis path: renders `[data-arete-offering-safety-interrupt]` plus support
    resources and no keep write
  - Recoverable error: a non-2xx keep response renders
    `[data-arete-offering-error]` and leaves the button enabled for a real BFF
    retry

## Data & contracts

- **Reads**: `getAreteOffering()` from `@/lib/lilith-data/arete-depth` — returns
  `AreteOfferingData` ({ sceneTimeline, scoreOptions, toneSwatch, sharingRules
  }). BFF mapping comment: `getAreteOffering ← /arete/offerings/:id`.
- **Writes**: `AreteOfferingWorkspace` calls `POST /v1/arete/coach/responses`
  from the coach-preview panel with `{ framework, userMessage }`, and renders
  only the returned framework-scoped copy/suggestions or an honest recoverable
  error. `AreteOfferingActions` calls
  `POST /v1/lilith/adapter/safety-assessment?role=safety` before keep/send; a
  blocking response halts in the action-region safety interruption. Clean
  assessments continue to `POST /v1/arete/offerings/keep`; the BFF stores the
  keep/send decision and a trimmed `intentionPreview` under the verified member
  id. Member-scoped read back is available through
  `GET /v1/arete/offerings/sent`. `AreteOfferingWorkspace` also calls
  `POST /v1/arete/offerings/receipt` for the cue-privacy/C2PA receipt envelope;
  it is member-authenticated and returns hashes/ids rather than raw intent.
- **Realtime**: none
- **Caching**: server fetch, default Next.js cache
- **Auth/role check**: app-level middleware

## Cross-references

- Domain hub: [`arete.md`](./arete.md)
- Atelier (composition tools): `apps/oshun/web/src/app/atelier/*` — see
  `customer/11-atelier/`
- Related: C2PA provenance receipts; this surface now has a member-scoped
  receipt envelope, while Arete-specific binary media signing still belongs to
  the render pipeline
- Component source: `apps/oshun/web/src/components/lilith/arete.tsx` (line 1794,
  `AreteOffering`)
- Data: `apps/oshun/web/src/lib/lilith-data/arete-depth.ts`
  (`getAreteOfferingFixture`)
- Feature spec: [`V1/features.md`](../../../V1/features.md#arete)

## Open questions / known gaps

- [x] Save draft and Send now have BFF-backed handlers through
      `AreteOfferingActions`
- [x] Read the full promise and See the C2PA receipt now have handlers
- [x] Section V recipient and occasion are editable, drive the send label, and
      persist through the live BFF keep/send path
- [x] Masthead summary and scene preview copy are now dynamic:
      `[data-arete-offering-summary]` follows edited recipient/occasion,
      `[data-arete-offering-scene-label]` is derived from `sceneTimeline[0]`,
      and `[data-arete-offering-scene-preview-line]` follows the intention
      textarea
- [x] Score selection is no longer hardcoded by index; the client control is
      seeded from the selected `scoreOptions` field and updates on click
- [x] Material swatches now have tile selection plus drag/drop and Move earlier
      / Move later reorder controls
- [x] Keep/send now pre-screen the intention through Lilith and block a
      crisis-like intention before the Arete offering write
- [x] `/arete/offering` now has a direct coach framework preview into the real
      `/v1/arete/coach/responses` BFF route; the user no longer needs to leave
      the offering surface to score the current intention through Stoic, CBT,
      ACT, Reflective, or General lenses
- [ ] How does this surface tie into Atelier / Iris (rendering pipeline) and Aja
      (cue-privacy promise / C2PA)? `/v1/arete/offerings/receipt` now backs the
      receipt envelope, but `AreteOffering` still does not invoke the binary
      render/signing pipeline
- [ ] The cue-privacy promise text claims "the working copy is destroyed"; the
      receipt now records this policy, but the actual renderer still needs an
      Arete-specific destruction/signing audit
