# Privacy, Consent, Data Portability, and User Controls

Privacy in Oshun V1 spans consent, retention, residency, deletion, export,
operator access, and disclosure across the customer, creator, tenant, and
institutional contexts. It is the user-facing half of the governance area: where
[Review, Compliance, and Trust & Safety](./review-trust-safety.md) governs what
the product does _to_ content, this surface governs what the product may do
_with_ a person's data — and gives that person the controls to grant, withdraw,
inspect, export, and erase it. The deterministic core lives in `@oshun/privacy`
(`libs/oshun/privacy/`); its modules are pure-function state machines and
taxonomies, with the actual enforcement (live persistence, real fan-out across
domains, immutable audit storage) supplied by the runtime that composes them.
The backlog for this area is **§22**; the feature hub is
[../features.md](../features.md); the architecture companions are
[Security, Privacy, and Compliance](../architecture/security-privacy-compliance.md)
and [Trust & Safety and Privacy](../architecture/trust-safety-and-privacy.md).

## How This Is Built — Real vs. Aspirational

The honest boundary matters here, so it is stated up front. The `@oshun/privacy`
library is **implemented and verified** as a set of deterministic taxonomies and
state machines: the consent taxonomy and transition/audit logic
(`consent/consent.ts`), the export-bundle builder with a SHA-256 integrity
manifest (`export-deletion/export.ts`), the deletion state machine with
retention constants and legal-hold gating (`export-deletion/deletion.ts`), the
residency router with cross-region-blocked verdicts and propagation tracking
(`residency/residency.ts`), the DSAR and operator-access workflow
(`dsar/dsar.ts`), the privacy-center model
(`privacy-surface/privacy-surface.ts`), and the regulatory regime / breach /
audit-completeness logic (`compliance/compliance.ts`). These are exported
through the package root `@oshun/privacy` (`libs/oshun/privacy/src/index.ts`).

What these pure cores deliberately do **not** do is touch storage themselves.
The one intentional exception is the account-deletion fan-out:
`deletion-erasers/generated-artifact-eraser.ts` reaches the Isis
`GeneratedOutput` store through a lazy, injectable seam to hard-delete a
subject's generated outputs (`deleteMany` by `ownerId`), returning the row count
rather than fabricating a result. Everywhere else, the cores describe and
validate transitions; the runtime is responsible for real classifiers, live
audit-platform persistence (`@oshun/audit-platform` owns immutable storage), and
real DSAR fan-out across domains. Consent and memory ownership are shared with
Iris — see [Iris Memory and Identity](./iris-memory-identity.md) — and the
crisis-frame cascade that privacy and safety both depend on is described in
[Lilith Persona Policy](./lilith-persona-policy.md) and
[Review, Compliance, and Trust & Safety](./review-trust-safety.md). Nothing in
this library reads as result-faking: the cryptographic-shred flag, for example,
is recorded verbatim from the executor that performed the deletion and can never
claim a shred that did not happen.

## Consent Taxonomy

Consent (§22.1) is organized into nine **consent families**, enumerated in
`CONSENT_FAMILIES` (`consent/consent.ts`): `memory`, `voice`, `avatar`,
`synthetic-media`, `notifications`, `privacy-surface`, `support`, `research`,
and `educational-context`. Each family carries its own purpose vocabulary so a
consent is never a vague all-or-nothing toggle — it names exactly what is being
permitted.

