# Output Gallery, Lineage, Branch, and Replay

The output gallery is the unified review-and-derive surface that sits at the
_end_ of every Isis generation. Once a workflow class has produced an image,
video, narration, music track, 3D mesh, caption-dub, or accessibility pass, the
artifact lands as an `OutputRecord` that the gallery can filter, trace through a
lineage tree, branch, replay-with-tweak, compare side-by-side, and bulk-act on.
It serves both the customer Studio (looking back over their own renders) and
operators (governing the full corpus). It is also the join point where the
generation pipeline meets the
[Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md),
[Living Scenes lineage](./scene-sharing-distribution.md), and the trust-&-safety
takedown machinery. The whole subsystem is real, pure-function, and unit-tested
in `libs/isis/output-gallery/` against §24.10.

## Where this sits in V1

The output gallery is the consumer-facing tail of the same release-gated
pipeline described in [Isis Generation Control](./isis-generation-control.md).
Isis dispatches a guarded generation; the live provider (Stability SD3.5,
ElevenLabs, Suno, fal.ai-hosted LTX-Video; see
[External Model Intelligence and Execution Providers](./external-models-and-execution.md)
and
[Creator Surfaces, Voice, Music, and 3D Generation](./creator-voice-music-3d.md))
returns bytes; a provenance bundle and a cost-ledger entry are stamped; and the
result becomes an `OutputRecord` the gallery surfaces. Which gallery a customer
sees, and what actions are available, are governed by the same four-tier
resolver as the rest of generation — see
[Generation Audience Tiers and Surface Boundaries](./generation-tiers-and-surfaces.md).
The dedicated operator gallery is itself one of the 28 named surfaces
(`output-gallery-admin`), reachable only by the `operator-admin` tier.

Everything below is implemented in `@isis/output-gallery`
(`libs/isis/output-gallery/src/`), whose package description is verbatim
_"Unified output gallery: filters, lineage, branch/replay, compare grid, bulk
actions (§24.10)"_. The library's public surface is exactly five modules:

| Module file        | Exports (real symbols)                                                                                                                               | Responsibility                                        |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| `output-record.ts` | `OutputRecord`, `OutputDomain`, `OutputKind`, `OUTPUT_DOMAINS`, `OUTPUT_KINDS`, `GalleryFilter`, `GalleryQuery`, `GalleryPage`, `applyGalleryFilter` | The faceted record + pure filter/pagination function  |
| `lineage.ts`       | `OutputLineage`, `LineageEdge`, `LineageEdgeKind`, `LineageTreeError`                                                                                | Per-output upward/downward edge graph + cascade walk  |
| `branch-replay.ts` | `planBranch`, `validateBranchDelta`, `ParameterAllowedRange`, `BranchDelta`, `OriginalInputs`, `BranchRunRequest`, `BranchValidationError`           | Range-bounded branch/replay derivation                |
| `compare-grid.ts`  | `buildCompareGrid`, `DiffMetric`, `CompareSlot`, `CompareGridLayout`, `CompareGridError`                                                             | 2-up / 4-up / n-up compare with per-kind diff metrics |
| `bulk-actions.ts`  | `BulkAction`, `BulkActionInvocation`, `validateBulkAction`, `recordBulkAction`, `undoBulkAction`, `AuthorizationCheck`, `BulkActionError`            | Operator-audited, undoable selection actions          |

The pure-function design is deliberate: the filter, the lineage walk, the branch
planner, and the bulk-action validators take their inputs and return their
outputs with no I/O, so they unit-test deterministically and can run identically
in the BFF, the admin service, and the client.

## The OutputRecord — one faceted row per render

Every artifact the gallery surfaces is an `OutputRecord` (`output-record.ts`).
Its fields _are_ the filter facets — the gallery filter UI exposes exactly the
fields the record carries, so there is no drift between "what you can filter on"
and "what is stored." The schema:

```ts
interface OutputRecord {
  outputId: string;
  tenantId: string;
  userId: string;
  domain: OutputDomain; // 'tara' | 'veritas' | 'nyx' | 'nisaba' | 'metis' | 'arete' | 'sophia'
  personaId: string | null;
  workflowClassId: string;
  kind: OutputKind; // see below
  model: string;
  generatorKey: string;
  locale: string;
  entitlementTier:
    | 'contemplative'
    | 'curated-creator'
    | 'aaa-creator'
    | 'operator-admin';
  licenseTag: string;
  producedAtUnixSeconds: number;
  takenDownAtUnixSeconds: number | null;
  assetUrl: string;
  thumbnailUrl: string;
  costCents: number;
  tags: readonly string[];
}
```

