# Scene Safety, Determinism, Provenance, and Cue Privacy

This page documents the governance spine of Living Scenes: the
photosensitive-epilepsy (PSE) / strobe killer, the continuous tone-and-motion
caps, the mid-stream crisis frame, the deterministic-render release gate, the
four redundant provenance channels, and the two-layer (private /
public-redacted) cue-privacy model. It serves the contemplative product, the
AAA-Creator and operator authoring consoles, and trust-and-safety review; in the
V1 stack it sits _on top of_ the Yemaya generation substrate and the Lilith
policy substrate. It is the most genuinely implemented part of the Living Scenes
story because governance is decision logic, not media rendering. Most of what
follows is real, tested code in `libs/yemaya/living-scenes-runtime/src/` and
`libs/oshun/trust-safety/src/`; where a piece is a fail-loud seam awaiting an
upstream model or a real transcoder, this page says so plainly. See
[Living Scenes — Concept and Customer Promise](./living-scenes-overview.md) for
the customer framing and [Lilith Persona Policy](./lilith-persona-policy.md) for
the policy substrate this composes on.

## The unit of safety: hard gates the user cannot turn off

A Living Scene is a continuous AI-generated video that _listens_ — the user
shapes it live by voice, tap, or text. That responsiveness is exactly why the
safety story is built as a set of **hard, non-overridable gates** rather than
preferences. Two of those gates — the PSE/strobe killer and the crisis frame —
cannot be relaxed by the user, the persona, the admin copilot, or any
entitlement tier. The rest of the page works outward from those two.

## Photosensitive-Epilepsy (PSE) / Strobe Detector — the hard kill

`detectPseStrobeHazard` in
`libs/yemaya/living-scenes-runtime/src/safety/pse-strobe-detector.ts` is a real
flash-rate analysis after **WCAG 2.3.1 / the Harding criteria** (not, as an
earlier draft of the features doc claimed, ITU-R BT.1702-2; that standard is
_not_ referenced in the code and was an imprecise citation). It runs on the live
frame-luminance sequence and returns a `PseStrobeResult`:

```ts
interface PseStrobeResult {
  safe: boolean; // false ⇒ caller MUST block (non-overridable)
  maxGeneralFlashesPerSecond: number;
  maxRedFlashesPerSecond: number;
  hazards: readonly string[]; // e.g. "general-flash 5/s exceeds 3/s"
}
```

The algorithm is concrete and load-bearing, so its constants are the safety
contract:

| Constant                 | Value | Meaning                                                                                          |
| ------------------------ | ----- | ------------------------------------------------------------------------------------------------ |
| `LUMINANCE_FLASH_DELTA`  | `0.1` | A general flash needs an opposing-luminance swing ≥ 10% of max.                                  |
| `DARK_THRESHOLD`         | `0.8` | The darker side of the opposing pair must be below 0.8 (a dark-to-bright reversal).              |
| `RED_FLASH_DELTA`        | `0.2` | A red flash is the same test on the saturated-red fraction, swing ≥ 0.2.                         |
| `FLASH_LIMIT_PER_SECOND` | `3`   | **More than** three general flashes _or_ three red flashes in any one-second window is a hazard. |

The detector walks the luminance series, counts a "flash" at each direction
reversal whose swing from the previous significant extremum clears the delta
(and, for general flashes, whose darker side is below `DARK_THRESHOLD`), then
takes the maximum number of such flashes inside any sliding window of `fps`
frames (one second). If either the general or red count exceeds three, `safe` is
`false`.

**Why a hard kill.** The module header is explicit: when `safe === false` the
caller _must_ block the frame sequence — "no tier, flag, or operator setting may
override it." This is the detector the V1 audit found missing entirely (a Living
Scene could emit a flashing sequence with nothing to detect or block it), so the
implementation is the genuine fix. In the live runtime the kill manifests as an
immediate fade-to-still plus a plain operator-voice notice; the same detector is
re-run in the nightly re-detection sweep (see the takedown disposition below).
Actual _pixel-level_ luminance/motion extraction from rendered frames is the one
upstream piece — the detector consumes a `PseFrame[]` of relative luminance and
red fraction; the renderer is responsible for producing those numbers.

## Continuous tone, luminance, contrast, and motion caps