| Family                | Real key shape / purpose enum                                                                                                                                                                                 | Notes                                                                                      |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `memory`              | `MEMORY_SCOPES` = `profile` \| `session` \| `notebook`; `MEMORY_SENSITIVITY` = `baseline` \| `sensitive`                                                                                                      | Scope × sensitivity; sensitive defaults off. Owned jointly with Iris.                      |
| `voice`               | `VOICE_CONSENT_PURPOSES` = `recording`, `cloning-own-voice`, `cloned-voice-in-personas`, `voice-data-analytics`                                                                                               | Each separately granular.                                                                  |
| `avatar`              | `AVATAR_CONSENT_PURPOSES` = `likeness-capture`, `generated-likeness`, `animation-rights`, `tenant-scoped-distribution`                                                                                        | Likeness rights are split from distribution rights.                                        |
| `synthetic-media`     | `SYNTHETIC_MEDIA_PURPOSES` = `ai-generation-on-uploads`, `derivative-works`, `tenant-publication`                                                                                                             | Generation on user inputs vs. publishing the result.                                       |
| `notifications`       | per `channel` (`in-app`, `push`, `email`, `sms`, `voice`, `webhook`) × `domain` × `severityMin` (`info`/`standard`/`important`/`critical`)                                                                    | Granular down to per-domain, per-severity.                                                 |
| `privacy-surface`     | `PRIVACY_SURFACE_PURPOSES` = `research-data-sharing`, `analytics-granularity-coarse`, `analytics-granularity-fine`, `third-party-processor-inclusion`, `training-data-inclusion`, `cross-tenant-data-sharing` | The high-sensitivity surfaces (training-data inclusion, cross-tenant sharing) default off. |
| `support`             | `SUPPORT_CONSENT_PURPOSES` = `agent-screen-share`, `agent-memory-access`, `session-recording`                                                                                                                 | Granted only for the troubleshooting window.                                               |
| `research`            | per `studyId`                                                                                                                                                                                                 | Per-study granularity; never a blanket opt-in.                                             |
| `educational-context` | `EDUCATIONAL_PURPOSES` = `assignment-data-sharing-with-teacher`, `assignment-data-sharing-with-institution`, `standards-reporting`                                                                            | Institutional Metis-specific; see [Metis — Education and Tutoring](./domain-metis.md).     |

Each consent is keyed by a discriminated-union `ConsentKey`, and every key
serializes to a stable, human-readable string via `canonicalKeyString` — for
example `memory:profile:sensitive`, `voice:cloning-own-voice`,
`privacy-surface:training-data-inclusion`, or
`notifications:push:tara:important`. That canonical string is what the audit
log, the withdrawal cascade, and the per-feature reveal all reference, so a
single consent is identifiable across every surface that touches it.

> **Reconciling the architecture doc.** Some companion text attributes consent
> ownership solely to Iris (`@oshun/memory-iris`), stating that "every consent
> change emits a `ConsentRecord` event." That is only half the picture: a full
> consent taxonomy _and_ the `ConsentRecord` type also live here in
> `libs/oshun/privacy/src/consent/consent.ts` (`CONSENT_FAMILIES`,
> `MEMORY_SCOPES`, the per-family purpose enums, the transition validator). Iris
> owns the _memory_ consent surface and recall; the privacy lib owns the
> cross-family consent core. Both are real, and they share the same record
> shape.

## Granularity, Default State, and Withdrawal

Consent granularity (§22.2) is enforced, not merely promised. Three rules are
encoded directly in `consent/consent.ts`:

- **Per-feature opt-in with no bundled sensitive consent.** Every key is
  independently toggleable. `validateConsentRecord` rejects any `ConsentRecord`
  whose key is sensitive (`isSensitiveKey`) _and_ whose `bundledWith` array is
  non-empty, returning a `bundled-sensitive-consent` error. There is no "accept
  all" that can sweep a sensitive category into a bundle.
- **Default state derived from sensitivity.** `defaultState(key)` returns
  `denied` for every sensitive case — sensitive memory, all of `voice` /
  `avatar` / `synthetic-media`, `support`, `research`, `educational-context`,
  and the sensitive `privacy-surface` purposes (`research-data-sharing`,
  `analytics-granularity-fine`, `third-party-processor-inclusion`,
  `training-data-inclusion`, `cross-tenant-data-sharing`). It returns `granted`
  only for baseline memory, coarse analytics, and transactional `notifications`.
  Training-data inclusion and cross-tenant sharing are off until the user
  explicitly opts in.
