explore.mdhome.mdlibrary-collection-collectionId.mdlibrary.mdsearch.mdveritas-story.mddomains-nyx-events-eventId.mdnisaba.mdaaa-upgrade.mdJourney flow#
Generated from the authored steps below — click a node to jump to that section.
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
/libraryand 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
Pre-conditions#
- Signed-in session (per
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 (percustomer/02-home-discovery/library.mdData & 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, Save buttons now wire touseOshunWebLibraryStorein Veritas / Nisaba / Search / Explore / Tara) - Service worker active so cached
/libraryroute reads work offline (pershell/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)
- 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)
- On
/nisaba, the passage actions row exposesNisabaPassageActions“Save passage" — saves anisabapassage library item perREADME.mdfinding 1
C. From Search
- On
/search, each result row exposes a library toggle whencanSaveSearchResultToLibrary(result)percustomer/02-home-discovery/search.mdLibrary save toggle - Click →
toggleOshunWebLibraryItem(buildLibraryItemFromSearchResult(...));trackLibraryItemSavedfires
D. From Explore
- On
/explore, live search-backed cards expose the same toggle when library-saveable percustomer/02-home-discovery/explore.mdLibrary save toggle
E. From Nyx event detail
- On
/domains/nyx/events/<eventId>, click the "Save event" toggle (data-testid="nyx-event-save-toggle") percustomer/06-nyx/domains-nyx-events-eventId.mdAction row 2 - Note that Nyx save state is owned by
useNyxEventActionState(localStorageoshun:nyx:event-actions:v1), notuseOshunWebLibraryStore— verify whether the Nyx save surfaces in/libraryor only in the Nyx event lane (this is one of the polished-vs-internal dual-fixture patterns flagged inREADME.mdfinding 5)
2. Open /library and confirm the save#
- Navigate to
/libraryvia shell nav (or viaWEB_SHELL_ROUTE_PATHS.library) -
ShellLayout active="library"mounts; breadcrumbHome → Library -
<LibraryDashboard />mounts; pre-hydrationShellRouteAnchor("Keep what matters, then return to it deliberately") flashes whilehasMounted === falsepercustomer/02-home-discovery/library.mdStates — Loading - Focal stage renders frame title "Saved continuity across the shell" and
the four
ShellLandingMetricPills (saved count, distinct domains, study count, re-entry count) - The just-saved item appears after the signed-in server reconciliation in
the filtered saved-items list with
LibraryItemSignalsbadges (ConfidenceBadge,LibraryGroundingBadge,ProvenanceBadge) - Verify:
customer/02-home-discovery/library.mdSaved 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.mdWorkspace toolbar
4. Create a collection from the current filters#
- Click "Create collection from filters"
(
handleCreateCollectionFromFilters) - A new collection clones the current
filteredItemsinto a fresh collection with a derived label - Router pushes to
/library/collection/<slug>perbuildLibraryCollectionHrefpercustomer/02-home-discovery/library.mdCreate collection from filters - Verify:
customer/02-home-discovery/library-collection-collectionId.mdLayout 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.labelpopulates the collection editor label input - Verify
selectedCollection.descriptionpopulates the description input - Edit the label (e.g. "Ruling part — companion") — drives
collectionEditor;syncCollectionwrites back tocollectionsand tolocalStorage['oshun.library.collections.v1'] - Edit the description and the followUp copy
- Switch back to
/library(clear the collection — callsnavigateToCollection(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)) - For each saved item, use the selected collection's membership toggle.
handleToggleCollectionItemupdatescollectionItemIds; metrics recompute throughwithCollectionMetricsand 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 byresumePriority(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:
private, shared with named users, shared via link with permissions (view,
comment, copy), and public-profile surface where the customer has opted in.
- Open the collection editor for the just-created collection
- Locate the "Share" / "Visibility" controls —
CollectionShareControlsmounts on/library/collection/research-to-practiceandlibrary-save-collection-share.spec.tsasserts the share-control mount. - Pick visibility: Private (only the user; default)
- Save — the collection retains
privatevisibility via the realPUT /v1/library/collections/:id/shareBFF route. - Switch to Link with key — a tokenized share URL is generated by the
real
POST /v1/library/collections/:id/share-linksroute. The current browser route is/share/:token?target=collection&collectionId=...rather than the older local/library/collection/<id>?key=...draft shape. - Copy/link result exposes the granted permission tier (view / comment /
copy) per
V1/features.md - 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"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#
- In a second browser session (different account, or anonymous), open the shared URL
- Private / revoked / wrong token: recipient-side BFF resolution returns
403and does not grant the collection.library-shared-resolutioncovers wrong-token and revoked-token denial; the route/store tests cover private denial. - 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-viewalso proves the page does not mount the recipient's localLibraryDashboardcollection even when localStorage has a same-id shadow collection. - 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 - Verify:
V1/features.mdsharing-permission isolation tests
9. Paywall behavior#
Per the cross-cutting README — Aje owns Web3 / crypto payments per
V1/ARCHITECTURE.md.
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— middleware bounces unentitled users to/aaa-upgraderather than showing the artifact - Crypto-payment rail handled by
libs/oshun/aje/*perV1/ARCHITECTURE.md; flow is out of scope for/libraryitself — 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.
- On a second device (signed in as the same user), open
/librarywithout copying Library localStorage. -
webLibraryStore.hydrate()reads/v1/library/saved-items; the item appears and resumes its canonical destination. - Guest saves are journaled under
oshun.web.library.pending-auth-migration.v1and reconciled through/v1/library/saved-items/syncafter 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.mdHomeMultiDeviceContinuityPanel— 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 -
trackLibraryItemUnsavedanalytics fires percustomer/02-home-discovery/library.mdSave toggle - Inside a collection: per-view file flags "remove item from collection" —
setCollectionItemIdsmutates 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_COLLECTIONSas 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_unsavedanalytics 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;
/libraryreads from another — Nyx event save usesuseNyxEventActionState(separate localStorage keyoshun:nyx:event-actions:v1); confirm whether Nyx saves surface in/libraryor only in the Nyx event lane (perREADME.mdfinding 5) - 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/syncroute when an access token appears.library-save-collection-share.spec.tssigns in through/welcome, reads the migrated row back from the BFF, and opens/libraryin a clean second browser context to prove the row is server-backed rather than localStorage residue. - Stored collections corrupted / missing fallback covered —
readStoredLibraryCollectionsreturnsnull; default collections render and user-created corrupt payload rows are ignored percustomer/02-home-discovery/library.mdStates and the collection-route E2E addendum - Collection deep link 404 fallback covered —
page.tsxdoes not 404; the dashboard preserves the unknown URL, clears collection management, and applies valid query-seeded filters percustomer/02-home-discovery/library-collection-collectionId.mdStates - LocalStorage version migration covered —
library-collection-management.spec.tsnow seeds a legacyversion: 0oshun.library.collections.v1payload, 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 currentversion: 1. - Sharing link shows recipient's own collection — fixed by the
BFF-backed
/share/:tokenroute.library-shared-web-viewopens 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 thispage.tsxdoes 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.mdOpen 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.mdmiddleware enforces upstream - Share with provenance stripped — embeddable share cards must preserve
grounding, synthetic, and provenance indicators per
V1/features.md; verify the share-card renderer doesn't drop these on small breakpoints
E2E coverage#
- Backed by
apps/oshun/web/e2e/library-collection-management.spec.tsandapps/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→ currentversion: 1with duplicate/stale item IDs sanitized); the analytics spec assertsitem_saved/item_unsavedtelemetry events from Explore, Search, Library, and the assistant panel. - Backed by
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 gateGET /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 higherx-oshun-creator-tier(aaa-creator) unlocks it whilecurated-creatorstays denied per-surface, unknown surface → 400unknown_surface, no bearer → 401. (Save round-trip) the server-authoritativePOST/DELETE /v1/favorites/:idtoggle (favorited:true↔favorited:false) and theGET /v1/libraryaggregation envelope asserting the tara-branch mapping (itemTypefavorite, keytara:<id>), each branching on the fail-closed Tara upstream (201/200 when up, 502upstream_errorwhen down) plus 403domain_scope_missing/ 401 gates. - Backed by
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 (CollectionShareControlson the collection-detail view) create a real tokenized link-with-key viaPOST /v1/library/collections/:id/share-links(the one-time token is shown), revoke it viaDELETE …/share-links/:tokenHash, set the collection private viaPUT …/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 byapps/oshun/bff/src/__tests__/library-sharing-route.test.ts. - Backed by
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/libraryload. 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/welcomesign-in flow, reconciled by the real BFF/v1/library/saved-items/syncendpoint, then rendered from a clean second-device/libraryhydrate. - Backed by
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/libraryload with no copied Library localStorage. - Backed by
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 realDELETE/POSTresponses, hydrate/libraryin 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— the live Explore-dashboard result save producer now starts from a real/v1/searchNisaba result, clicks the/explorerow save toggle, waits for the real/v1/library/saved-items/nisaba/nisaba-passage-speechwrite, verifies the Explore-specific saved metadata, and hydrates the saved passage row from a clean second-device/libraryload. - Backed by
apps/oshun/web/e2e/sophia-grounded-answer-page.spec.ts— the direct/sophiagrounded-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 theassistant-excerptrow from a clean second-device/libraryload. - Backed by
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-itemsBFF, 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(new: recipient-side resolution) — closes the loopcollection-share.spec.tsopens by exercising the real-HTTP recipient share-link resolution against the live BFF: a valid token grants the tier (granted/via=link), a wrong token403, and a revoked link403. 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(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 localLibraryDashboardcollection editor even when localStorage contains a same-id shadow collection, and renders a real BFFinsufficient-permissiondenial when the URL asks forcopypermission from aviewtoken. - Coverage depth: partial — see
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
/librarywithout 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 Nisabaassistant-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.
- 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
Per-view files touched#
customer/02-home-discovery/library.md— primary library dashboard; collections lane; saved items list; filterscustomer/02-home-discovery/library-collection-collectionId.md— deep-link collection surface; editor; query-driven filterscustomer/02-home-discovery/search.md—Library save togglefrom search resultscustomer/02-home-discovery/explore.md—Library save togglefrom curated cardscustomer/02-home-discovery/home.md— focal-rail Library shortcutcustomer/07-nisaba/nisaba.md— save passage from Nisaba (NisabaPassageActions)customer/05-veritas/veritas-story.md— save passage from Veritascustomer/06-nyx/domains-nyx-events-eventId.md— save event (separate store; cross-domain consistency gap)customer/13-system/aaa-upgrade.md— paywall behavior for gated artifacts
Cross-references#
- Feature spec:
V1/features.md§ Customer Curation, Notebooks, Collections, and Sharing
- Architecture:
V1/ARCHITECTURE.md— Aje (Yoruba commerce orisha) owns crypto/Web3 payment substrate; any paywall behavior in the library routes through Aje's adapterV1/ARCHITECTURE.md— shelllibraryroute ownership
- BFF routes:
apps/oshun/bff/src/routes/library-saved-items.ts— signed-in saved-item snapshot, save, delete, and guest-migration syncapps/oshun/bff/src/routes/library-sharing.ts— collection share policy, token lifecycle, and recipient resolutionapps/oshun/bff/src/routes/library.ts— legacy aggregate/favorites read
- Related journeys:
messages-center-deliver-and-unsubscribe.md— sharing notification triggers a messagemulti-device-workspace-handoff.md— cross-device library sync via Iris preferencessophia-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.tsxapps/oshun/web/src/components/library/libraryModels.tsapps/oshun/web/src/components/library/librarySavedItems.tsapps/oshun/web/src/components/library/ResearchPracticeJourneyRail.tsxapps/oshun/web/src/lib/library/webLibraryStore.ts—useOshunWebLibraryStore,toggleOshunWebLibraryItemapps/oshun/web/src/lib/library/librarySaveMapper.ts—buildLibraryItemFromSearchResult
Open questions / known gaps#
- The signed-in saved-item endpoint is
/v1/library/saved-items;webLibraryStorepulls it during hydration and pushes non-demo saves/unsaves in the background. - Document the share-permissions contract end to end — the shipped local-dev
contract is
CollectionShareControlsplus/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
/libraryor only in/domains/nyx/events?filter=saved(separate store, perREADME.mdfinding 5) - Per-view file Open question: confirm whether user-created collections can
be deleted (the source defines
LIBRARY_COLLECTIONSas readonly; only stored extras appear removable) - 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.