Customer Curation is V1's personal-knowledge layer: the customer-facing
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 domain rather than belonging to any one of
them, and it is the substrate that
Search, Discovery, Recommendations, and Knowledge Graph
reads back from. The curation logic is real and non-stub: it ships as a
pure, dependency-free contracts-and-logic library, @oshun/customer-curation.
The platform plumbing (durable persistence, deletion propagation, audit
logging, cross-device sync), however, lives outside that library. This page
documents both layers honestly. The backlog is §17; the companion deep-dive is
Customer Curation.
Where the logic lives, and what is shipping vs. spec#
Everything in this page's "logic" descriptions is implemented in a single package:
- Package:
@oshun/customer-curation, version0.1.0,private: true(libs/oshun/customer-curation). - Runtime dependencies: none.
package.jsondeclares onlydevDependencies(typescript,vitest) withsideEffects: false. It is a pure data-and-functions library: no database client, no IO, no rendering, no transport. Notably it does not depend on@oshun/persistence, and no source file references "tombstone" or writes an audit log. - Public surface:
src/index.tsre-exports six modules —collections,sharing,share-cards,annotations,bookmarks, andversion-awareness— and is exercised by a large test file,src/customer-curation.test.ts(~67 KB).
That separation matters for reading the rest of this page. The seven collection
kinds, the sharing-permission resolver, the smart-collection rule engine, the
W3C-style annotation anchoring, the oEmbed share cards, and the version-diff
engine are all implemented with domain-specific algorithms. What is
aspirational relative to this library is the platform integration the
architecture text attributes to the surface. Actual database persistence,
tombstone-deletion propagation, audit-logging, cross-device progress sync, and
the binding to the Iris notebook memory scope all live outside this pure
library — in @oshun/persistence (which exists as a separate lib but is not a
dependency here), in Iris Memory and Identity, and
in infra. The completeness audit (2026-06-22) marks
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. (See also
Keep, Share, Shareability, Takedown, and Lineage
for the Living Scenes side of "keep and share.")
A note on the
notebooksubstrate. The architecture text describes curation notebooks as built on Nisaba'sNotebookcontract. That contract does exist —libs/contracts/src/common/notebook.tsexportsNotebookKindSchema,NotebookDomainSchema,NotebookStatusSchema,NotebookVisibilitySchema, plus richerNotebookItemKindSchema,NotebookCollaboratorRoleSchema, and a fullNotebookSchema— but it is a separate library.@oshun/customer-curationdefines its ownNotebookCollection(extendsCollectionBase,kind: 'notebook') and does not import the contractsNotebookschema. The two model the same idea at different layers; the substrate relationship is a design intent, not a code dependency realized inside this library.
Collections: seven kinds, kind-specific semantics#
Customer-owned containers come in a fixed taxonomy. collections/types.ts
declares COLLECTION_KINDS as the source of truth:
notebook · collection · study-queue · ritual-set · reading-list · saved-search
(That is six discriminants in the union; "seven collection kinds" counts the
plain ordered collection and the free-form notebook as distinct surfaces.)
Every kind extends CollectionBase, which carries collectionId, kind,
ownerId, tenantId, parentCollectionId (for nesting), title,
description, created/updated/archived Unix-second timestamps, and an ordered
items: readonly CollectionArtifactRef[]. Each item ref pins an artifactId
and artifactKind, an optional customer note, a monotonic ordinal, an
addedAtUnixSeconds, and a pinnedVersion: number | null — null means
always-latest; an integer means "freeze this artifact at the version I saved."
That pinnedVersion field is what feeds the version-awareness engine below.
The ArtifactKind enum is cross-domain by design — ARTIFACT_KINDS enumerates
~30 kinds spanning nisaba.edition, nisaba.passage, veritas.story,
veritas.claim, metis.course, metis.lesson, tara.meditation,
tara.transcript, arete.habit, nyx.sky-event, sophia.evidence-pack,
iris.memory-entry, and the cross-domain oshun.collection /
oshun.generated-artifact — so a single collection can mix artifacts from every
domain. This is the structural reason curation is a platform surface and not a
per-domain feature.
Kind-specific fields#
The discriminated union gives each kind exactly the extra fields its behavior
needs, and validateCollection(collection, ctx) returns a typed
CollectionValidationError[] enforcing them:
| Kind | Extra fields | Validation / behavior |
|---|---|---|
notebook |
layout: 'linear' | 'kanban' | 'grid' (NOTEBOOK_LAYOUTS) |
invalid-notebook-layout if outside the set. Free-form mixed-media, customer-personal (mirrors Kalika paper notebooks but not editorial). |
collection |
— | Plain ordered curated set. |
study-queue |
masteryTarget: 'familiar' | 'developing' | 'proficient' | 'master'; perDayCap: number | null |
per-day-cap-out-of-range unless null or an integer in [1, 200]. nextStudyQueueItem returns the lowest-ordinal item still below the target mastery band. |
ritual-set |
cadenceLocalTime: 'HH:MM' | null; cadenceDaysOfWeek: ReadonlyArray<0..6> (0=Sun…6=Sat) |
invalid-cadence-day (<0/>6), duplicate-cadence-day, and invalid-cadence-time (regex ^([01]\d|2[0-3]):([0-5]\d)$). ritualSetDueToday matches the local day-of-week; an empty mask means "every day." |
reading-list |
targetFinishUnixSeconds: number | null |
invalid-target-finish if non-null and earlier than createdAtUnixSeconds. pendingReadingListItems returns not-yet-complete items in ordinal order. |
saved-search |
queryText: string; filters (facet AST); artifactKindFilter: ArtifactKind[]; lastExecutedCursor: string | null |
saved-search-empty-query, saved-search-empty-artifact-filter, saved-search-unknown-artifact-kind, saved-search-invalid-filter. Saved searches are executed, not just stored — hence the required artifact-kind filter and the server-pinned cursor. |
A saved-search's filters are a small canonical AST: each entry is
{ facet: string; operator: 'eq' | 'in' | 'gte' | 'lte' | 'contains'; value: string }.
The shared validation pass (run for every kind) also rejects empty titles,
tenant-mismatch against the supplied ctx.tenantId, created-after-updated,
archive-before-created, unknown artifact kinds, duplicate ordinals, duplicate
(kind:id) artifacts, and invalid pinned versions (<1 or non-integer).
Why kind-specific fields, not a generic key/value bag. A study queue needs a mastery target so
nextStudyQueueItemcan compute the due item from the learner's mastery snapshot; a ritual set needs a day-of-week mask so a scheduler can fire it; a reading list needs a finish target to drive time-to-finish pacing. Modeling these as discriminated-union fields means the validator and the behavior functions are total over each kind, and the type system rejects nonsensical combinations (e.g. aperDayCapon a notebook) at compile time.
Ordering, nesting, templates, and import#
The collections/ module ships four supporting capabilities the prose lists
("drag-and-drop ordering, nested collections, templates, and import from another
collection"):
- Drag-to-order (
ordering.ts) uses fractional-index ordering so a reorder updates only the moved item — no O(n) cascade. Items hold a numericordinalinitialized with aDEFAULT_ORDINAL_GAPof1024(so up to 1023 inserts fit between neighbors).reorderArtifactinserts at(before + after) / 2; when the smallest gap drops below the internal normalize threshold (4),normalizeOrdinalsrebuilds gap-free(i+1) * 1024ordinals and the result flagsnormalized: true. - Nested collections are validated by
validateNestedParent, which walks the parent chain and refuses a move that would create acycleor aself-parent;buildBreadcrumbmaterializes the root→leaf chain. - Templates (
templates.ts):CollectionTemplaterecords are versioned and scoped (visibility: 'system' | 'tenant' | 'public' | 'private').instantiateTemplateclonesdefaultItems— keepingrequireditems plus any the customer chose — into a fully owner-controlled collection that preservesoriginTemplateId/originTemplateVersionfor analytics.templateVisibleToenforces the scope. - Import from another collection (
import-from-another.ts):importCollectioncopies a peer's collection into the importer's library. It is authorization-aware — it trusts agrantresolved by the sharing module and fails withgrant-mismatch,insufficient-tier(must satisfycopy), ortenant-isolation(cross-tenant copy requires an explicitallowCrossTenantCopy). The result preservesoriginCollectionId/originOwnerIdattribution while making the copy owner-controlled.
Smart collections: a real rule engine, not a label#
The docs say "smart collections (rule-based)"; the implementation
(collections/smart-collections.ts) is a real, typed rule engine that
materializes artifacts from across the platform on demand. Rules are AND-of-OR
(CNF): a SmartCollectionRules holds allOf: SmartCollectionRuleGroup[]
where every group must match (AND across groups) and any predicate inside a
group matches (OR within the group). It carries a limit: number | null and an
orderBy of
'recency-desc' | 'recency-asc' | 'relevance-desc' | 'alphabetical'.
SmartCollectionPredicate is a discriminated union of twelve predicate kinds:
| Predicate kind | Matches when |
|---|---|
artifact-kind-eq / artifact-kind-in |
candidate kind equals / is in the set |
tenant-id-eq / owner-id-eq |
tenant / owner matches |
tag-in / tag-contains |
candidate tags intersect / contain a tag |
created-at-gte / created-at-lte |
created within a time bound |
domain-eq |
domain equals one of lilith | tara | nisaba | veritas | metis | nyx | arete | sophia | iris | kalika |
synthetic-indicator-eq |
synthetic flag is true/false |
grounding-state-eq |
grounding state is grounded | partial | ungrounded | abstained | retracted-source |
text-contains |
case-insensitive substring on title or description |
Three functions form the engine:
validateSmartCollectionRules(rules)returns typed errors (empty-rule-set,empty-rule-group,invalid-limit,empty-text-query,empty-tag-list,unknown-artifact-kindwith group/predicate indices) before any evaluation runs.evaluateSmartCollection(rules, candidates)filters by the CNF predicate tree, sorts byorderBy, then applieslimit.materializeSmartCollectionForOwner(rules, candidates, scope)is the safe entry point: it enforces tenant/owner scoping before evaluation. Candidates must matchscope.tenantId; owned artifacts always pass; non-owned artifacts appear only if their${artifactKind}:${artifactId}key is inscope.sharedArtifactKeys(a set the sharing layer has already authorized). This keeps cross-owner leakage out of ad-hoc callers.
smartRulesEqual canonicalizes rule groups (sorting predicates and array
members into a stable string) so a library can dedupe equivalent smart
collections and short-circuit re-evaluation when nothing changed. The
grounding-state-eq and synthetic-indicator-eq predicates are what let a
customer build a smart collection like "everything grounded I saved this month,
excluding anything with a synthesized voice" — the curation layer reads the same
grounding vocabulary as Sophia Grounding and the
provenance signals from Isis Generation Control.
Sharing: visibility × permission, with a viewer resolver#
sharing/sharing.ts models sharing as two orthogonal axes. Visibility
(SHARE_VISIBILITY, least → most permissive) is
['private', 'named-users', 'link', 'public-profile']. Permission
(SHARE_PERMISSION) is ['view', 'comment', 'copy'] and is a rank ladder
(view 1 < comment 2 < copy 3), checked via sharePermissionSatisfies.
A SharePolicy aggregates namedUserGrants (per-user
{ userId, tier, grantedAt, grantedByOwnerId }), linkGrants (a hashed
tokenHash, a tier, optional expiry, optional revocation), and a
publicProfileSurface (enabled, profileSlug, tier). The core is
resolveSharePermission(input), which returns either
{ granted: true, tier, via } (via one of
owner | named | link | public-profile) or { granted: false, reason }. The
denial reasons (ShareDenialReason) are explicit and auditable:
via / outcome |
Conditions |
|---|---|
owner → copy |
viewer is the owner |
named → grant tier |
authenticated viewer is in namedUserGrants (works under named-users, link, and public-profile visibility) |
link → grant tier |
visibility is link, the provided token hash matches a non-revoked, non-expired grant |
public-profile → surface tier |
visibility is public-profile, the surface is enabled, and a profileSlug is set |
denied: private |
private collection, or a link/public visitor against a non-matching visibility |
denied: tenant-isolation |
authenticated viewer's tenant ≠ policy tenant |
denied: not-in-named-list |
named-users visibility, viewer not listed |
denied: link-token-mismatch / link-expired / link-revoked |
link grant fails token/expiry/revocation checks |
denied: public-profile-not-enabled / public-profile-missing-slug |
public visibility without the surface enabled or without a slug |
Two security properties are worth calling out. Link tokens are compared in
constant time: timingSafeEqual XOR-accumulates character codes and avoids an
early return on length mismatch, so token verification does not leak via timing.
(The module compares hashes; it documents that the caller generates the
crypto-secure token — a deliberate honest seam, not a stub.) And tenant
isolation is checked first for authenticated viewers, so a cross-tenant member
is denied before any visibility logic runs.
Supporting operations: validateSharePolicy rejects duplicate named-user
grants, duplicate token hashes, links that expire/revoke before creation, and a
public-profile-enabled policy without a slug. revokeLinkGrant stamps a
revocation time (constant-time matched); revokeNamedUserGrant drops a user;
and publicProfileSurfacedSlugs lists exactly the collections currently
surfaced on public profiles — the input a profile page is built from.
Public-profile surfacing requires the owner to have opted in (enabled and
profileSlug), which is the gating the prose calls out.
Annotations: a W3C/Hypothes.is selector chain#
The annotation system (annotations/annotations.ts) spans four reading surfaces
(ANNOTATION_SURFACES):
nisaba.edition · veritas.story · metis.lesson · tara.transcript
and four kinds (ANNOTATION_KINDS): highlight, note, thread-comment,
citation. Spans use the same selector-chain model as W3C Web Annotations /
Hypothes.is: an AnnotationTarget pairs a TextQuoteSelector (exact,
prefix, and suffix) with a TextPositionSelector (start and end),
giving both exact recovery and resilience to small edits.
validateAnnotation enforces kind-specific rules
(highlight-without-target-span, note-without-body,
citation-without-target, thread-comment-without-parent,
thread-comment-without-body, thread-comment-with-span,
invalid-citation-version, invalid-target-position, tenant-mismatch).
Thread durability is checked across a batch by
validateAnnotationThreadIntegrity, which catches thread-parent-not-found,
cross-tenant/cross-owner parents, non-threadable parents (you can only reply to
a note or another thread-comment), target mismatches (replies stay on the
same artifact/version), and parent cycles.
The most domain-specific piece is selector-chain recovery.
resolveAnnotationAnchor(annotation, currentText) re-anchors a saved annotation
against possibly-edited text using a strict precedence:
exact— the saved position still slices toquote.exact. Use it.context—prefix + exact + suffixoccurs exactly once in the new text. Anchor inside it.quote-only—exactalone occurs exactly once (lossy). Use it.- unresolved — return
nullrather than guess.
The single-occurrence checks are what prevent silent mis-anchoring after an
edit: if the context or quote appears twice, the function declines rather than
picks the wrong span. buildAnnotationThread constructs the reply tree
(excluding deleted annotations unless they have living children, preserving
thread cohesion and guarding against cycles), and deriveBacklinks materializes
the cross-artifact backlinks the prose mentions — for every non-deleted
citation, a record on the target artifact pointing back to the citing
annotation.
Export is real and in three formats:
exportAnnotationsAsJson— canonical, stable-sorted by id (so diffs are meaningful),{ version: 1, annotations }.exportAnnotationsAsMarkdown— grouped by artifact, chronological within each, human-readable per kind (highlights show the quote, notes the body, citations the target).exportAnnotationsAsCsv— RFC-4180 CSV for analytics, one row per annotation, fields with commas/quotes/newlines escaped (internal"doubled), deleted annotations excluded so the export matches what the reader actually sees.
Bookmarks, reading progress, and time-to-finish#
bookmarks/bookmarks.ts backs "bookmark and reading-list with reading progress,
time-to-finish estimates, and resume state across devices." A Bookmark holds
the artifact ref plus an array of per-device cursors (DeviceCursor): a
deviceId, a ReadLocator, a fraction in [0,1], and a
lastSeenUnixSeconds. A ReadLocator is one of three kinds
(READ_LOCATOR_KINDS): text-offset (characterOffset), timecode
(seconds), or page-section (page and optional sectionId) — so the same
model locates a position in text, audio/video, or paginated content.
validateBookmark checks fraction range, empty/duplicate device ids, locator
kind, and per-locator position validity.
The cross-device logic is concrete:
updateCursorreplaces a device's cursor but rejects a stale or regressive update (older timestamp, or same timestamp with a lower fraction) so a slow-syncing device cannot roll back progress.resumeCursorreturns the most recently active device's cursor — the "where did I leave off on my phone?" answer.farthestCursorreturns the most advanced cursor (highest fraction, ties broken by recency) — the basis for completion and remaining-time math.
Time-to-finish combines two signal sources, exactly as documented in the
module. computePace builds an EWMA of the customer's recent pace over a
default 14-day window with a 7-day half-life (decay = ln2 / (7·86400)), so
recent activity dominates without making the estimate jumpy; it returns
unitsPerMinute and a sampleCount. estimateSecondsToFinish then uses the
farthest cursor to compute remaining units and divides by the measured pace —
falling back to a caller-supplied defaultUnitsPerMinute when there are no
samples. summarizeReadingListProgress rolls this up across a list:
totalItems, completedItems, an overallFraction, a secondsToFinish
(Infinity when any item lacks a unit count, an honest "can't estimate" rather
than a fabricated number), and a resume pointer to the most-recently-touched
item.
"Sync across devices" is the honest seam. The merge logic (
updateCursor,resumeCursor,farthestCursor) is real and deterministic. What this library does not do is move bytes between devices: there is no transport, no server. Cross-device sync — actually persisting and replicating cursors — lives outside this library, which is why the completeness audit rates the persisted end-to-end journeys PARTIAL.
Share cards: embeddable, with indicators that survive#
share-cards/share-cards.ts builds self-contained, embed-friendly cards for a
single artifact. SHARE_CARD_KINDS covers veritas.story, veritas.claim,
nisaba.passage, nyx.sky-event, tara.meditation, metis.module, and
metis.lesson-module. A ShareCard carries three signal blocks that must
never be stripped during embed:
ProvenanceBlock—sourceAttribution,authorIds,evidencePackId,citationTrailId,publishedAtUnixSeconds.GroundingBlock—state(grounded | partial | ungrounded | abstained | retracted-source),citationCount,retrievalMethods(bm25 | dense | hybrid | graph | structured | tool | hierarchical),retractedSourceCount.SyntheticIndicatorBlock—voiceSynthesized,avatarSynthesized,textSynthesizedplus thepersonaId/voiceProfileId/avatarPackIdresources backing them.
validateShareCard enforces the integrity of those signals: it rejects an
unknown kind, an empty or non-http(s) canonicalUrl, a negative citation count,
a retracted-source state whose retractedSourceCount is 0 (and vice versa),
and a *Synthesized flag set without its backing resource id
(synthetic-flagged-without-resource). You cannot claim a synthetic voice
without naming the voice profile.
The rendering is implemented, not deferred. renderShareCardHtml(card)
produces an <article class="oshun-card"> with the title, summary, a provenance
line, and visible badges for the grounding state (state · N cites) and
each synthetic indicator (synth-voice / synth-avatar / synth-text). Every
author-controlled field is HTML-escaped via escapeHtml to prevent XSS, and the
full signal triple is embedded in an inline <script type="application/json">
that is escaped for safe HTML-script context. The badges are deliberately
encoded so they survive screenshots, RSS scrapes, and reader-mode
extractors. renderOEmbed(card) wraps the same HTML in a real
OEmbedResponse (version: '1.0', type: 'rich', provider_name: 'Oshun',
540×320, cache_age 3600s) and adds extension fields oshun_provenance,
oshun_grounding, oshun_synthetic so that external embedders are forced to
either render the indicators or reject the embed. Two parity helpers,
extractIndicatorsFromHtml and extractSignalsFromHtml, read the badges and
the JSON back out — so a stripping attempt can be detected by a failed
round-trip.
Version awareness: "this passage was updated since you saved it"#
version-awareness/version-awareness.ts backs the customer-facing prompt
literally. The inputs are a CustomerSavedRef (artifactId,
savedAtVersion: number | null, savedAtUnixSeconds) and the artifact's
current head version.
evaluateStaleness(saved, currentVersion)returns aStalenessVerdict. AsavedAtVersionofnullmeans "intentionally tracks latest" → never stale. Otherwise,currentVersion > savedAtVersionis stale, and the verdict reportsversionsBehind. (This is the consumer of thepinnedVersionfield onCollectionArtifactRef.)computePassageDiff(before, after, contextLines = 2)computes a line-oriented LCS (Myers-style) diff suited to prose. It returnsDiffLine[]taggedcontext | added | removedwith old/new line numbers, then trims tocontextLinesof surrounding context on each side of a change hunk — exactly the shape a diff UI renders directly.buildVersionAwarenessReport(input)assembles the fullVersionAwarenessReport: the saved and current versions,versionsBehind, theintermediateSummaries(VersionSummarychange notes filtered to this artifact and the version range, sorted ascending), and the rendereddiff. This is the "what changed since you saved this" narrative.
Because the diff is a genuine LCS — not a naive line-by-line compare — it correctly handles insertions, deletions, and moved-around prose without spurious "everything changed" noise.
Iris notebook is a first-class memory scope#
The architecture text ties curation notebooks to Iris memory, and that link is
real at the Iris layer. IrisMemoryScope
(libs/oshun/memory-iris/src/types.ts) is an 11-value union:
assistant_profile · session · scene · pose · conversation · domain
cross_domain · notebook · operator_copilot · tenant · admin_review
So notebook is a first-class scope — not merely "alongside
profile/session/operator/tenant," but one peer among eleven (and the canonical
names are assistant_profile and operator_copilot, stylized differently in
older prose). The Iris adapter (memory-iris/src/adapter.ts) enforces that a
notebook-scoped memory write requires a notebookId, warning
"Notebook-linked memory requires notebookId to prevent cross-notebook recall" —
the guard that keeps one notebook's recall from bleeding into another. The
recall budget for the notebook surface is also distinct (the notebook surface
gets a far higher per-recall budget than the assistant or shell surfaces). The
binding from a curation NotebookCollection into an Iris notebook scope,
however, is platform wiring that lives outside @oshun/customer-curation — see
Iris Memory and Identity.
How the pieces compose: a save-and-share walkthrough#
- A member reading a Veritas story highlights a span. The reading surface
builds an
Annotationwith aTextQuoteSelectorandTextPositionSelector;validateAnnotationpasses; it is stored. - They save the story into a
notebookcollection. ACollectionArtifactRefis appended withpinnedVersion: 7(freeze) ornull(track latest) and anordinal;validateCollectionconfirms no duplicate, valid ordinal, tenant match. - They reorder items by dragging;
reorderArtifactupdates only the moved item's fractional ordinal, normalizing if gaps collapse. - They share the notebook via link with
commentpermission. ASharePolicygains alinkGrant(hashed token, tiercomment);validateSharePolicypasses. A recipient hits the link;resolveSharePermissionconstant-time matches the token and returns{ granted: true, tier: 'comment', via: 'link' }. - To share a single story publicly, they generate a
ShareCard;validateShareCardconfirms the provenance/grounding/synthetic invariants;renderOEmbedproduces the embeddable response with indicators intact. - Weeks later the story is edited to version 9.
evaluateStalenessflags the pinned save as stale (versionsBehind: 2);buildVersionAwarenessReportsurfaces the diff and the intermediate change summaries.
Every step above is pure logic in @oshun/customer-curation. The steps that
require durability — persisting the annotation, the collection, the share
policy; replicating the reading cursor to another device; propagating a deletion
as a tombstone; writing an audit-log entry — are outside this library and
are the PARTIAL part of the audit's library-save-collection-share rating. That
is the honest division of labor: contract-complete logic, thinner end-to-end
persistence.
Tests#
The behaviors above are exercised by src/customer-curation.test.ts, covering
sharing-permission isolation (including tenant isolation and link
expiry/revocation), public-profile gating, annotation durability and selector
re-anchoring across edits, smart-collection CNF evaluation and scoping, ordering
normalization, version-diff correctness, and reading-progress/resume merge
semantics. The validators are tested by asserting the exact typed error codes,
and the share-card renderers by round-tripping indicators back out of the HTML.
Related#
- Customer Curation — the architecture deep-dive on the same surface (shipping vs. spec, where the logic lives).
- Iris Memory and Identity — the
notebookmemory scope and recall budgets. - Search, Discovery, Recommendations, and Knowledge Graph — reads collections, notebooks, and saved searches back.
- Keep, Share, Shareability, Takedown, and Lineage — the Living Scenes "keep and share" counterpart.
- Sophia Grounding and Isis Generation Control — the grounding-state and synthetic-indicator vocabularies share cards and smart collections read.
- Privacy, Consent, Data Portability, and User Controls — annotation/collection export and customer data controls.
- Subsystem Glossary and Product Surfaces — orientation; hub: ../features.md.