- **Audited transitions.** `transitionConsent` is the only sanctioned way to
  change a consent. It computes `priorState` from the current record (`granted`
  / `denied` / `unset`), rejects an inconsistent prior (a mismatched user,
  tenant, or key produces `prior-state-inconsistent`), requires a non-empty
  `reasonCode`, and on success returns both the new `ConsentRecord` _and_ a
  `ConsentTransitionAudit` carrying `keyString`, `priorState`, `newState`,
  `atUnixSeconds`, `actorId`, and `reasonCode`. Every state change therefore
  produces an audit entry by construction.

### Withdrawal cascade

Withdrawing a consent is not a single bit-flip; it propagates to everything
derived from the prior grant. `planWithdrawalCascade` builds a `WithdrawalJob`
whose steps each name a `WithdrawalCascadeKind` from `WITHDRAWAL_CASCADE_KINDS`:
`evict-from-memory`, `remove-cloned-voice-instance`,
`remove-cloned-avatar-instance`, `unpublish-derivative`,
`remove-from-training-set`, `remove-from-research-cohort`, and
`unshare-with-teacher`. The plan distinguishes **immediate** effects from
**propagating** ones exactly as the spec requires: when memory consent is
withdrawn, `effectiveImmediately` contains `evict-from-memory` (storage and
recall stop at once), while the remaining downstream artifacts become tracked
steps with `estimatedCompletionUnixSeconds`. The job's `etaUnixSeconds` is the
max across them, so the UI can show "withdrawal effective now; derivative
cleanup completes by …". Each step starts `pending` and advances through
`in-flight` → `completed` / `failed`, giving the privacy center a real job state
to render rather than an unverifiable "done."

## Region, Residency, and Routing

Residency (§22.3, `residency/residency.ts`) keeps a region's data on that
region's plane unless a sanctioned exception applies. Each launch region is a
`RegionDeclaration` with a `primaryPlaneId` and an optional `failoverPlaneId`,
and every governed resource carries a `ResidencyAssignment` recording its
`homeRegionId`.

The router `routeRead` returns one of three `ResidencyRoutingResult` verdicts:

- `{ route: 'primary', planeId }` — the caller is in the resource's home region
  and the primary plane is up.
- `{ route: 'failover', planeId }` — the caller is home, the primary is down,
  and a failover plane exists.
- `{ route: 'cross-region-blocked', reason: 'residency-violation' }` — the
  caller's region differs from the resource's home region (or the home region is
  unknown). This is a hard block, not a degraded read: data does not leave its
  region just because a caller asked from elsewhere.

Cross-region movement happens only through an explicit, two-party exception.
`CROSS_REGION_EXCEPTION_KINDS` enumerates the three lawful reasons:
`legal-hold`, `security-incident`, and `operator-explicit-with-consent`.
`approveCrossRegionException` enforces that the rationale is non-empty
(`rationale-empty`), that an `operator-explicit-with-consent` exception carries
a `customerConsentRecordId` (`consent-required`), and that the approver is not
the requester (`self-approval`). Once approved, the move is **tracked to
completion**: `startPropagationTracker` enumerates replica destinations,
`advanceReplicaState` walks each through `enqueued` → `in-flight` →
`completed`/`failed` (illegal transitions are rejected), and
`evaluatePropagationCompletion` reports `allTerminal` only when no destination
is still in flight or enqueued. `applyTrackerToExceptionRequest` flips the
request's `completionTracked` flag to `true` solely when every replica is
terminal — and the doc comment notes that this flag is exactly the signal the
compliance dashboard alerts on when it stays `false`.

Two further residency guarantees are concrete functions, not prose:
`residencyAwareCacheKey({ baseKey, homeRegionId })` prefixes the home region
(`{region}::{baseKey}`) so a cache entry from one region can never be served to
a caller in another, and `logSegmentForRegion(regionId, logName)` returns
`logs/{regionId}/{logName}` so logs are segregated by region by construction.
Subprocessor change notices are generated by `buildSubprocessorChangeNotices`,
which diffs a prior and current subprocessor list and emits `added` / `removed`
/ `purpose-changed` `SubprocessorChange` entries, each carrying a
`customerNotificationRequiredByUnixSeconds` deadline (per-tenant for
institutional contracts).

## Export, Deletion, and Retention

### Export bundles