Beyond the strobe killer, the runtime enforces per-persona-class budgets on
luminance, contrast, and motion density, plus forbidden-frequency flicker bands
and per-domain narrative-cadence bands. A Tara meditation budget is dramatically
lower than a Veritas explainer budget; templates declare the cap, and the
runtime enforces it. When the runtime intervenes, it emits a
`scene.policy-intervention` event (see Scene Events below) tagging the
`interventionKind` — `pse-strobe-cap`, `luminance-cap`, `motion-cap`,
`tone-band-shift`, or `persona-cap` — with the pre- and post-clamp values, so
the intervention is observable rather than silent.

## Live-cue policy gate and the prompt-injection defense

Every voice or text cue is policy-checked before it is allowed to queue. The
defense begins in the cue parser
(`libs/yemaya/living-scenes-runtime/src/cues/cue-parser.ts`): raw customer text
**never reaches the segment generator**. Instead, `sanitizeForCueParse` strips
quotes, control characters (U+0000–U+001F and U+007F), code-fence markers, and
HTML tags, collapses whitespace, and truncates to **240 characters**; the
sanitized head token is then matched against the small, deliberate
`CUE_VERBS_VOCABULARY` (`linger`, `advance`, `morph`, `recall`, `shift-style`,
`shape-by`, `save-moment`, `abandon`, `kill`). Unrecognized input returns an
`unrecognised` verdict (it falls through to a Lilith pre-screen, never to the
generator). For the voice channel, an ASR confidence below
`LOW_CONFIDENCE_THRESHOLD = 0.6` yields a `low-confidence` verdict so a misheard
cue is not acted on. Tap cues are policy-free by construction. The constrained
verb mapping — not a content filter — is the structural prompt-injection
defense: there is no path by which arbitrary text becomes a generation prompt.

## Mid-stream crisis frame and cross-surface cascade

A crisis signal — detected in conversation, in a live cue, or in a kept-artifact
replay viewer — supersedes persona, lineage, pedagogy, grounding, and any
user/admin override. The decision shape lives in
`libs/yemaya/living-scenes-runtime/src/cues/crisis-classifier.ts`:

- `LILITH_CRISIS_LABELS` = `self-harm-imminent`, `self-harm-ideation`,
  `harm-to-others`, `severe-dissociation`, `panic-acute`, `overwhelm`,
  `distress`, `safe`.
- `decideCrisisAction` maps the top label to a severity: the imminent set
  (`self-harm-imminent`, `harm-to-others`) → `imminent`; the severe set
  (`self-harm-ideation`, `severe-dissociation`, `panic-acute`) → `severe`;
  `overwhelm` / `distress` → `moderate`; `safe` → `continue`.
- `checkLocaleParity` enforces that the **imminent-class false-negative rate
  stays ≤ 0.02** (alongside a macro-F1 floor) per locale — a miss on the most
  dangerous class is the failure the parity gate exists to catch.