Two enumerations are frozen constants the rest of the codebase keys off:

- `OUTPUT_DOMAINS` — the seven generating domains: `tara`, `veritas`, `nyx`,
  `nisaba`, `metis`, `arete`, `sophia`. (These mirror the V1 domains documented
  in [domain-tara.md](./domain-tara.md),
  [domain-veritas.md](./domain-veritas.md), [domain-nyx.md](./domain-nyx.md),
  [domain-nisaba.md](./domain-nisaba.md), [domain-metis.md](./domain-metis.md),
  [domain-arete.md](./domain-arete.md), and
  [Sophia Grounding](./sophia-grounding.md).)
- `OUTPUT_KINDS` — the seven artifact kinds: `image`, `video`,
  `audio-narration`, `audio-music`, `mesh-3d`, `caption-dub`,
  `accessibility-pass`. These map one-to-one onto the BFF generation executors
  (`apps/oshun/bff/src/generation/`: `image`, `narration`, `music`, `video`,
  `caption-dub`, `accessibility-pass`, plus `explainer`, `sky-briefing`,
  `curated-generation`, and `realtime-music`).

Note that `entitlementTier` on the record is the **code-canonical** tier
vocabulary — `contemplative`, not "Customer"; `operator-admin`, not "Operator".
The earlier prose in [../features.md](../features.md) that called the four tiers
"`Customer`, `Curated-Creator`, `AAA-Creator`, `Operator` … used verbatim" is
inaccurate against the union in `libs/isis/entitlements/src/generation-tier.ts`;
the authoritative names are those carried by the record. See
[Generation Audience Tiers and Surface Boundaries](./generation-tiers-and-surfaces.md)
for the full reconciliation.

### Filtering — a pure function over records

`applyGalleryFilter({ records, query })` is the entire query engine. A
`GalleryFilter` is an AND of optional facet predicates; every present facet must
match for a record to pass. The facets:

| Facet                    | Semantics                                            |
| ------------------------ | ---------------------------------------------------- |
| `domain`                 | record domain ∈ list                                 |
| `personaId`              | record persona is non-null and ∈ list                |
| `workflowClassId`        | ∈ list                                               |
| `kind`                   | ∈ list                                               |
| `licenseTag`             | ∈ list                                               |
| `generatorKey`           | ∈ list                                               |
| `model`                  | ∈ list                                               |
| `locale`                 | ∈ list                                               |
| `tenantId`               | ∈ list                                               |
| `entitlementTier`        | ∈ list                                               |
| `tag`                    | record tags ⊇ all listed tags (every-of, not any-of) |
| `freshnessWindowSeconds` | `producedAtUnixSeconds ≥ now − seconds`              |
| `excludeTakenDown`       | drops records whose `takenDownAtUnixSeconds` is set  |

The matcher is strict on a couple of edge cases worth calling out. A `personaId`
filter excludes records whose persona is `null` (a persona-scoped view never
accidentally shows persona-less renders). The `tag` facet uses `every` —
supplying `['retro', 'poster']` returns only records carrying _both_ tags, which
is what an operator narrowing a corpus actually wants, not an any-of union.
Freshness is evaluated against a caller-supplied `nowUnixSeconds`, which keeps
the function deterministic and testable rather than reading a wall clock
internally.

Results are sorted newest-first by `producedAtUnixSeconds`, with `outputId`
breaking ties (so pagination is stable across calls), and paginated with an
opaque integer `cursor` returned as `nextCursor` (`null` when exhausted). The
`GalleryPage` also carries the full `total` match count for the filtered set.

## Lineage — the per-output edge graph

`OutputLineage` (`lineage.ts`) is a directed graph over output IDs that records
_how a render relates to the rest of the corpus_. Each output edges **upward**
to the run/prompt/workflow class that produced it and **downward** to the
derivative artifacts that consume it. Edges are typed by `LineageEdgeKind`:

| `LineageEdgeKind`     | Meaning                                                                         |
| --------------------- | ------------------------------------------------------------------------------- |
| `derived-from`        | a branch/replay child of a parent output                                        |
| `cited-by-claim`      | the output backs a [Veritas](./domain-veritas.md) claim                         |
| `used-in-lesson`      | the output appears in a [Metis](./domain-metis.md) lesson                       |
| `embedded-in-passage` | the output is embedded in a [Nisaba](./domain-nisaba.md) passage                |
| `used-in-scene`       | the output is referenced by a [Living Scene](./living-scenes-overview.md) score |
| `send-to-editorial`   | the output was routed into the editorial queue                                  |