Export (§22.4, `export-deletion/export.ts`) covers eleven `EXPORT_SCOPES`:
`profile`, `conversation-history`, `memory`, `notebooks`, `journals`,
`ritual-completions`, `claims-passages-saved`, `generated-artifacts`,
`audit-visible-to-user`, `support-history`, and `billing-receipts`. Three
`EXPORT_FORMATS` are supported — `json` (canonical), `csv` (structured scopes
only), and `markdown` (prose scopes, plus `profile`). `defaultFormatFor` picks
JSON for structured scopes and Markdown for prose scopes, and
`isFormatValidForScope` rejects, e.g., CSV for a free-text journal.

Every export ships with a tamper-evident integrity manifest. `buildExportBundle`
canonicalizes the bundle (sorting scopes and per-entry digests) and computes a
SHA-256 `bundleDigest` over bundle identity, scopes, format, and the entry list
(via `@noble/hashes`), recording
`{ version: 1, algorithm: 'sha-256', bundleDigest, perEntryDigests, producedAtUnixSeconds }`.
Each `ExportEntry` also carries its own `consentContext` — the granted/denied
state of the consents relevant to that data — so the export is self-describing
about what permissions governed the data it contains. `verifyExportBundle`
recomputes the digest and checks the per-entry manifest (catching
`missing-entry`, `digest-mismatch`, and `unexpected-entry`); callers must reject
on mismatch before serving the bundle to the data subject. This same
verification backs DSAR portability.

### Deletion state machine

Deletion (`export-deletion/deletion.ts`) is a strict state machine over
`DELETION_SCOPES` (`profile`, the three `memory.*` scopes,
`conversation-history`, `notebooks`, `journals`, `generated-artifacts`,
`full-account`). A `DeletionRequest` moves through the states `pending` →
`soft-deleted` → `hard-deleted`, with `cancelled` and `blocked-by-hold` as
off-ramps:

- `enqueueDeletion` validates the soft-delete window and checks active legal
  holds; if any apply, the request is created `blocked-by-hold` rather than
  `pending`.
- `advanceToSoftDelete` requires the `pending` state and that the execution time
  has arrived (`too-early` otherwise), and on success sets the state to
  `soft-deleted` and **issues a tombstone** (`tombstoneIssued: true`) to prevent
  re-creation.
- `advanceToHardDelete` requires `soft-deleted`, that the hard-delete deadline
  has passed, and that no legal hold has since attached (`blocked-by-hold`). It
  takes a `CryptographicShredOutcome` from the executor and records
  `cryptographicShredApplied` _verbatim_ — `applied` is true only when a real
  shred (encryption-key destruction) was carried out and confirmed; a row-level
  delete without key destruction reports `applied: false` with
  `reason: 'deletion-by-row-removal'`. The state machine never infers the flag,
  so it cannot claim a shred that did not happen.
- `cancelDeletion` reverses a `pending` or `soft-deleted` request.

### Soft-delete window and retention constants

The 30-day reversible window the prose describes is a real, bounded constant.
`SOFT_DELETE_DEFAULT_SECONDS = 30 * 86400`, and `validateSoftDeleteWindow`
clamps any tenant override between `SOFT_DELETE_MIN_SECONDS = 24 * 3600` (a UX
safeguard against accidental clicks) and `SOFT_DELETE_MAX_SECONDS = 90 * 86400`
(a regulatory cap on dangling, still-controlled data), returning `below-minimum`
/ `above-maximum` errors outside that band.

Per-data-class retention is the `RETENTION_DAYS` table, surfaced via
`retentionDaysFor(dataClass)`:

| Data class           | Retention                |
| -------------------- | ------------------------ |
| `raw-chat`           | 30 days                  |
| `summarized-profile` | `durable`                |
| `billing`            | `365 * 7` days (7 years) |
| `audit`              | `365 * 7` days (7 years) |
| `generated-artifact` | `per-artifact-policy`    |

Legal holds (`LegalHold`) suspend deletion and are evaluated by
`holdsApplicable`, which matches by `userId`, honors hold expiry, and treats a
`full-account` deletion as in-scope for any hold. A hold blocks both the enqueue
and the hard-delete steps, with operator audit recorded on the hold itself.