The module owns the taxonomy and the decision shape; the **classifier model
itself is downstream** (the header is candid: "the actual classifier model is
downstream; this module owns the decision shape + taxonomy mapping"), so any
compatible classifier output can be plugged in.

When a frame is activated, the propagation is real and cross-surface. In
`libs/oshun/trust-safety/src/crisis/crisis-frame-cascade.ts`, a single event
constant `LILITH_CRISIS_FRAME_ACTIVATED_EVENT = 'lilith.crisis_frame.activated'`
fans out to **five surfaces** — `CRISIS_FRAME_SURFACES = psyche`,
`lilith-video`, `tara`, `iris`, `assistant`. The published
`LilithCrisisFrameActivatedEvent` carries two non-overridable directives,
`haltSynthesis: true` and `suspendMemoryWrites: true`, so a per-surface
projector cannot soften them. The domain stays free of the event bus (it talks
to a `CrisisFramePublishPort`); `crisis-frame-worker.ts` binds the real Redis
bus. `consumeCrisisFrameActivation` attempts each projector independently and
records every outcome (`projected | skipped | failed`) so a recovery sweep can
re-project failures — one surface failing to enter its frame never blocks the
others. This is exactly the doc's "fade-to-still + plain operator voice +
suspend Iris memory writes + open a safety-incident record," now with the named
event and the five-surface fan-out (Iris memory-write suspend, Tara scheduling,
video policy).

The crisis frame also reaches the scene event feed as a `scene.crisis-frame`
event (see below), and any kept artifact whose render history touched a crisis
frame is forced `private-only` by the shareability matrix.

## Determinism, the Render Envelope, and re-render-from-score

Determinism is a V1 release gate: the same Render Envelope must produce a
pixel-equivalent video within tolerance, every time, on any approved GPU node.
Score storage — Score + Render Envelope + Cue Plan — is the source of truth for
a kept artifact, **not** the materialized MP4; the MP4 is materialized on first
share (when channel-watermark bake-in matters) and re-rendered bit-equivalently
on demand.

The gate is `checkDeterminism` in
`libs/yemaya/living-scenes-runtime/src/determinism/determinism-harness.ts`, a
pure decision function over per-`(node, frame)` SHA-256 digests:

```ts
interface NodeFrameDigest {
  nodeId: string;
  frameNumber: number;
  sha256: string;
}

interface DeterminismReport {
  nodeIds: readonly string[];
  frameCount: number;
  mismatchedFrames: readonly number[]; // frames where node hashes disagree
  perNodeMismatchCount: ReadonlyMap<string, number>;
  pass: boolean; // every node within mismatchToleranceFrames
}
```

It groups digests by frame, flags any frame where the set of node hashes has
more than one unique value, attributes the mismatch to each diverging node
against the reference node, and fails if any node's mismatch count exceeds
`mismatchToleranceFrames`. A run with **fewer than two nodes auto-passes**
(there is nothing to cross-check). The actual rendering happens upstream; the
harness is the release-gate decision. When any envelope component is later
deprecated (model upgraded, blend kernel revised, locale assets updated), kept
artifacts gain a "v2 available" notice with a one-tap re-bake; the original
remains playable.

## Provenance across channels — four redundant signals

Provenance is carried in redundant channels so no single transcoding pipeline
can erase it. The runtime does **not** export a single `ProvenanceBundle` type
(a symbol by that name exists only in the unrelated
`libs/shared/audit-platform`); provenance is split into separate, independently
recoverable modules under `libs/yemaya/living-scenes-runtime/src/provenance/`.

### Visible mark and the out-of-band attestation page

`provenance/visible/visible-mark.ts` builds a `VisibleMarkSpec` — a minimal,
contemplative-aesthetic corner glyph (bottom-right at normalized `0.92, 0.94`,
`oshun-mono`, opacity `0.45`) carrying a **6-character short code** drawn from a
Crockford-style base32 alphabet with confusable characters (`0/O/1/I/L`)
removed. The attestation URL is `buildAttestationUrl({ origin, shortCode })`,
which constructs `${origin}/scene/${encodeURIComponent(shortCode)}`. Note: the
resolver key is the **short code, not an artifact id**, and the origin is
injected — there is no hardcoded `oshun.app` in the builder. Prose that wrote
the attestation page as the literal `oshun.app/scene/<id>` is therefore stale on
both counts. The page re-asserts provenance even if every embedded mark has been
stripped.

### Inaudible audio watermark — a real keyed DSP

`provenance/audio-watermark/dsp.ts` is a genuine spread-spectrum embed/extract,
not just a payload encoder. Each ±1 chip is spread across a block of audio
samples by a deterministic ±1 pseudo-noise (PN) sequence and added at low gain;
extraction correlates each block against the same PN sequence:

- `pnSequence(seed, length)` generates the PN sequence via **xorshift32** — the
  seed _is the watermark key_; embed and extract must share it.
- `DEFAULT_CHIP_LENGTH = 128` samples per chip; `DEFAULT_GAIN = 0.08`.
- `embedWatermark` requires `chipBits.length * chipLength` samples of headroom
  and throws otherwise; `extractWatermark` recovers `0 | 1 | -1` per chip (`-1`
  = erasure below an optional correlation threshold).

The key guarantee: the payload is recoverable from the watermarked signal and
robust to moderate additive noise, while a **wrong PN key recovers nothing —
correlation ≈ 0**. That keyed-correlation property is what makes the audio mark
hard to forge or strip blindly. (Note that the default _deterministic Living
Scene render is silent_; where there is no audio track, the download grant
honestly reports `audioWatermarkIntact: 'not-applicable'` rather than claiming a
mark that is not there.)

### C2PA Content Credentials — real Ed25519, tamper-evident

`provenance/c2pa/ed25519-signer.ts` provides the genuine cryptographic signer
the audit found faked. `createEd25519C2paSigner({ privateKey, signerKeyId })`
takes a **32-byte** Ed25519 secret key (it throws if the length is wrong), keeps
the private key inside the closure, and exposes `signerKeyId`, `publicKeyHex`,
`sign(bytes) → hex`, and `verify(bytes, hex) → boolean`, all over the
`@noble/curves` Ed25519 (EdDSA) primitive.
`createEd25519C2paVerifier(publicKeyHex)` gives consumers a standalone verifier
from the published public key. The signer backs `buildC2paManifest` in the
sibling `c2pa/sidecar.ts`, which canonicalizes the media hash
(`mediaHashSha256`, 64 hex chars), sorted assertions, and claim generator, then
attaches a real `signatureHex`. The module header is explicit that this
de-fabricates a prior faked signature; the result is a tamper-evident sidecar.
Living Scene shares always carry two assertions: `oshun.living_scene.artifact`
(artifact id, envelope hash, privacy level, rendition) and a
`c2pa.training-mining` do-not-train preference. Real C2PA-into-MP4 /
HLS-manifest muxing at materialization time is the upstream piece; the manifest
itself is real and signed.

### Channel-robustness eval — the release gate

`provenance/channel-robustness/channel-eval.ts` measures per-channel survival
across `SHARE_CHANNELS` = `telegram`, `whatsapp`, `twitter-x`, `instagram`,
`discord`, `email`, `generic-web` (seven channels). For each channel,
`evaluateChannel` records, per fixture, whether _at least one_ of
`{ visibleMark, audioWatermark, c2paSidecar }` was recovered; a channel `passes`
iff **every** fixture surfaced ≥ 1 signal. `evaluateAllChannels` rolls this up
into `perChannel`, `missingChannels`, and `allChannelsPass` — which is true only
when every channel is present _and_ every channel passes. Below 100% recovery on
a supported channel blocks release of the share path. Real channel transcoding
(running fixtures through the actual platforms' recompression) is upstream; the
harness is the gate decision over recorded recovery results.

## Cue privacy, redaction, and sensitive-intention handling

Every Score's `intent` slot carries two layers: a `private` layer and a
`public_redacted` layer. `cue-privacy/intent-redaction.ts` owns the redaction
pass over the concrete Lilith taxonomy of eight categories, each a real regex +
replacement token:

| `LILITH_REDACTION_CATEGORIES` | Example pattern                                         | Replacement            |
| ----------------------------- | ------------------------------------------------------- | ---------------------- |
| `named-person`                | `Dr Jane Smith`, `Mrs Smith` (title + capitalized name) | `[NAMED-PERSON]`       |
| `deceased`                    | `my late mother`                                        | `[DECEASED]`           |
| `medical-condition`           | `diagnosed with …`                                      | `[MEDICAL-CONDITION]`  |
| `abuse-disclosure`            | `I was abused …`                                        | `[ABUSE-DISCLOSURE]`   |
| `location-of-safety`          | `safe house at …`                                       | `[LOCATION-OF-SAFETY]` |
| `minor-identity`              | `my 7-year-old …`                                       | `[MINOR-IDENTITY]`     |
| `financial-account`           | `account number …`                                      | `[FINANCIAL-ACCOUNT]`  |
| `legal-status`                | `undocumented`, `asylum-seeker`, `on probation`         | `[LEGAL-STATUS]`       |

`redactIntent` returns the `publicRedactedText` plus the set of `categoriesHit`;
`buildIntentLayers` wraps that into an `IntentLayer` and computes a
`privateIntentHash` by **SHA-256 over `` `${intentId}\0${privateText}` ``**
using `@noble/hashes` (chosen over `node:crypto` so the module bundles for the
browser `'use client'` surfaces). `evaluateCorpus` computes per-category and
macro precision/recall against a fixture corpus, so the taxonomy's quality is
measurable rather than assumed.

**Honest scope.** The classifier header is candid: "Real-world deployment will
swap in a model-backed classifier with the same signature; tests pin the
categorical taxonomy." The shipped regexes are deterministic pattern-matchers —
robust for the test taxonomy, but not an ML classifier. The seam is the safe
kind: the signature is fixed and the corpus evaluator is real, so an upstream
model can drop in without changing callers.

**Default-deny and operator inspection.** Sharing exports the public layer only;
opting to share _with_ the private layer requires per-share confirmation and is
recorded in the artifact's audit log. The private intent layer and cue history
are gated to the same authority class as Iris memory inspection
([Iris Memory and Identity](./iris-memory-identity.md)) — consented or
elevated-authorization — while standard support and trust-and-safety reviewers
see only the public-redacted layer
([Review, Compliance, and Trust & Safety](./review-trust-safety.md)).

## Backend resolver — and the contemplative hard guard

Which generation backend serves a session is decided by `resolveBackend` in
`backend/backend-resolver.ts`. There are two backends
(`BACKEND_IDS = chained-clip`, `phase-176-nous`) and four entitlement tiers
(`ENTITLEMENT_TIERS = contemplative`, `curated-creator`, `aaa-creator`,
`operator-admin`). The selector:

1. **Hard guard:** the `contemplative` tier _never_ resolves to
   `phase-176-nous`, even if the workflow class declares it; it must support
   `chained-clip` and that backend must be able to serve under SLO, or the
   resolver throws.
2. Higher tiers sort the class's supported backends by `BACKEND_QUALITY_RANK`
   (`chained-clip` 0, `phase-176-nous` 1) and pick the highest-rank backend that
   (a) is not `phase-176-nous`-while-the-`phase176FeatureFlag`-is-off and (b)
   passes the per-backend SLO check (`canServeNow`, finite non-negative
   headroom). If none can serve, it throws `no-backend-can-serve`.

The contemplative guard is the reason a meditation session can never be silently
routed onto an experimental world-model backend.

## Scene events — the observable governance feed

Governance actions surface on the Psyche envelope as scene events
(`libs/oshun/embodiment-psyche/src/events/scene-events.ts`,
`SCENE_EVENT_KINDS`). Two are governance-specific:

- `scene.policy-intervention` — `interventionKind` ∈ `pse-strobe-cap`,
  `luminance-cap`, `motion-cap`, `tone-band-shift`, `persona-cap`, with
  `preValue` / `postValue` and `policyVersion`.
- `scene.fallback-engaged` — `fallbackKind` ∈ `reduced-motion-render`,
  `still-image-only`, `transcript-only`, `audio-with-still`,
  `pre-rendered-substitute`; `trigger` ∈ `provider-degraded`,
  `policy-rejection`, `eval-gate-fail`, `shareability-restriction`.

Plus `scene.crisis-frame` itself. `verifyEventStream` enforces a gap-free,
monotonic sequence and per-session trace-id propagation, so the governance feed
is auditable end to end. See
[Psyche Real-Time Runtime](./psyche-realtime-runtime.md) for the envelope these
ride on.

## Launch locales — a known divergence to reconcile

"V1 launch locales" is referenced widely but is **two different sets** in code,
and the cue-policy / crisis "locale parity across V1 launch locales" requirement
is therefore ambiguous about which set it must hold over:

| Set                        | Source                                                         | Members                                                                                   |
| -------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------- |
| `OSHUN_LAUNCH_LOCALES` (8) | `libs/oshun/i18n/src/index.ts`                                 | `en-US`, `es-US`, `fr-FR`, `de-DE`, `ar`, `he`, `ja-JP`, `pt-BR`                          |
| `V1_LAUNCH_LOCALES` (10)   | `libs/isis/workflow-classes/src/living-scene/locale-parity.ts` | `en-US`, `en-GB`, `es-ES`, `es-419`, `fr-FR`, `de-DE`, `pt-BR`, `ja-JP`, `ar-EG`, `he-IL` |

The Living-Scenes locale-parity module uses the **10-locale** set with
regionalized tags and `RTL_LOCALES = { ar-EG, he-IL }`; the platform i18n module
uses the 8-locale set with bare `ar` / `he`. The crisis-classifier locale-parity
gate (imminent FNR ≤ 0.02) must hold over whichever set the deploy targets;
until the two lists are unified, treat the **10-locale LS set** as authoritative
for Living-Scenes parity and flag the platform-vs-LS divergence in any
cross-locale launch checklist. See §25.5 (locale parity) and §25.2 (crisis
classifier) for the backlog framing.

## Related

- [Living Scenes — Concept and Customer Promise](./living-scenes-overview.md) —
  the customer promise these gates protect.
- [Keep, Share, Shareability, Takedown, and Lineage](./scene-sharing-distribution.md)
  — the share-time companion: the shareability matrix, takedown dispositions,
  and personal-artifact lifecycle.
- [Lilith Persona Policy](./lilith-persona-policy.md) — the policy substrate the
  cue gate and crisis frame compose on.
- [Psyche Real-Time Runtime](./psyche-realtime-runtime.md) — the envelope scene
  events ride on.
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md) — the
  review queues crisis and abuse signals route into.
- [Scene Latency, Accessibility, Eval Sets, and Tests](./scene-quality-and-eval.md)
  — the fixture eval-gates and quality bars.
- The features hub: [../features.md](../features.md).
