# Journey: Library save, collect, share

The cross-view flow from a domain Save action through signed-in BFF sync,
`/library` hydration, browser-local collection editing, and server-enforced
share-policy resolution. Saved **items** and collection **sharing policy** have
real BFF paths; collection labels, descriptions, membership, and archive state
remain in `localStorage['oshun.library.collections.v1']`. The recipient route
currently proves the collection ID and granted permission tier—it does not yet
render the sender's collection contents. Keeping those three planes separate is
the central invariant of this journey.

## Personas

- **Curated reader** — saves passages, sources, and assistant excerpts; lives in
  `/library` and uses smart filters to find them later
- **Collection curator** — builds named collections per project (e.g. "ruling
  part" notebook companion, "tonight's sky" reading list)
- **Link-share collaborator** — wants to share a collection with a colleague via
  a link-with-key URL without exposing their full library
- **AAA-creator** — public-profile surface user; shares a curated collection
  with provenance and grounding indicators preserved per
  [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)

## Pre-conditions

- Signed-in session (per
  [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md))
- `useOshunWebLibraryStore()` initialized from showcase defaults and the local
  cache. For a signed-in member, `hydrate()` then reconciles real items from
  `/v1/library/saved-items`; saves/unsaves are optimistic and fail-soft while
  their BFF writes run (per
  [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
  Data & contracts)
- `localStorage['oshun.library.collections.v1']` writable (or already populated
  with prior user-created collections). Collection composition is not
  cross-device today.
- At least one in-domain Save affordance reachable (per the cross-cutting
  finding in [`README.md`](./README.md), Save buttons now wire to
  `useOshunWebLibraryStore` in Veritas / Nisaba / Search / Explore / Tara)
- Service worker active so cached `/library` route reads work offline (per
  [`shell/03-pwa-behavior.md`](../shell/03-pwa-behavior.md))

## Steps

### 1. Save an artifact from a domain surface

Pick whichever surface the user is starting from; all converge in
`useOshunWebLibraryStore`.

**A. From Veritas (passage)**

- [x] On `/veritas`, click “Save to notebook.” The direct producer writes a
      Veritas saved item through the web store and real BFF, then hydrates it in
      a clean second browser context.

**B. From Nisaba (passage)**

- [x] On `/nisaba`, the passage actions row exposes `NisabaPassageActions` “Save
      passage" — saves a `nisaba` passage library item per
      [`README.md`](./README.md) finding 1

**C. From Search**

- [x] On `/search`, each result row exposes a library toggle when
      `canSaveSearchResultToLibrary(result)` per
      [`customer/02-home-discovery/search.md`](../customer/02-home-discovery/search.md)
      Library save toggle
- [x] Click →
      `toggleOshunWebLibraryItem(buildLibraryItemFromSearchResult(...))`;
      `trackLibraryItemSaved` fires

**D. From Explore**

- [x] On `/explore`, live search-backed cards expose the same toggle when
      library-saveable per
      [`customer/02-home-discovery/explore.md`](../customer/02-home-discovery/explore.md)
      Library save toggle

**E. From Nyx event detail**

- [ ] On `/domains/nyx/events/<eventId>`, click the "Save event" toggle
      (`data-testid="nyx-event-save-toggle"`) per
      [`customer/06-nyx/domains-nyx-events-eventId.md`](../customer/06-nyx/domains-nyx-events-eventId.md)
      Action row 2
- [ ] Note that Nyx save state is owned by `useNyxEventActionState`
      (localStorage `oshun:nyx:event-actions:v1`), not `useOshunWebLibraryStore`
      — verify whether the Nyx save surfaces in `/library` or only in the Nyx
      event lane (this is one of the polished-vs-internal dual-fixture patterns
      flagged in [`README.md`](./README.md) finding 5)

### 2. Open `/library` and confirm the save

- [x] Navigate to `/library` via shell nav (or via
      `WEB_SHELL_ROUTE_PATHS.library`)
- [ ] `ShellLayout active="library"` mounts; breadcrumb `Home → Library`
- [x] `<LibraryDashboard />` mounts; pre-hydration `ShellRouteAnchor` ("Keep
      what matters, then return to it deliberately") flashes while
      `hasMounted === false` per
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      States — Loading
- [ ] Focal stage renders frame title "Saved continuity across the shell" and
      the four `ShellLandingMetricPill`s (saved count, distinct domains, study
      count, re-entry count)
- [x] The just-saved item appears after the signed-in server reconciliation in
      the filtered saved-items list with `LibraryItemSignals` badges
      (`ConfidenceBadge`, `LibraryGroundingBadge`, `ProvenanceBadge`)
- [ ] **Verify**:
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      Saved item row + States — Populated (filters: all/all)

### 3. Narrow the filters

- [ ] Click the Domain segment rail (`FilterRail<LibraryDomainFilter>`,
      aria-label "Library domain") — pick the domain matching the just-saved
      item (e.g., Nisaba)
- [ ] Frame title interpolates `LIBRARY_DOMAIN_LABELS[domain]` (e.g., "Nisaba
      returns")
- [ ] Click the Kind segment rail (`LIBRARY_KIND_FILTERS`) — pick the kind
      (e.g., `passage`)
- [ ] Frame title now interpolates `LIBRARY_KIND_LABELS[kind].toLowerCase()`
- [ ] The "Create collection from filters" chip becomes visible
      (`filteredItems.length > 0`)
- [ ] **Verify**:
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      Workspace toolbar

### 4. Create a collection from the current filters

- [ ] Click "Create collection from filters"
      (`handleCreateCollectionFromFilters`)
- [ ] A new collection clones the current `filteredItems` into a fresh
      collection with a derived label
- [ ] Router pushes to `/library/collection/<slug>` per
      `buildLibraryCollectionHref` per
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      Create collection from filters
- [ ] **Verify**:
      [`customer/02-home-discovery/library-collection-collectionId.md`](../customer/02-home-discovery/library-collection-collectionId.md)
      Layout regions + States — Collection found

### 5. Add multiple items to the collection

The collection deep-link surface uses the same `LibraryDashboard` with
`initialCollectionId` pre-set.

- [ ] Verify `selectedCollection.label` populates the collection editor label
      input
- [ ] Verify `selectedCollection.description` populates the description input
- [ ] Edit the label (e.g. "Ruling part — companion") — drives
      `collectionEditor`; `syncCollection` writes back to `collections` and to
      `localStorage['oshun.library.collections.v1']`
- [ ] Edit the description and the followUp copy
- [ ] Switch back to `/library` (clear the collection — calls
      `navigateToCollection(null)` →
      `router.push(WEB_SHELL_ROUTE_PATHS.library)`)
- [ ] Save additional items from any domain (repeat step 1 with different
      artifacts — passage from Nisaba, claim from Veritas, source from Sophia
      evidence sidebar)
- [ ] Return to `/library/collection/<id>` via the collection card click
      (`router.push(buildLibraryCollectionHref)`)
- [x] For each saved item, use the selected collection's membership toggle.
      `handleToggleCollectionItem` updates `collectionItemIds`; metrics
      recompute through `withCollectionMetrics` and the versioned local payload
      is rewritten.

### 6. Browse priority resume rail

- [ ] Back on `/library` (no collection), the priority resume rail shows the top
      3 items by `resumePriority` (`ContinuityCard`)
- [ ] Each card opens `buildLibraryResumeHref(item.domain, item.resumePath)`
      (domain root + `?path=…`)
- [ ] Click one to verify the resume path actually lands in the right domain at
      the right depth

### 7. Set sharing permissions

Sharing controls per
[`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing):
private, shared with named users, shared via link with permissions (view,
comment, copy), and public-profile surface where the customer has opted in.

- [x] Open the collection editor for the just-created collection
- [x] Locate the "Share" / "Visibility" controls — `CollectionShareControls`
      mounts on `/library/collection/research-to-practice` and
      `library-save-collection-share.spec.ts` asserts the share-control mount.
- [x] Pick visibility: **Private** (only the user; default)
- [x] Save — the collection retains `private` visibility via the real
      `PUT /v1/library/collections/:id/share` BFF route.
- [x] Switch to **Link with key** — a tokenized share URL is generated by the
      real `POST /v1/library/collections/:id/share-links` route. The current
      browser route is `/share/:token?target=collection&collectionId=...` rather
      than the older local `/library/collection/<id>?key=...` draft shape.
- [x] Copy/link result exposes the granted permission tier (view / comment /
      copy) per
      [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)
- [x] Switch to **Public** — the public-profile slug/tier controls persist to
      the real BFF policy and are read back by `collection-share.spec.ts`.
- [ ] Embeddable share cards: per
      [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)
      "Embeddable share cards for stories, claims, passages, sky events,
      meditations, and lesson modules with provenance, grounding, and synthetic
      indicators preserved" — verify the embed-card generator if surfaced

### 8. Recipient opens the shared link

- [x] In a second browser session (different account, or anonymous), open the
      shared URL
- [x] **Private / revoked / wrong token**: recipient-side BFF resolution returns
      `403` and does not grant the collection. `library-shared-resolution`
      covers wrong-token and revoked-token denial; the route/store tests cover
      private denial.
- [x] **Link with key**: the recipient page renders access granted/denied with
      the permission scope (`view` / `comment` / `copy`); the BFF denies a
      request above the token's tier. `library-shared-web-view` also proves the
      page does not mount the recipient's local `LibraryDashboard` collection
      even when localStorage has a same-id shadow collection.
- [x] **Public-profile policy write**: public-profile slug/tier is persisted and
      read back through the real BFF policy. The full sender public-profile page
      rendering for collections remains covered separately by public-profile
      surfaces, not this collection-share journey.
- [ ] **Embeddable/public share cards**: synthetic-content indicators and
      grounding-state preserved per
      [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)
- [ ] **Verify**:
      [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)
      sharing-permission isolation tests

### 9. Paywall behavior

Per the cross-cutting README — Aje owns Web3 / crypto payments per
[`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#2-adapters--libsoshundomain--and-substrate-adapters).
For any artifact behind a tier paywall:

- [ ] If a saved item is gated by entitlement (e.g., AAA-creator-only reading),
      the library card surfaces the gate inline; clicking "Resume" routes
      through the entitlement gate
- [ ] Per
      [`customer/13-system/aaa-upgrade.md`](../customer/13-system/aaa-upgrade.md)
      — middleware bounces unentitled users to `/aaa-upgrade` rather than
      showing the artifact
- [ ] Crypto-payment rail handled by `libs/oshun/aje/*` per
      [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#2-adapters--libsoshundomain--and-substrate-adapters);
      flow is out of scope for `/library` itself — the library only displays the
      gated badge

### 10. Library sync across devices

Saved items now implement the cross-device half of the feature contract;
collection composition does not.

- [x] On a second device (signed in as the same user), open `/library` without
      copying Library localStorage.
- [x] `webLibraryStore.hydrate()` reads `/v1/library/saved-items`; the item
      appears and resumes its canonical destination.
- [x] Guest saves are journaled under
      `oshun.web.library.pending-auth-migration.v1` and reconciled through
      `/v1/library/saved-items/sync` after authentication.
- [ ] User-created collection labels, descriptions, membership, and archive
      state follow the user across devices. They remain localStorage-only.
- [ ] **Verify** broader workspace handoff in
      [`multi-device-workspace-handoff.md`](./multi-device-workspace-handoff.md)
      `HomeMultiDeviceContinuityPanel` — Iris-driven preference sync

### 11. Unsave / remove items

- [ ] On any saved-item card, click the Save toggle to unsave
- [ ] `handleLibraryItemToggle(item)` flips the store entry; the row disappears
      from the filtered list
- [ ] `trackLibraryItemUnsaved` analytics fires per
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      Save toggle
- [ ] Inside a collection: per-view file flags "remove item from collection" —
      `setCollectionItemIds` mutates the collection's item set
- [ ] Delete a user-created collection — per-view file Open question: "Confirm
      whether user-created collections can be deleted (the source defines
      `LIBRARY_COLLECTIONS` as readonly; only stored extras appear removable)"

## Post-conditions

- A signed-in saved item has a server record and can hydrate in a clean browser;
  the local cache remains an optimistic/offline aid.
- A created or edited collection has a version-1 localStorage payload matching
  the in-memory label, description, follow-up, and member IDs.
- A generated share link resolves through the BFF at no more than its granted
  tier; wrong, revoked, private, and insufficient-permission requests fail
  closed.
- The recipient never substitutes a same-ID collection from its own local
  Library state.
- Save/unsave UI emits the verified `item_saved` / `item_unsaved` analytics
  events. Collection composition and share-policy writes do not imply those item
  events.
- The shared page proves access policy only; collection content delivery and
  embeddable provenance cards remain open.

## Failure modes

- [ ] **In-domain save writes to one store; `/library` reads from another** —
      Nyx event save uses `useNyxEventActionState` (separate localStorage key
      `oshun:nyx:event-actions:v1`); confirm whether Nyx saves surface in
      `/library` or only in the Nyx event lane (per [`README.md`](./README.md)
      finding 5)
- [x] **Anonymous → signed-in transition loses saves covered** — guest saves are
      journaled under `oshun.web.library.pending-auth-migration.v1`, then
      replayed through the real `/v1/library/saved-items/sync` route when an
      access token appears. `library-save-collection-share.spec.ts` signs in
      through `/welcome`, reads the migrated row back from the BFF, and opens
      `/library` in a clean second browser context to prove the row is
      server-backed rather than localStorage residue.
- [x] **Stored collections corrupted / missing fallback covered** —
      `readStoredLibraryCollections` returns `null`; default collections render
      and user-created corrupt payload rows are ignored per
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      States and the collection-route E2E addendum
- [x] **Collection deep link 404 fallback covered** — `page.tsx` does not 404;
      the dashboard preserves the unknown URL, clears collection management, and
      applies valid query-seeded filters per
      [`customer/02-home-discovery/library-collection-collectionId.md`](../customer/02-home-discovery/library-collection-collectionId.md)
      States
- [x] **LocalStorage version migration covered** —
      `library-collection-management.spec.ts` now seeds a legacy `version: 0`
      `oshun.library.collections.v1` payload, opens a direct
      `/library/collection/<id>` link against the real authenticated shell +
      BFF-seeded saved items, verifies duplicate and stale item IDs are
      sanitized, and polls localStorage until the payload is re-persisted as
      current `version: 1`.
- [x] **Sharing link shows recipient's own collection** — fixed by the
      BFF-backed `/share/:token` route. `library-shared-web-view` opens a
      generated token URL in a different signed-in browser session with a
      same-id local shadow collection and proves the shared page resolves the
      BFF grant instead of rendering the recipient's local collection editor.
- [ ] **URL `?domain=` / `?kind=` not stable** — per-view file Open question:
      "Confirm whether `?domain=` and `?kind=` are stable in the URL after the
      user changes filters from inside the dashboard (filters mutate state but
      this `page.tsx` does not write them back to the URL)"
- [ ] **Resume href navigates to a domain root that requires network** — saved
      items are local store, but link resumes navigate to domain surfaces that
      may require network per
      [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
      Open questions; offline branch is not wired in the library view
- [ ] **Paywall bypass** — gated artifacts must route through the entitlement
      gate; saving the item to the library should not grant access. Per
      [`customer/13-system/aaa-upgrade.md`](../customer/13-system/aaa-upgrade.md)
      middleware enforces upstream
- [ ] **Share with provenance stripped** — embeddable share cards must preserve
      grounding, synthetic, and provenance indicators per
      [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing);
      verify the share-card renderer doesn't drop these on small breakpoints

## E2E coverage

- Backed by
  [`apps/oshun/web/e2e/library-collection-management.spec.ts`](../../apps/oshun/web/e2e/library-collection-management.spec.ts)
  and
  [`apps/oshun/web/e2e/library-save-analytics.spec.ts`](../../apps/oshun/web/e2e/library-save-analytics.spec.ts)
  — the collection spec drives the collection deep-link route, creates a
  collection from a domain filter, edits its label, adds and removes an item via
  toggle, archives it, and covers unknown collection fallback, query-seeded
  filters, corrupt stored collections, stored custom collection hydration, and
  legacy collection localStorage migration (`version: 0` → current `version: 1`
  with duplicate/stale item IDs sanitized); the analytics spec asserts
  `item_saved`/`item_unsaved` telemetry events from Explore, Search, Library,
  and the assistant panel.
- Backed by
  [`apps/oshun/web/e2e/library-entitlement-and-save.spec.ts`](../../apps/oshun/web/e2e/library-entitlement-and-save.spec.ts)
  — real HTTP integration against the live BFF (step 9 + the server side of
  steps 1–2). **(Paywall)** the generation-surface entitlement gate
  `GET /v1/entitlements/generation/surface/:surface`: the default
  (`contemplative`) tier is denied a gated AAA surface (`graph-editor` → `deny`
  / `404-hard-block`) but allowed a curated one, a higher `x-oshun-creator-tier`
  (`aaa-creator`) unlocks it while `curated-creator` stays denied per-surface,
  unknown surface → 400 `unknown_surface`, no bearer → 401. **(Save
  round-trip)** the server-authoritative `POST`/`DELETE /v1/favorites/:id`
  toggle (`favorited:true` ↔ `favorited:false`) and the `GET /v1/library`
  aggregation envelope asserting the tara-branch mapping (`itemType` `favorite`,
  key `tara:<id>`), each branching on the fail-closed Tara upstream (201/200
  when up, 502 `upstream_error` when down) plus 403 `domain_scope_missing` / 401
  gates.
- Backed by
  [`apps/oshun/web/e2e/collection-share.spec.ts`](../../apps/oshun/web/e2e/collection-share.spec.ts)
  _(new: A-to-Z sharing)_ — **steps 7–8** wired end-to-end against the **real
  BFF**: the per-collection share controls (`CollectionShareControls` on the
  collection-detail view) create a real tokenized link-with-key via
  `POST /v1/library/collections/:id/share-links` (the one-time token is shown),
  revoke it via `DELETE …/share-links/:tokenHash`, set the collection private
  via `PUT …/share`, assign an explicit named-user grant via the visible
  named-users controls, and publish a public-profile slug/tier via the visible
  public-profile controls. The browser spec reads the live BFF policy back after
  the named-user and public-profile saves, so those modes are not selector-only
  coverage. The sharing engine itself (four-tier visibility, link expiry/revoke,
  constant-time recipient resolution, tenant isolation) is proven at the BFF
  layer by `apps/oshun/bff/src/__tests__/library-sharing-route.test.ts`.
- Backed by
  [`apps/oshun/web/e2e/library-save-collection-share.spec.ts`](../../apps/oshun/web/e2e/library-save-collection-share.spec.ts)
  — real-BFF saved-items seed/readback for the rows this spec asserts, plus a
  direct `/domains/nisaba?path=/library/passage/...` non-search save producer:
  the browser clicks the Nisaba reader save toggle, waits for the live
  `/v1/library/saved-items/nisaba/<passageId>` write, verifies persisted
  metadata, and hydrates the passage row from a clean second-device `/library`
  load. The same spec also covers the library resume rail, saved queue
  filters/snooze/complete/resume, collection share controls mounting, saved-item
  unsave row removal, and the anonymous-to-signed-in save migration failure
  mode: a pending guest item is carried through the real `/welcome` sign-in
  flow, reconciled by the real BFF `/v1/library/saved-items/sync` endpoint, then
  rendered from a clean second-device `/library` hydrate.
- Backed by
  [`apps/oshun/web/e2e/veritas-evidence-trail.spec.ts`](../../apps/oshun/web/e2e/veritas-evidence-trail.spec.ts)
  — the non-search Veritas lead save producer now resets the current lead's
  saved row, clicks `/veritas` "Save to notebook", waits for the real
  `/v1/library/saved-items/veritas/<itemId>` write, verifies persisted passage
  metadata, and hydrates the saved row from a clean second-device `/library`
  load with no copied Library localStorage.
- Backed by
  [`apps/oshun/web/e2e/search-saved-and-recent.spec.ts`](../../apps/oshun/web/e2e/search-saved-and-recent.spec.ts)
  — the real-BFF search-result save-producer branch for **steps 1B–2**: save a
  live Nisaba result through `/v1/library/saved-items`, prove the in-domain
  Nisaba reading action is already saved, drive unsave/resave through real
  `DELETE`/`POST` responses, hydrate `/library` in a fresh second browser
  context with no library localStorage, and resume the exact passage workspace.
  The same spec now also saves Veritas claim/story, Tara practice-as-meditation,
  Nyx sky-event, and Metis course search results, verifies their saved-items
  metadata, hydrates each through a cache-free Library filter, and resumes each
  canonical destination.
- Backed by
  [`apps/oshun/web/e2e/explore-real-search-continuity.spec.ts`](../../apps/oshun/web/e2e/explore-real-search-continuity.spec.ts)
  — the live Explore-dashboard result save producer now starts from a real
  `/v1/search` Nisaba result, clicks the `/explore` row save toggle, waits for
  the real `/v1/library/saved-items/nisaba/nisaba-passage-speech` write,
  verifies the Explore-specific saved metadata, and hydrates the saved passage
  row from a clean second-device `/library` load.
- Backed by
  [`apps/oshun/web/e2e/sophia-grounded-answer-page.spec.ts`](../../apps/oshun/web/e2e/sophia-grounded-answer-page.spec.ts)
  — the direct `/sophia` grounded-answer save producer now submits to the live
  Sophia BFF, saves the answer card through the real
  `/v1/library/saved-items/nisaba/<itemId>` route, verifies persisted
  query/primary-witness/surface/trust metadata, and hydrates the
  `assistant-excerpt` row from a clean second-device `/library` load.
- Backed by
  [`apps/oshun/web/e2e/atelier-plate-actions.spec.ts`](../../apps/oshun/web/e2e/atelier-plate-actions.spec.ts)
  — the non-search Atelier generated-artifact save producer now saves a kept
  plate with the browser-edited prompt, reads the persisted metadata back from
  the real `/v1/library/saved-items` BFF, and hydrates the generated-artifact
  row in a clean second browser context with no copied Library localStorage.
- Backed by
  [`apps/oshun/web/e2e/library-shared-resolution.spec.ts`](../../apps/oshun/web/e2e/library-shared-resolution.spec.ts)
  _(new: recipient-side resolution)_ — closes the loop
  `collection-share.spec.ts` opens by exercising the **real-HTTP** recipient
  share-link resolution against the live BFF: a valid token grants the tier
  (`granted` / `via=link`), a wrong token `403`, and a **revoked** link `403`.
  This is the headline opt-out/scope invariant from **steps 7–8** (the sender
  creates links; the recipient's read must enforce the grant) that the
  share-controls spec created links for but never resolved.
- Backed by
  [`apps/oshun/web/e2e/library-shared-web-view.spec.ts`](../../apps/oshun/web/e2e/library-shared-web-view.spec.ts)
  _(new: recipient-side web view)_ — drives the generated
  `/share/:token?target=collection&collectionId=...` URL in a second signed-in
  browser session. The page resolves the share through the real BFF, renders the
  granted collection id/tier/via state, proves the route does **not** mount the
  recipient's local `LibraryDashboard` collection editor even when localStorage
  contains a same-id shadow collection, and renders a real BFF
  `insufficient-permission` denial when the URL asks for `copy` permission from
  a `view` token.
- **Coverage depth**: partial — see [`coverage.md`](./coverage.md).
- **Uncovered**:
  - Step 10 now has browser-level save→sync→resume coverage for the seeded
    search-result paths plus the non-search Atelier generated-artifact producer
    and direct Nisaba/Veritas passage producers: device A saves through the real
    BFF, device B hydrates `/library` without local cache, and the Library row
    resumes the asserted destination. The live Explore-dashboard search lane now
    has the same BFF-write/clean-device-hydrate proof for a Nisaba passage; the
    direct Sophia grounded-answer card has the same proof for a Nisaba
    `assistant-excerpt`. Remaining gaps are other non-search save producers
    beyond Atelier/Nisaba/Veritas/Sophia and broader per-domain save-toggle
    permutations outside the seeded Search/Explore/Sophia matrix.

## Per-view files touched

- [`customer/02-home-discovery/library.md`](../customer/02-home-discovery/library.md)
  — primary library dashboard; collections lane; saved items list; filters
- [`customer/02-home-discovery/library-collection-collectionId.md`](../customer/02-home-discovery/library-collection-collectionId.md)
  — deep-link collection surface; editor; query-driven filters
- [`customer/02-home-discovery/search.md`](../customer/02-home-discovery/search.md)
  — `Library save toggle` from search results
- [`customer/02-home-discovery/explore.md`](../customer/02-home-discovery/explore.md)
  — `Library save toggle` from curated cards
- [`customer/02-home-discovery/home.md`](../customer/02-home-discovery/home.md)
  — focal-rail Library shortcut
- [`customer/07-nisaba/nisaba.md`](../customer/07-nisaba/nisaba.md) — save
  passage from Nisaba (NisabaPassageActions)
- [`customer/05-veritas/veritas-story.md`](../customer/05-veritas/veritas-story.md)
  — save passage from Veritas
- [`customer/06-nyx/domains-nyx-events-eventId.md`](../customer/06-nyx/domains-nyx-events-eventId.md)
  — save event (separate store; cross-domain consistency gap)
- [`customer/13-system/aaa-upgrade.md`](../customer/13-system/aaa-upgrade.md) —
  paywall behavior for gated artifacts

## Cross-references

- Feature spec:
  - [`V1/features.md`](../../V1/features.md#customer-curation-notebooks-collections-and-sharing)
    § Customer Curation, Notebooks, Collections, and Sharing
- Architecture:
  - [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#2-adapters--libsoshundomain--and-substrate-adapters)
    — Aje (Yoruba commerce orisha) owns crypto/Web3 payment substrate; any
    paywall behavior in the library routes through Aje's adapter
  - [`V1/ARCHITECTURE.md`](../../V1/ARCHITECTURE.md#shared-consumer-shell--libsoshunshell-)
    — shell `library` route ownership
- BFF routes:
  - `apps/oshun/bff/src/routes/library-saved-items.ts` — signed-in saved-item
    snapshot, save, delete, and guest-migration sync
  - `apps/oshun/bff/src/routes/library-sharing.ts` — collection share policy,
    token lifecycle, and recipient resolution
  - `apps/oshun/bff/src/routes/library.ts` — legacy aggregate/favorites read
- Related journeys:
  - [`messages-center-deliver-and-unsubscribe.md`](./messages-center-deliver-and-unsubscribe.md)
    — sharing notification triggers a message
  - [`multi-device-workspace-handoff.md`](./multi-device-workspace-handoff.md) —
    cross-device library sync via Iris preferences
  - [`sophia-grounded-answer.md`](./sophia-grounded-answer.md) — Save to library
    from a Sophia grounded answer carries the citation trail at pin-time
- Component sources:
  - `apps/oshun/web/src/components/library/LibraryDashboard.tsx`
  - `apps/oshun/web/src/components/library/libraryModels.ts`
  - `apps/oshun/web/src/components/library/librarySavedItems.ts`
  - `apps/oshun/web/src/components/library/ResearchPracticeJourneyRail.tsx`
  - `apps/oshun/web/src/lib/library/webLibraryStore.ts` —
    `useOshunWebLibraryStore`, `toggleOshunWebLibraryItem`
  - `apps/oshun/web/src/lib/library/librarySaveMapper.ts` —
    `buildLibraryItemFromSearchResult`

## Open questions / known gaps

- [x] The signed-in saved-item endpoint is `/v1/library/saved-items`;
      `webLibraryStore` pulls it during hydration and pushes non-demo
      saves/unsaves in the background.
- [x] Document the share-permissions contract end to end — the shipped local-dev
      contract is `CollectionShareControls` plus
      `/v1/library/collections/:id/share`,
      `/v1/library/collections/:id/share-links`,
      `/v1/library/shared/:collectionId`, and `/share/:token`. Browser coverage
      proves private, link/invite/referral, named-users, public-profile policy
      writes, recipient grant rendering, insufficient-permission denial, wrong
      token denial, and revoked-token denial.
- [ ] Snapshot the embeddable share-card renderer per artifact type (passage,
      claim, sky event, meditation, lesson module) and verify provenance /
      grounding / synthetic indicators are preserved
- [ ] Confirm whether Nyx event saves surface in `/library` or only in
      `/domains/nyx/events?filter=saved` (separate store, per
      [`README.md`](./README.md) finding 5)
- [ ] Per-view file Open question: confirm whether user-created collections can
      be deleted (the source defines `LIBRARY_COLLECTIONS` as readonly; only
      stored extras appear removable)
- [x] Per-view file Open question: document collection sharing semantics — the
      pre-BFF localStorage risk is closed. Collection editor deep links remain
      personal, while actual recipient shares use the BFF-generated
      `/share/:token?target=collection&collectionId=...` URL and the
      recipient-side page intentionally avoids the personal Library editor.
- [ ] Persist collection composition itself—or clearly keep it device-local—and
      make the recipient route render the authorized collection content rather
      than only its ID and permission grant.