## Customer-Facing Privacy Surface

The privacy center (§22.5, `privacy-surface/privacy-surface.ts`) is a single
in-product surface. `buildPrivacyCenter` returns one `PrivacyCenterEntry` per
section, each with a `summary`, a `deepLinkPath`, and an `itemCount`. The eight
sections are exactly: `consents`, `memory`, `exports`, `deletions`, `residency`,
`operator-access-audit`, `subprocessors`, and `disclosures` — so a user sees
every consent, memory state, export, deletion, residency choice, subprocessor,
disclosure, _and_ an audit of operator access to their data, in one place. The
deep links are concrete (`/privacy/consents`, `/privacy/memory`,
`/privacy/exports`, `/privacy/deletions`, `/privacy/residency`,
`/privacy/audit`, `/privacy/subprocessors`, `/privacy/disclosures`).

Two further surface guarantees are enforced in code:

- **Plain-language reveal.** A `FeaturePrivacyReveal` answers "what this uses,
  where it goes, how to turn it off" and carries a `readingGradeLevel`.
  `checkRevealReadingGrade` rejects copy above the jurisdiction's ceiling —
  grade 8 for US/CA/UK/EU/BR, defaulting to 10 elsewhere — with
  `reading-grade-too-high`. Disclosure copy is also reviewed under Lilith tone
  policy ([Lilith Persona Policy](./lilith-persona-policy.md)).
- **Contextual, non-buried prompts.** `decideContextualPrompt` decides whether
  to surface a consent prompt at first relevant feature use. It returns
  `shouldPrompt: false` with a reason of `consent-not-required-for-feature`,
  `already-granted`, or `recently-dismissed` (re-showing only after
  `dismissalReshowSeconds` elapses), and `true` otherwise — so prompts appear in
  context, are remembered on dismissal, and are never buried in onboarding.

## Admin DSAR and Operator-Access Workflow

### DSAR state machine

Data-subject access requests (§22.6, `dsar/dsar.ts`) cover the six `DSAR_KINDS`:
`access`, `portability`, `rectification`, `erasure`, `restriction`, and
`objection`. A `DsarRequest` advances through the seven `DSAR_STATES`:
`received` → `identity-verified` → `scope-determined` → `in-execution` →
`completed`, with `rejected` and `restored` as terminal alternatives. The
requesting party is recorded as `requestingActorKind` ∈
`{ subject, authorized-representative, operator }`.

Eligibility is checked, not assumed. `checkEligibility` requires identity
verification (`identity-not-verified`) and validates the request kind against
the jurisdiction: the EU and UK support all six kinds, while the US supports
`access` / `portability` / `erasure`, and CA / BR support `access` /
`portability` / `rectification` / `erasure`. An unknown jurisdiction returns
`jurisdiction-not-supported`; an unsupported kind returns
`unsupported-kind-in-jurisdiction`. Execution is per-data-class:
`determineScope` records a `perClassCompletion` map, and `recordClassCompletion`
marks each scope `completed` / `unavailable` and flips the request to
`completed` only when every scope is terminal, attaching the export's
`integrityManifest` (bundle id and digest) for portability verification.

**Restore** is time-windowed and elevated. `checkRestoreEligibility` returns
`tenant-forbids-restore` when the tenant policy forbids it and
`elevation-required` when no `elevationApproverActorId` is present — a restore
never happens on a single operator's say-so.

### Operator access (non-DSAR)

Operator access to a subject's data outside a DSAR is governed by the same file.
`OPERATOR_ACCESS_REASONS` enumerates `support-investigation`,
`safety-investigation`, `privacy-review`, `research-cohort`, and
`legal-discovery`, and `SENSITIVE_OPERATOR_ACCESS` flags `legal-discovery` and
`research-cohort` as the categories requiring two-operator authorization. Every
`OperatorAccessRequest` carries a `reason`, a time-bound window
(`timeBoundStartUnixSeconds` … `timeBoundEndUnixSeconds`), the data it is scoped
to (`scopedToData`), a `notificationToSubjectByUnixSeconds` deadline (post-hoc
user notification within the compliance window where lawful), and an `auditId`.
`authorizeOperatorAccess` rejects an invalid or inverted time window
(`time-bound-invalid`), a sensitive access lacking a second approver
(`two-operator-required`), and a sensitive access where the approver is the
requester (`self-approval`). This is the privacy-side counterpart to the
operator surfaces described in
[Tenant, Institution, and Operator Toolkit](./tenant-toolkit.md) and
[Admin Products — Web and Mobile](./admin-products.md).