The graph maintains both `outgoing` and `incoming` adjacency, so `parentsOf` and
`childrenOf` are O(1) lookups. Three integrity properties are enforced at
`addEdge` time, each raising a typed `LineageTreeError`:

- **Both endpoints must be registered** (`unknown-output`). You cannot edge to
  or from an output the lineage has not seen via `registerOutput`.
- **No cycles** (`cycle-detected`). `wouldCreateCycle` walks the outgoing graph
  from the proposed target looking for the source before adding; a self-edge is
  rejected immediately. This guarantees the lineage is always a DAG, which is
  what makes the descendant walk terminate.
- **No duplicate edges of the same kind** (`duplicate-edge`). The same
  `from → to` pair may carry two _different_ kinds (an output can be both
  `cited-by-claim` and `used-in-lesson`), but not the same kind twice.

`descendantsOf(outputId)` does an iterative depth-first walk over outgoing edges
and returns every reachable output ID. This is the primitive that powers
**takedown cascade**: when an output is taken down, the operator surface walks
its descendants so that lessons, claim citations, passage embeddings, and scenes
built on top of it are surfaced for the cascade decision. Because the graph is a
proven DAG, the walk is bounded and the `seen` set prevents re-visits.

## Branch and replay-with-tweak — range-bounded derivation

A **branch** derives a new run from an existing output by applying an explicit
parameter delta; **replay-with-tweak** is the same operation seeded with the
user's _original_ inputs as the starting point. Both go through
`branch-replay.ts`, and the load-bearing fact the prose elsewhere underplays is
that **the delta is range-validated, not free-form**.

`validateBranchDelta({ delta, allowed })` checks every override in the
`BranchDelta.parameterOverrides` map against the workflow class's declared
`ParameterAllowedRange[]`. A range is either a `number-range` (with `min`/`max`)
or an `enum` (with `enumOptions`). Validation raises a typed
`BranchValidationError` with one of three codes:

- `unknown-parameter` — the override names a parameter not on the workflow
  class's allowed list. You cannot smuggle in a parameter the class never
  declared.
- `wrong-type` — a `number-range` parameter was given a non-number.
- `out-of-range` — a numeric value outside `[min, max]`, or an enum value not in
  `enumOptions`.

This is the mechanism behind the docs' phrase "targeted parameter override
within allowed range": the workflow class, not the customer, decides which knobs
are tweakable and how far. A `contemplative`-tier curated surface can expose a
narrow override set; an `aaa-creator` graph-editor class can declare a wide one
— the validator is the same code path either way, so the guarantee holds across
tiers.

`planBranch({ original, delta, allowed, costPerParameterOverrideCents })`
validates first, then merges the overrides over the original parameters and
returns a `BranchRunRequest`:

```ts
interface BranchRunRequest {
  parentOutputId: string; // becomes the 'derived-from' lineage parent
  inputs: Record<string, number | string>; // original ⊕ overrides
  workflowClassId: string; // inherited from the original — branch stays in-class
  estimatedCostCents: number; // overrideCount × costPerParameterOverrideCents, floored at 0
}
```

