Oshun Platform · Features

Output Gallery, Lineage, Branch, and Replay

A focused page within the Oshun Platform Features documentation. The full map and every sibling page live in the Features hub.

12sections13 minread5tables

On this page

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, Living Scenes lineage, 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 dispatches a guarded generation; the live provider (Stability SD3.5, ElevenLabs, Suno, fal.ai-hosted LTX-Video; see External Model Intelligence and Execution Providers and Creator Surfaces, Voice, Music, and 3D Generation) 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. 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-veritas.md, domain-nyx.md, domain-nisaba.md, domain-metis.md, domain-arete.md, and Sophia Grounding.)
  • 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 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 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 claim
used-in-lesson the output appears in a Metis lesson
embedded-in-passage the output is embedded in a Nisaba passage
used-in-scene the output is referenced by a Living Scene 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-narrationaudio-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. 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.

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 under "Reshare, Lineage, and Remix Attribution."

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 and External Model Intelligence and Execution Providers 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.