## Compliance, Disclosure, and Audit

### Regulatory regimes

Compliance (§22.7, `compliance/compliance.ts`) maps regions to regimes rather
than flattening everything into one list. `REGULATORY_REGIMES` enumerates
`GDPR`, `CCPA`, `CPRA`, `LGPD`, `PIPEDA`, `US-State-Privacy`, `FERPA`, `COPPA`,
and `UK-DPA-2018`, and `regimesFor(region)` resolves the applicable set:

| Region | Applicable regimes                         |
| ------ | ------------------------------------------ |
| `EU`   | GDPR                                       |
| `UK`   | GDPR, UK-DPA-2018                          |
| `US`   | CCPA, CPRA, US-State-Privacy, FERPA, COPPA |
| `CA`   | PIPEDA                                     |
| `BR`   | LGPD                                       |

Notably, the US mapping includes the education- and child-privacy regimes
**FERPA** and **COPPA** — relevant to institutional Metis — which the prose
feature bullet does not name. An unmapped region resolves to the empty set.

### Mandatory disclosures

`MANDATORY_DISCLOSURE_KINDS` enumerates the five required disclosures:
`synthetic-content-disclosure` (per Lilith), `ai-use-disclosure`,
`third-party-processor-list`, `breach-notice`, and `retention-policy`. Each
`MandatoryDisclosure` carries `perLocaleCopy`; `resolveDisclosure` selects the
active (effective, non-archived) disclosure for a region/kind and walks the
BCP-47 locale hierarchy (`de-CH` → `de` → first available) to find copy,
returning `no-active-disclosure` or `no-locale-copy` when it cannot.
`requireDisclosureCoverage` returns the missing `(region, kind)` pairs across
the active launch regions — the compliance dashboard blocks a release while that
list is non-empty. `archiveDisclosure` supersedes an old disclosure by stamping
`archivedAtUnixSeconds`; it never deletes, so the historical record stays
reconstructable.

### Breach response and audit completeness

The breach runbook is the seven-stage sequence `BREACH_RESPONSE_STAGES`:
`detect` → `contain` → `assess` → `notify` → `remediate` → `postmortem` →
`disclosure`. `advanceBreachStage` enforces strictly sequential progression
(`illegal-transition` on any skip or out-of-order timestamp).
`regulatoryDeadlineSecondsFromNow` computes the notification clock from the
region's _strictest_ regime — 72 hours for GDPR / UK-DPA-2018 / CCPA / CPRA /
LGPD / PIPEDA, 168 hours for US-State-Privacy / FERPA / COPPA, defaulting to 168
hours for an unmapped region.

Audit completeness is measured, not assumed. `AUDITABLE_EVENT_KINDS` lists the
five event classes that must each be audited: `consent-transition`,
`operator-access`, `deletion`, `export`, and `cross-region-move`.
`evaluateAuditCompleteness` divides audited events by observed events per kind
and returns `allCovered: true` only when the minimum coverage across all kinds
is ≥ 1 — i.e., every consent transition, operator access, deletion, export, and
cross-region move has an audit event. Immutable storage of those events is owned
by `@oshun/audit-platform` per the architecture; this module computes the
coverage signal over them.

## Where Billing and Crisis Cross This Surface

Privacy does not live alone in the governance area. Two adjacent mechanisms are
worth naming because they cross this surface directly.