Two design choices fall out of this. First, `workflowClassId` is copied straight
from the original, so a branch can never escape the workflow class that produced
its parent — derivation stays "within workflow-class bounds" as the spec
requires. Second, the `parentOutputId` is exactly what the caller turns into a
`derived-from` lineage edge once the branch run completes, which is how
replay-with-tweak "appends to the lineage tree." Determinism within a seed (the
spec's branch/replay test target) follows from the merge being a pure override
of the original inputs — same original and same delta → same merged inputs →
same downstream render under a fixed seed.

## Compare grid — typed diff metrics per asset class

`buildCompareGrid({ records, layout, diffMetrics })` (`compare-grid.ts`)
assembles a `CompareGridLayout` of `2-up`, `4-up`, or `n-up` slots and attaches
a typed `DiffMetric` to each pairwise comparison the caller supplies. The metric
union is asset-class-specific, which is the real contract behind the prose "diff
overlays per asset class":

| `DiffMetric.kind`   | Payload                  | Asset class       |
| ------------------- | ------------------------ | ----------------- |
| `pixel-delta`       | `meanAbsoluteDelta`      | image             |
| `frame-delta`       | `meanAbsoluteDelta`      | video (per-frame) |
| `audio-rms-delta`   | `rmsDeltaDb`             | audio loudness    |
| `waveform-delta`    | `correlationCoefficient` | audio waveform    |
| `mesh-vertex-delta` | `hausdorffDistance`      | 3D mesh           |

The builder enforces several invariants, each a typed `CompareGridError`:

- **At least two records** and exact slot counts for fixed layouts: `2-up`
  requires exactly 2 records, `4-up` exactly 4 (`invalid-layout`).
- **n-up is capped at 16 slots** (`MAX_N_UP_SLOTS = 16`, `too-many-slots`) — a
  hard upper bound so a compare view can't be asked to diff an unbounded set.
- **Kinds must be compare-compatible** (`mixed-kinds`). Same kind is always
  compatible; the one cross-kind exception is `audio-narration` ↔ `audio-music`,
  which share a playback kind and are meaningfully comparable by waveform (e.g.
  an operator cross-checking a narration against a music bed). Comparing an
  image against a mesh is rejected.

Diff pairs are built for every `(i, j)` slot combination, keyed
`"<fromOutputId>::<toOutputId>"` against the supplied `diffMetrics` map; a pair
only appears in the layout if the caller actually provided a metric for it. The
metrics themselves are computed upstream (by the asset-comparison machinery) and
handed in — the grid model is the assembly-and-validation layer, not the
pixel/waveform math.

## Bulk actions — operator-audited and undoable

`bulk-actions.ts` covers selection-scoped operations: `re-tag`,
`send-to-editorial`, `revoke-consent`, and `takedown`. Each is a discriminated
`BulkAction`:

```ts
type BulkAction =
  | { kind: 're-tag'; addTags: string[]; removeTags: string[] }
  | { kind: 'send-to-editorial'; queueId: string }
  | { kind: 'revoke-consent'; consentRecordId: string }
  | { kind: 'takedown'; reason: string };
```

`validateBulkAction` enforces both shape and **tier authorization** via an
`AuthorizationCheck { operatorId, tier }`:

| Action              | Required tier                                         | Extra validation            |
| ------------------- | ----------------------------------------------------- | --------------------------- |
| `re-tag`            | `curated-creator`, `aaa-creator`, or `operator-admin` | non-empty add or remove set |
| `send-to-editorial` | `curated-creator`, `aaa-creator`, or `operator-admin` | non-empty `queueId`         |
| `revoke-consent`    | **`operator-admin` only**                             | non-empty `consentRecordId` |
| `takedown`          | **`operator-admin` only**                             | non-empty trimmed `reason`  |

The two destructive, rights-bearing actions — revoking a consent record and
taking content down — are locked to `operator-admin`. A non-empty `operatorId`
is required on every action for the audit trail. `recordBulkAction` additionally
asserts the recorded `operatorId` matches the one in the `AuthorizationCheck`
(`unauthorized` if it doesn't), so the audited actor cannot be spoofed to differ
from the authorized one.

`recordBulkAction` produces a `BulkActionInvocation` stamped with
`performedAtUnixSeconds`, the affected `outputIds`, and an `undoWindowSeconds`
that **defaults to 5 minutes** (`5 * 60`). `undoBulkAction` flips `undone` and
sets `undoneAtUnixSeconds` if called within the window; past the window, it
raises `undo-window-elapsed`. Re-undoing an already-undone invocation is
idempotent (returns it unchanged). This is the concrete "undo window" the spec
calls for — bounded, audited, and reversible.

## Provenance-bundle inspector

Every output the gallery surfaces is backed by a `CanonicalProvenanceBundle`
stamped at dispatch time by the Isis control plane — consent ID, prompt, model,
watermark, timestamp, invoking user, tenant, workflow class, and the cost-ledger
entry that mirrors the record's `costCents`. The gallery's provenance inspector
renders that bundle for any output; the schema and the fail-closed dispatch seam
that produces it live in
[Isis Generation Control](./isis-generation-control.md). The music/audio path
additionally carries a watermark and provenance contract from
`@isis/music-generation` ("provider abstraction, workflow classes, watermark +
provenance (§24.8)"), whose `guardrails.ts` constrains per-class
`allowedMimeTypes` to `audio/wav`, `audio/mpeg`, `audio/ogg`, and `audio/flac` —
see
[Creator Surfaces, Voice, Music, and 3D Generation](./creator-voice-music-3d.md).

## Living Scenes lineage extension

Kept Living Offerings and shared scenes are first-class lineage nodes: the
`used-in-scene` edge kind ties an output into a scene score, and
attribution-policy edges travel with reshares. Replay-with-tweak from a public
artifact produces correctly attributed children (the branch's `parentOutputId`
becomes the `derived-from` edge), `branch-from-this-moment` forks a new score,
and reshare cannot escalate scope. The full attribution model is specified in
[Keep, Share, Shareability, Takedown, and Lineage](./scene-sharing-distribution.md)
under "Reshare, Lineage, and Remix Attribution."

## Who sees which gallery, and the AAA execution substrate

The customer Studio gallery is exposed under `apps/oshun/web/src/app/studio/`
alongside the `generation` and `generation-gallery` surfaces. The legacy
`studio/isis/*` segments are now **404-hard-blocked** (the studio-boundary
allowlist was emptied). The older description that "AAA-tier routes return a
disclosure + Yemaya signup gate" is therefore partly stale — the legacy isis
segments hard-block rather than serving a CTA. The full AAA creator workspace
lives in the separate `apps/yemaya/studio-web` and `apps/yemaya/studio-desktop`
apps; that is where heavyweight derivation runs. `studio-web` imports
`@oshun/render-farm` (`libs/oshun/render-farm`) — the render-farm scheduler that
the gallery's branch and bulk re-render flows feed into. The render farm
provides priority queues, worker-node capability matching, GPU requirement
matching, dependency execution, preemption, checkpoint/resume, cloud-burst, and
cost estimation. It exports `RenderJob`, `RenderAssignment`, `RenderCheckpoint`,
`RenderCloudBurstPlan`, `RenderCostEstimate`,
`RenderGpuCapability`/`RenderGpuRequirement`, `PreemptionDecision`,
`RenderDashboardSnapshot`, and `RenderQuotaBreach`. It is the AAA-tier execution
scheduler the docs gesture at; the gallery is the review surface over its
outputs.

The dedicated operator gallery (`output-gallery-admin`) is one of the 28 named
`GenerationSurface` values, on the `operator-admin` allowlist only. The
`bulk-actions` tier checks above are the gallery-local enforcement of that same
boundary — the operator-only destructive actions can only fire from a surface
the operator tier can reach in the first place.

## Tests and verification

`output-gallery.test.ts` covers each module against §24.10 with
correctness-asserting cases (not shape-only checks): filter-by-domain+class+tag,
taken-down exclusion, freshness windows, newest-first cursor pagination; lineage
cycle refusal, duplicate-edge refusal, `descendantsOf` reachability, and
`parentsOf` incoming edges (the takedown-cascade and lineage-integrity targets);
branch rejection of unknown parameters / out-of-range numbers / bad enum values
plus a merged-inputs-with-cost-estimate happy path (the branch/replay
determinism target); bulk-action empty-set / non-admin-revoke / missing-reason /
operator-mismatch rejections and the re-tag undo-within-window vs
undo-after-window pair; and compare-grid 2-up construction, wrong-slot-count
rejection, mixed-kind (image vs mesh) rejection, the narration↔music compare
exception, and the 16-slot n-up cap. These map directly onto the spec's listed
test targets — lineage chain integrity, branch/replay determinism within seed,
takedown cascade reach, provenance display completeness, filter correctness, and
bulk-action authorization.

## Honest status

The gallery model — records, filtering, lineage DAG, branch/replay validation,
compare grid, and bulk actions — is **real, in-repo, and unit-tested** as pure
functions. What it surfaces depends on the upstream live providers, which are
env-gated and fail-closed by design: with no provider key, the Isis resolver
returns `null` and the job fails closed with `provider_not_configured`, so there
is simply no `OutputRecord` to gallery. See
[Isis Generation Control](./isis-generation-control.md) and
[External Model Intelligence and Execution Providers](./external-models-and-execution.md)
for the `OSHUN_STABILITY_*`, `OSHUN_ELEVENLABS_*`, `OSHUN_SUNO_*`, and fal.ai
deploy-time toggles. A fully rendered AAA Yemaya Studio graph-editor UI driving
the `apps/yemaya/studio-{web,desktop}` apps over `@oshun/render-farm` is present
in the tree but not verified end-to-end here. The provenance inspector and
watermark contracts are real schemas; their population assumes a live dispatch.

## Related

- [Isis Generation Control](./isis-generation-control.md)
- [Generation Audience Tiers and Surface Boundaries](./generation-tiers-and-surfaces.md)
- [Creator Surfaces, Voice, Music, and 3D Generation](./creator-voice-music-3d.md)
- [External Model Intelligence and Execution Providers](./external-models-and-execution.md)
- [Editorial Calendar and Asset & Media Library](./editorial-and-asset-library.md)
- [Keep, Share, Shareability, Takedown, and Lineage](./scene-sharing-distribution.md)
- [Review, Compliance, and Trust & Safety](./review-trust-safety.md)
- [Subsystem Glossary](./glossary.md)
- Hub: [../features.md](../features.md)
