# Customer Curation

Customer Curation is the V1 **personal-knowledge layer**: the surfaces that let
a member keep, organize, annotate, share, and re-find anything they encounter
anywhere on the platform — a Veritas claim, a Nisaba passage, a Nyx sky event, a
Metis lesson, a Tara meditation transcript, an Arete habit, or a generated
artifact. It serves every customer-facing domain rather than belonging to any
one of them, and it is the substrate that Search and Recommendations read back
from (see
[Search, Discovery, and Knowledge Graph](./search-discovery-knowledge-graph.md)).
This page sits among the cross-cutting product-surface deep-dives in the V1
architecture set hubbed at [../ARCHITECTURE.md](../ARCHITECTURE.md); product
scope lives in
[`V1/features.md` § Customer Curation, Notebooks, Collections, and Sharing](../features.md#customer-curation-notebooks-collections-and-sharing),
and the backlog is [`V1/TODOS.md` § 17](../TODOS.md).

> **Read this page for what is _shipping_ vs. _spec_.** The curation **logic is
> real and non-stub**: it ships as a pure, dependency-free contracts-and-logic
> library, `@oshun/customer-curation` (`libs/oshun/customer-curation`), whose
> seven collection kinds, sharing-permission resolver, smart-collection rule
> engine, W3C-style annotation anchoring, oEmbed share cards, and version-diff
> engine are all implemented with domain-specific algorithms and exercised by a
> large test file (`src/customer-curation.test.ts`). What is **aspirational
> relative to this library** is the _platform plumbing_ the architecture text
> attributes to it: durable database persistence, tombstone-deletion
> propagation, audit-logging, cross-device progress _sync_, and the binding to
> the Iris `notebook` memory scope. All of them live **outside** this pure
> library (in infra and other libs) or remain partial. The completeness audit
> (2026-06-22) flags `library-save-collection-share` and `scene-keep-and-share`
> as **PARTIAL** walkthrough journeys — consistent with the library being
> contract-complete while the end-to-end persisted flow is still thinner than
> the prose implies.

## Where the logic actually lives

Everything described below is implemented in one package:

- **Package:** `@oshun/customer-curation`, version `0.1.0`, `private: true`.
- **Dependencies:** **none at runtime.** `package.json` declares only
  `devDependencies` (`typescript`, `vitest`) — it is a pure data-and-functions
  library with `sideEffects: false`. Notably it does **not** depend on
  `@oshun/persistence`, does not import any database client, and contains no
  code that references "tombstone" or writes an audit log. Those behaviors,
  where they exist, belong to other libraries wired at the call site.
- **Barrel:** `src/index.ts` re-exports six modules — `collections`, `sharing`,
  `share-cards`, `annotations`, `bookmarks`, and `version-awareness`.

| Module            | Path                                         | Responsibility                                                                     |
| ----------------- | -------------------------------------------- | ---------------------------------------------------------------------------------- |
| collections       | `src/collections/`                           | Seven collection kinds, validation, smart collections, ordering, templates, import |
| sharing           | `src/sharing/sharing.ts`                     | Visibility/permission matrix and viewer-resolution                                 |
| share-cards       | `src/share-cards/share-cards.ts`             | Embeddable cards, oEmbed and HTML rendering, indicator preservation                |
| annotations       | `src/annotations/annotations.ts`             | Highlights, notes, threads, citations, selector-chain anchoring, exports           |
| bookmarks         | `src/bookmarks/bookmarks.ts`                 | Per-device cursors, EWMA pace, time-to-finish, reading-list progress               |
| version-awareness | `src/version-awareness/version-awareness.ts` | Staleness verdicts and Myers/LCS passage diff                                      |

## Collections — seven kinds, one taxonomy

The heart of the library is a single collection taxonomy, defined in
`src/collections/types.ts`. `COLLECTION_KINDS` enumerates six canonical
container kinds — `notebook`, `collection`, `study-queue`, `ritual-set`,
`reading-list`, and `saved-search` — and `collections.ts` adds an
`OrderedCollection` for the bare `collection` kind, so seven concrete
`CustomerCollection` shapes exist in total. Every kind extends a shared
`CollectionBase` (`collectionId`, `kind`, `ownerId`, `tenantId`,
`parentCollectionId`, `title`, timestamps, `archivedAtUnixSeconds`, and an
ordered `items: CollectionArtifactRef[]` list). Each kind then layers on
**kind-specific schema fields** that the prose "study queues, ritual sets,
reading lists, saved searches" hides:

| Kind           | Interface               | Kind-specific fields                                                                                            |
| -------------- | ----------------------- | --------------------------------------------------------------------------------------------------------------- |
| `notebook`     | `NotebookCollection`    | `layout: 'linear' \| 'kanban' \| 'grid'` (`NOTEBOOK_LAYOUTS`)                                                   |
| `collection`   | `OrderedCollection`     | (ordered set; base only)                                                                                        |
| `study-queue`  | `StudyQueueCollection`  | `masteryTarget: 'familiar' \| 'developing' \| 'proficient' \| 'master'`, `perDayCap: number \| null`            |
| `ritual-set`   | `RitualSetCollection`   | `cadenceLocalTime: string \| null` (`HH:MM`), `cadenceDaysOfWeek: ReadonlyArray<0..6>` mask (0 = Sun … 6 = Sat) |
| `reading-list` | `ReadingListCollection` | `targetFinishUnixSeconds: number \| null`                                                                       |
| `saved-search` | `SavedSearchCollection` | `queryText`, `filters` AST, `artifactKindFilter`, `lastExecutedCursor`                                          |

Membership is by **typed reference, not by copy**. A `CollectionArtifactRef`
records the `artifactId`, an `artifactKind` drawn from the large cross-domain
`ARTIFACT_KINDS` union (e.g. `nisaba.edition`, `veritas.claim`, `metis.lesson`,
`tara.ritual-script`, `nyx.sky-event`, `arete.habit`,
`oshun.generated-artifact`), a `pinnedVersion: number | null` (where `null`
means _always-latest_ and a number pins the exact version saved), a free-form
`note`, an `ordinal`, and `addedAtUnixSeconds`. Pinning is what makes the
version-awareness feature later in this page possible.

### Validation is the contract

`validateCollection(collection, { tenantId })` returns a typed
`CollectionValidationError[]` — never throws — so callers compose it into their
own request handling. The error union is exhaustive and kind-aware. Beyond the
generic checks (`empty-title`, `tenant-mismatch`, `created-after-updated`,
`archive-before-created`, duplicate-artifact and duplicate/invalid-ordinal,
`invalid-pinned-version`), it enforces each kind's rules:

- **notebook** — `layout` must be one of `NOTEBOOK_LAYOUTS`
  (`invalid-notebook-layout`).
- **ritual-set** — every `cadenceDaysOfWeek` entry must be in `0..6`
  (`invalid-cadence-day`), no duplicates (`duplicate-cadence-day`), and
  `cadenceLocalTime` must match `^([01]\d|2[0-3]):([0-5]\d)$`
  (`invalid-cadence-time`).
- **study-queue** — `perDayCap`, when set, must be an integer in `1..200`
  (`per-day-cap-out-of-range`).
- **reading-list** — `targetFinishUnixSeconds`, when set, must be an integer not
  earlier than `createdAtUnixSeconds` (`invalid-target-finish`).
- **saved-search** — non-empty `queryText`, non-empty `artifactKindFilter` whose
  entries are real `ARTIFACT_KINDS`, and well-formed filter predicates (`facet`
  and operator in `eq | in | gte | lte | contains`).

The kinds also carry **real behavioral helpers**, not just shape:
`nextStudyQueueItem` ranks queue items by a mastery ladder
(`novice < familiar < developing < proficient < master`) and returns the first
item still below `masteryTarget`, breaking ties by `ordinal`;
`pendingReadingListItems` filters a reading list to artifacts whose progress
fraction is `< 1`, ordered by `ordinal`; and `ritualSetDueToday` answers the
day-of-week-mask question (an empty mask means _every day_).

### Ordering, nesting, templates, import

Four supporting files round out the collections module — the features promised
by "drag-and-drop ordering, nested collections, … templates, and import from
another collection":

- **`ordering.ts`** implements gap-based drag-to-order. `reorderArtifact` places
  an item immediately after another (or at the front) by computing a new ordinal
  — midpoint between neighbors, or `± DEFAULT_ORDINAL_GAP` (`1024`) at the edges
  — and triggers a `normalizeOrdinals` pass when gaps collapse below threshold.
  It also exposes `validateNestedParent` and `buildBreadcrumb` for nested
  collections.
- **`templates.ts`** defines `CollectionTemplate` / `CollectionTemplateItem`,
  `instantiateTemplate`, and `templateVisibleTo` for the role-aware template
  gallery.
- **`import-from-another.ts`** implements `importCollection`, which **requires
  an authorization grant from the sharing layer**: the input carries a `grant`
  whose `sourceCollectionId` must match the source, whose `grantedToOwnerId`
  must be the importer, and which must be `copy`-tier; cross-tenant copy must be
  opted in via `allowCrossTenantCopy`. The result is a tagged union —
  `{ ok: true, collection }` carrying `originCollectionId` / `originOwnerId`
  provenance, or
  `{ ok: false, reason: 'grant-mismatch' | 'insufficient-tier' | 'tenant-isolation' }`.

## Smart collections — a real rule engine

The features line "smart collections (rule-based)" undersells what
`src/collections/smart-collections.ts` ships: a small but real predicate engine
in **conjunctive normal form** (AND-of-OR). A `SmartCollectionRules` has
`allOf: SmartCollectionRuleGroup[]` (every group must match), each group an
`anyOf: SmartCollectionPredicate[]` (any predicate inside it matches), plus a
`limit` and an `orderBy`
(`recency-desc | recency-asc | relevance-desc | alphabetical`). The
`SmartCollectionPredicate` union covers artifact-kind eq/in, tenant/owner
equality, `tag-in`/`tag-contains`, `created-at` gte/lte, `domain-eq` (over a
`SmartCollectionDomain` of the ten platform domains), `synthetic-indicator-eq`,
`grounding-state-eq`
(`grounded | partial | ungrounded | abstained | retracted-source`), and a
case-insensitive `text-contains` over title or description.

Three public functions drive it:

- `validateSmartCollectionRules(rules)` returns typed errors (`empty-rule-set`,
  `empty-rule-group`, `invalid-limit`, `empty-text-query`, `empty-tag-list`,
  `unknown-artifact-kind`) so a rule set is checked before it is ever evaluated.
- `evaluateSmartCollection(rules, candidates)` filters candidates through the
  CNF, then sorts by `orderBy` and applies `limit`.
- `materializeSmartCollectionForOwner(rules, candidates, scope)` is the
  **scoped** entry point that ad-hoc callers should use: it keeps tenant/owner
  isolation out of caller hands by first dropping any candidate whose `tenantId`
  differs, then admitting only owner-owned artifacts **or** artifacts whose
  `${artifactKind}:${artifactId}` key appears in `scope.sharedArtifactKeys`
  (which must already be authorized by the sharing layer) — before delegating to
  `evaluateSmartCollection`.

A `smartRulesEqual` helper canonicalizes rule groups (sorting predicates and
`tag-in`/`artifact-kind-in` value lists) so two rule sets can be compared for
equivalence — used to dedupe smart collections in a library and to short-circuit
re-evaluation when nothing changed.

## Sharing — visibility × permission, with a viewer resolver

`src/sharing/sharing.ts` models sharing as **two orthogonal axes**. Visibility
(`SHARE_VISIBILITY`, least → most permissive) is `private`, `named-users`,
`link`, `public-profile`; permission grants (`SHARE_PERMISSION`) are `view`,
`comment`, `copy`. A `SharePolicy` carries the visibility plus the named-user
grants, tokenized link grants, and the public-profile surface.

The core is `resolveSharePermission(input)`, which decides what a specific
viewer gets. Viewers are typed — `owner`, `authenticated` (with `userId` and
`tenantId`), `link` (with `providedTokenHash`), or `public-profile-visitor` —
and the resolver returns either `{ granted: true, tier, via }` (where `via` is
`'owner' | 'named' | 'link' | 'public-profile'`) or
`{ granted: false, reason }`. The denial reasons are an honest enumerable set:

| `ShareDenialReason`           | When it fires                                              |
| ----------------------------- | ---------------------------------------------------------- |
| `private`                     | Visibility is private (or wrong viewer for the visibility) |
| `not-in-named-list`           | Authenticated viewer absent from `namedUserGrants`         |
| `link-expired`                | Link grant past `expiresAtUnixSeconds`                     |
| `link-revoked`                | Link grant has a `revokedAtUnixSeconds`                    |
| `link-token-mismatch`         | No link grant matches the provided token hash              |
| `public-profile-not-enabled`  | `public-profile` visibility but surface disabled           |
| `public-profile-missing-slug` | Surface enabled but no `profileSlug`                       |
| `tenant-isolation`            | Authenticated viewer's `tenantId` ≠ policy's               |

Security-relevant details are real, not hand-waved. The owner always resolves to
`copy` tier. Link verification uses a `timingSafeEqual` constant-time comparison
(length-masked to avoid early-return timing leaks) — the module **compares**
token hashes but explicitly leaves crypto-secure **generation** to the caller. A
named-user grant takes precedence even when visibility is `link` or
`public-profile`, so an explicitly named viewer is never _down_-graded by a
broader surface. Companion functions cover the lifecycle: `validateSharePolicy`
(duplicate grants, expiry/revocation before creation, public-profile enabled
without slug), `revokeLinkGrant`, `revokeNamedUserGrant`,
`sharePermissionSatisfies` / `sharePermissionRank` (view = 1, comment = 2, copy
= 3), and `publicProfileSurfacedSlugs` to build a member's public profile page.

## Annotations — W3C-style selector chains

`src/annotations/annotations.ts` is far more concrete than "highlights, threaded
notes, citations." Annotations attach to one of four reading surfaces
(`ANNOTATION_SURFACES`: `nisaba.edition`, `veritas.story`, `metis.lesson`,
`tara.transcript`) and come in four kinds (`ANNOTATION_KINDS`: `highlight`,
`note`, `thread-comment`, `citation`). Spans use the same **selector-chain model
as Hypothes.is / W3C Web Annotations**: an `AnnotationTarget` carries a
`TextQuoteSelector` (`exact`, `prefix`, and `suffix`) and a
`TextPositionSelector` (`start`/`end` offsets), so anchors survive small edits.

`resolveAnnotationAnchor(annotation, currentText)` is the recovery algorithm,
with a documented strategy precedence:

1. **`exact`** — the saved `position` still slices to `quote.exact` in the
   current text; reuse it verbatim.
2. **`context`** — `prefix + exact + suffix` occurs exactly once; re-anchor to
   the `exact` substring inside it.
3. **`quote-only`** — `exact` alone occurs exactly once (lossy); use it.
4. Otherwise **unresolved** (`null`) — the library refuses to guess rather than
   anchor to the wrong place.

Threads are validated for durability across a batch by
`validateAnnotationThreadIntegrity`, which catches `thread-parent-not-found`,
`thread-parent-cross-tenant`, `thread-parent-cross-owner`,
`thread-parent-not-threadable`, `thread-parent-target-mismatch`, and
`thread-parent-cycle`. `buildAnnotationThread` assembles a tree while keeping a
deleted node only if it still has live children (preserving thread cohesion),
and `deriveBacklinks` materializes the **cross-artifact backlinks** the features
doc mentions: each non-deleted `citation` annotation yields an
`AnnotationBacklink` record on its target.

Export is implemented for all three formats: `exportAnnotationsAsJson`
(canonical, stable-sorted), `exportAnnotationsAsMarkdown` (grouped by artifact,
chronological), and `exportAnnotationsAsCsv` (**RFC-4180** escaping, deleted
rows excluded so the export matches what the reader sees).

## Share cards — provenance that survives the embed

`src/share-cards/share-cards.ts` implements embeddable cards for the kinds in
`SHARE_CARD_KINDS` (`veritas.story`, `veritas.claim`, `nisaba.passage`,
`nyx.sky-event`, `tara.meditation`, `metis.module`, `metis.lesson-module`). A
`ShareCard` bundles three signal blocks that **must never be stripped** during
an embed: a `ProvenanceBlock` (source attribution, author ids, evidence-pack and
citation-trail refs, publish time), a `GroundingBlock` (state, citation count,
`retrievalMethods` such as `bm25`/`dense`/`hybrid`/`graph`, retracted-source
count), and a `SyntheticIndicatorBlock` (voice/avatar/text synthesized flags
plus the persona/voice/avatar resource ids).

The architecture text notes the indicators are "preserved" but omits that the
**rendering is implemented**:

- `renderShareCardHtml(card)` emits an `<article class="oshun-card">` with every
  author-controlled field HTML-escaped (`escapeHtml`) to prevent XSS, and
  encodes provenance/grounding/synthetic state as **visible badges** plus an
  embedded `<script type="application/json" class="oshun-card__signals">` block
  — so the indicators survive screenshots, RSS scrapes, and reader-mode
  extractors.
- `renderOEmbed(card)` returns a real `OEmbedResponse` (`version: '1.0'`,
  `type: 'rich'`, `provider_name: 'Oshun'`, `540×320`, `cache_age` 3600s) that
  carries the same provenance/grounding/synthetic blocks as **`oshun_*`
  extension fields**, forcing external embedders to either render the indicators
  or reject the embed.
- `validateShareCard` rejects unknown kinds, empty/non-HTTP canonical URLs,
  negative citation counts, retracted-source mismatches, and any synthetic flag
  set without its backing resource id; `extractIndicatorsFromHtml` /
  `extractSignalsFromHtml` re-parse rendered output for **strip-detection**
  parity checks.

## Bookmarks — per-device cursors and pace-aware estimates

`src/bookmarks/bookmarks.ts` backs "bookmark and reading-list with reading
progress, time-to-finish estimates, and resume state across devices." A
`Bookmark` holds a list of `DeviceCursor`s, each a
`(deviceId, locator, fraction, lastSeenUnixSeconds)`. Locators are typed
(`READ_LOCATOR_KINDS`): `text-offset` (character offset), `timecode` (seconds),
or `page-section` (page plus optional section). `updateCursor` merges a device
update, ignoring stale or regressive cursors; `resumeCursor` returns the
most-recently-seen device cursor (the "where did I leave off on my phone?"
answer) and `farthestCursor` returns the most-advanced one.

Time-to-finish is a real estimate, not a constant.
`computePace(events, now, windowSeconds = 14 days)` computes an **exponentially
weighted** units-per-minute over a 14-day window with a **7-day half-life**, so
recent activity dominates without the estimate becoming jumpy.
`estimateSecondsToFinish` divides remaining units
(`totalUnits × (1 − fraction)`) by the measured pace, falling back to
`defaultUnitsPerMinute` when there are no samples, and
`summarizeReadingListProgress` rolls a whole reading list into
`{ totalItems, completedItems, overallFraction, secondsToFinish, resume }` —
returning `Infinity` for time-to-finish when any item lacks a unit count rather
than fabricating a number.

> **Honest caveat — "across devices" is _modeling_, not _sync_.** This module
> gives every operation the per-device cursor structure and the merge/resume
> semantics needed for cross-device resume, but the actual cross-device
> **synchronization** (transport, conflict propagation, persistence) lives
> outside this pure library. The completeness audit's PARTIAL marks reflect
> exactly this gap between contract-complete logic and a fully persisted, synced
> flow.

## Version awareness — "this changed since you saved it"

`src/version-awareness/version-awareness.ts` realizes the customer-facing "this
passage was updated since you saved it — show diff" feature concretely. A
`CustomerSavedRef` records `savedAtVersion` (a number, or `null` to mean
"intentionally track latest") and `savedAtUnixSeconds`. `evaluateStaleness`
returns a `StalenessVerdict` — `{ stale: false }` when tracking latest or when
the current version isn't ahead, otherwise
`{ stale: true, currentVersion, versionsBehind }`.

The diff is a real **Myers-style line-oriented LCS** (`computePassageDiff`): it
builds the LCS table over `\n`-split lines, backtracks into a `DiffLine[]`
stream of `context`/`added`/`removed` lines (each with old/new line numbers),
and trims to `contextLines` (default 2) around each change hunk so the surfaced
diff reads like prose, not a wall of unchanged text.
`buildVersionAwarenessReport` assembles the full UI payload — `savedAtVersion`,
`currentVersion`, `versionsBehind`, the filtered+sorted `intermediateSummaries`
(only versions strictly after the saved one, up to the current head), and the
diff — giving the reader both a "what changed" narrative and the line-level
diff.

## Platform integration — what's claimed vs. what's wired

The architecture hub historically attributed several **platform-integration**
behaviors to this surface that the library itself does not implement. Reconciled
against the code:

- **Persistence / tombstones / audit-log.** The hub text said "per-domain
  notebook/collection tables persist through `@oshun/persistence` and inherit
  tombstone semantics; deletion propagates and audit-logs." In reality
  `@oshun/customer-curation` declares **no** `@oshun/persistence` dependency, no
  source file imports it, and nothing in `src/` references "tombstone" or writes
  an audit log. `@oshun/persistence` does exist as a library
  (`libs/oshun/persistence`, with Prisma), but any persistence/tombstone/audit
  behavior is wired **elsewhere**, not inside this contracts library.
  (Soft-delete is modeled at the _data_ level — annotations carry
  `deletedAtUnixSeconds`, collections carry `archivedAtUnixSeconds` — but
  propagation is not this lib's job.)
- **Nisaba `Notebook` contract substrate.** The hub framed personal notebooks as
  built on "Nisaba's notebook schema (`Notebook` contract)." That contract is
  real and lives in `libs/contracts/src/common/notebook.ts`
  (`NotebookKindSchema` =
  `research | reflection | study | evidence | mixed | investigation`, plus
  `NotebookDomainSchema`, `NotebookStatusSchema`, `NotebookVisibilitySchema`,
  `NotebookMethodologySchema`, `NotebookViewSchema`, and the full
  `NotebookSchema`). But `collections/collections.ts` defines its **own**
  `NotebookCollection` (`kind: 'notebook'`, `layout: linear | kanban | grid`)
  and does **not** import the contracts `Notebook` schema. The two are separate
  models in separate libraries; the substrate relationship is a design intent,
  not a realized import.
- **Iris `notebook` memory scope.** This one is **directionally correct** but
  was stated imprecisely. `notebook` _is_ a first-class Iris memory scope:
  `IrisMemoryScope` (`libs/oshun/memory-iris/src/types.ts:45`) is an
  **11-value** union — `assistant_profile`, `session`, `scene`, `pose`,
  `conversation`, `domain`, `cross_domain`, `notebook`, `operator_copilot`,
  `tenant`, `admin_review` — and the Iris adapter
  (`libs/oshun/memory-iris/src/adapter.ts`) enforces that `notebook`-scoped
  memory requires a `notebookId` "to prevent cross-notebook recall." So the
  older note's enumerated peer list (profile/session/operator-copilot/tenant)
  was incomplete, and the names are stylized differently from code
  (`assistant_profile`, not `profile`; `operator_copilot`, not
  `operator-copilot`). See
  [Iris — Assistant Memory Substrate](./substrate-iris.md).

The takeaway: `@oshun/customer-curation` is the **contracts-and-logic** layer,
and
[`V1/features.md`](../features.md#customer-curation-notebooks-collections-and-sharing)
describes it accurately as self-contained customer surfaces. The architecture
hub over-attributed _integration_ (persistence, Notebook substrate, Iris
binding) to this surface; those layers are real where they exist, but they live
in other libs and infra, and the end-to-end persisted flow is still **partial**
per the 2026-06-22 completeness audit.

## Data-flow walkthrough: "Keep this passage, then share my collection"

1. A reader saves a Nisaba passage. A `CollectionArtifactRef` is appended to a
   `NotebookCollection` (or a `reading-list`), with `pinnedVersion` set to the
   passage's current version (or `null` to track latest). `validateCollection`
   rejects duplicate ordinals, tenant mismatches, or unknown artifact kinds.
2. Later, the passage is re-edited upstream.
   `evaluateStaleness(savedRef, currentVersion)` returns
   `{ stale: true, versionsBehind }`; `buildVersionAwarenessReport` produces the
   trimmed Myers diff the reader sees as "show diff."
3. The reader curates a `smart-collection` ("all my grounded Nisaba passages
   tagged _exegesis_"). `validateSmartCollectionRules` checks the CNF;
   `materializeSmartCollectionForOwner` scopes candidates to the owner's tenant
   and authorized shared keys before `evaluateSmartCollection` filters and
   orders them.
4. The reader shares the collection by link with `comment` tier. The
   `SharePolicy` gets a `LinkGrant`; a visitor's request resolves through
   `resolveSharePermission` (constant-time token check, expiry/revocation,
   tenant isolation) to `{ granted: true, tier: 'comment', via: 'link' }`.
5. A friend imports it. `importCollection` requires a `copy`-tier grant matching
   the source; on success it returns a new collection stamped with
   `originCollectionId` / `originOwnerId`.
6. The reader embeds a single passage as a share card. `renderShareCardHtml` /
   `renderOEmbed` emit the card with provenance/grounding/synthetic badges that
   downstream embedders cannot silently strip.

The _logic_ of every step above is shipping code with tests. The _persistence_
linking steps 1–6 into durable, audit-logged, cross-device-synced records is the
part the completeness audit marks PARTIAL.

## Related

- [Search, Discovery, and Knowledge Graph](./search-discovery-knowledge-graph.md)
  — reads saved-searches, collections, and notebooks back as ranking and
  recommendation signals.
- [Iris — Assistant Memory Substrate](./substrate-iris.md) — owns the `notebook`
  memory scope and its cross-notebook-recall guard.
- [Customer-Facing Domains](./customer-domains.md) — the domains whose artifacts
  (`ARTIFACT_KINDS`) curation references.
- [Trust, Safety, and Privacy](./trust-safety-and-privacy.md) —
  synthetic-content indicators and sharing-abuse controls.
- [`V1/features.md` § Customer Curation, Notebooks, Collections, and Sharing](../features.md#customer-curation-notebooks-collections-and-sharing)
  — product scope.
- Hub: [../ARCHITECTURE.md](../ARCHITECTURE.md); backlog
  [`V1/TODOS.md` § 17](../TODOS.md).