**Entitlement single-source-of-truth.** The billing core
(`@oshun/billing-support`, `libs/oshun/billing-support/`) defines six
`ENTITLEMENT_CLASSES` (`free`, `starter`, `plus`, `pro`, `scholar`,
`institutional`), and `billing-aje-bridge.ts` collapses them onto the three
canonical `OshunEntitlementTier` values via `TIER_BY_CLASS` (`free → free`;
`starter`/`plus → pro`; `pro`/`scholar`/`institutional → premium`).
`entitlementTierForSubscription` falls back to `free` whenever a subscription is
not in an entitling state (`trial`/`active`/`grace`/`restored`), so a lapsed
payment de-entitles immediately. `applyPaymentSettlementToSubscription` advances
the subscription state machine from a settled Aje payment
(`confirmed`/`failed`/`refunded`), which is how a confirmed crypto payment
becomes an entitlement change. This is the entitlement model that gates
voice/avatar generation and the institutional gradebook; details live on
[Support, Entitlements, Billing, and Customer Operations](./support-billing.md)
and
[Crypto Payments — Non-Custodial Entitlement Settlement](./crypto-payments.md).

**Crisis-frame cascade.** When a crisis is detected, a single activation must
reach every contemplative surface. The cascade
(`libs/oshun/trust-safety/src/crisis/crisis-frame-cascade.ts`) publishes
`LILITH_CRISIS_FRAME_ACTIVATED_EVENT = 'lilith.crisis_frame.activated'` and fans
out to `CRISIS_FRAME_SURFACES` =
`['psyche', 'lilith-video', 'tara', 'iris', 'assistant']`, carrying
non-overridable `haltSynthesis` and `suspendMemoryWrites` directives. The domain
stays free of `@oshun/event-bus` (it uses a `CrisisFramePublishPort` only);
`crisis-frame-worker.ts` binds the real Redis bus and re-projects each event.
This is the privacy-relevant link because a crisis frame **suspends memory
writes** — it overrides whatever memory consent would otherwise permit. Full
detail is on [Review, Compliance, and Trust & Safety](./review-trust-safety.md)
and [Lilith Persona Policy](./lilith-persona-policy.md).

## Tests and Verification

The library is covered by Vitest suites under `libs/oshun/privacy/src/` (e.g.
`privacy.test.ts`, `deletion-erasers/generated-artifact-eraser.test.ts`). The
verification targets match the spec's test list: consent enforcement and the
bundled-sensitive rejection, the withdrawal cascade plan, deletion completeness
with tombstones and the verbatim shred outcome, restore authorization, residency
routing and cross-region propagation completion, audit completeness, DSAR
end-to-end across the seven states, the export integrity manifest round-trip,
and the breach runbook stage ordering. The `generated-artifact-eraser` test
exercises the real Isis `deleteMany` path against the live local DB (two of
three owners' rows deleted, the third left intact), confirming that the one
place the privacy engine touches domain persistence does so correctly.
Cross-domain DSAR fan-out and live audit-platform persistence are the runtime's
responsibility and are integration concerns beyond this pure core.

## Related

- [Iris Memory and Identity](./iris-memory-identity.md) — the memory consent
  surface and recall, jointly owned with the consent core here.
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md) — policy
  taxonomy, severity SLAs, decisions, and the crisis-frame cascade.
- [Support, Entitlements, Billing, and Customer Operations](./support-billing.md)
  — entitlement classes, dunning, metered billing, and support routing.
- [Crypto Payments — Non-Custodial Entitlement Settlement](./crypto-payments.md)
  — how a settled Aje payment advances the subscription and entitlement.
- [Lilith Persona Policy](./lilith-persona-policy.md) — disclosure tone policy
  and synthetic-content disclosure.
- [Tenant, Institution, and Operator Toolkit](./tenant-toolkit.md) — operator
  access and institutional data-sharing context.
- [Architecture, Platform Foundations, and Security](./platform-foundations-and-security.md)
  — where residency, audit storage, and the event bus sit in the platform.
- Companion docs:
  [Security, Privacy, and Compliance](../architecture/security-privacy-compliance.md)
  and [Trust & Safety and Privacy](../architecture/trust-safety-and-privacy.md).
- Backlog: **§22**; feature hub: [../features.md](../features.md).
