# Authoring, Tenants & Content Pipeline

V3 is not one world — it is three (Tara Studio, Saraswati Stage, Lilith Commons)
plus eighteen room _modes_ on top of them, and every one of those has to be
something a non-engineer can author, build into the client, ship to a region (or
deliberately _not_ ship to one), and hot-load at runtime without an engine
rebuild. That is the job of this layer: the **tenant packaging model** that
turns each world into a Game Feature Plugin, the **authoring split** that lets a
yoga instructor publish an entire class from a web form while an environment
artist builds the venue in UE5, and the **content pipeline** that reconciles
those two very different source-control worlds (git for code and
machine-generated assets, Perforce for binary art). It is the least cinematic
and most load-bearing layer in the product — a tenant that won't register is a
world nobody can enter, and a design token that drifts is a button that looks
wrong in every room at once. The section hub is
[../V3_ARCHITECTURE.md](../V3_ARCHITECTURE.md).

## What ships, honestly

The **tenant packaging and the authoring tooling are real, committed, and
machine-verified**; the _art_ those tenants point at, and the cross-region
publish machinery, are partly spec. Three honest layers:

- **Real and committed.** All **21 Game Feature Plugins** exist as genuine
  `*.uplugin` descriptors under `V3/ue/Plugins/GameFeatures/` (3 tenants + 18
  modes), each with a JSON registration manifest. Two real UE commandlets in the
  `V3Editor` module build and _smoke-test_ them: one materializes a
  `GameFeatureData.uasset` + a gameplay-tag `.ini` from each manifest, the other
  drives the live `UGameFeaturesSubsystem` through an Active→Registered
  load/unload transition per plugin. The 21 generated `GameFeatureData.uasset`
  files are committed (a deliberate `.gitignore` exception). The web authoring
  surface is real Next.js (`apps/oshun/web/src/app/lilith-studio/`, 9 routes)
  backed by a substantial domain library (`libs/v3/tara-studio`, 19 domain
  modules incl. a 300-entry canonical asana library with a GA gate) and a Prisma
  schema. Design tokens cross the web↔UE boundary through a third commandlet
  with a drift-checking verify pass and ~107 committed token assets.
- **Manifest references real, art not committed.** The registration manifests
  name ability-set, world-template, Niagara, and MetaSound asset _paths_
  (`/Game/Venues/Tara/...`, `/Game/VFX/Tara/NS_BreathCue`). Those target assets
  are **not in the tree** — they are the UE-Editor art surface that, per the
  monolith, lives in **Perforce, not git**. There is no committed Perforce
  config (p4ignore/typemap) here either: Perforce is the documented posture, not
  a checked-in artifact.
- **Spec / described in the monolith.** The nightly **fallback bake** (UE cooked
  assets → glTF + KTX2 + lightmaps for Tier-2), the **V1 environment-promotion
  publish gates**, and the world server's 30 s asset-manifest hot-reload poll
  are described in `V3_ARCHITECTURE.md` and have no standalone committed job in
  this tree. And `libs/v3/isis-world-asset` is a **capability descriptor**, not
  a runtime world-asset generator — see the closing note.

## Tenants are Game Feature Plugins

V3 ships every world and every room mode as an Unreal **Game Feature Plugin** so
the build pipeline can include or exclude one per platform/region without an
engine rebuild, and so the client can hot-load it on demand. The 21 descriptors
live under `V3/ue/Plugins/GameFeatures/`:

- **3 tenants** — `V3Tenant_TaraStudio`, `V3Tenant_SaraswatiStage`,
  `V3Tenant_LilithCommons`.
- **18 modes** — four Tara room modes (`V3Mode_TaraLiveClass`, `…TaraOnDemand`,
  `…TaraPrivate`, `…TaraCohort`), five Saraswati event modes (Concert / Club /
  Listening / Drop / Festival), and nine Commons venues (Atrium, Observatory,
  Debate Hall, Lecture Hall, Stacks, Ritual Room, Arête Atrium, Lantern Hall,
  Solitary Cell).

Every descriptor follows the same load contract — verified across all 21 — for
example `V3Tenant_TaraStudio/V3Tenant_TaraStudio.uplugin`:

```json
"EnabledByDefault": false,
"ExplicitlyLoaded": true,
"BuiltInInitialFeatureState": "Registered",
"CanContainContent": true,
"Plugins": [ { "Name": "GameFeatures" }, { "Name": "ModularGameplay" } ]
```

`EnabledByDefault: false` + `ExplicitlyLoaded: true` is what makes a tenant
hot-loadable rather than always-resident;
`BuiltInInitialFeatureState: "Registered"` means it is known to the subsystem
but not Active until a client joins a room that needs it. The monolith's
tenant/mode table (`V3_ARCHITECTURE.md` §"Game Feature Plugins (Tenants)") maps
each plugin to its activation rule — tenants active globally at GA, modes loaded
on room join.

### What a registration manifest carries

Each plugin keeps a co-located JSON manifest at
`Content/<Plugin>/GameFeatureData_<Plugin>.v3asset.json`. It is the declarative
source of truth for what the plugin registers:

```json
{ "assetType": "GameFeatureData", "plugin": "V3Tenant_TaraStudio",
  "activation": "global-ga",
  "gameplayTags": ["Tenant.TaraStudio", "Tara.Class.Live", ...],
  "abilitySets":   ["/Game/Tara/Abilities/AS_TaraInstructorControls", ...],
  "worldTemplates":["/Game/Venues/Tara/WBP_TaraStudioTemplate", ...],
  "niagaraSystems":["/Game/VFX/Tara/NS_BreathCue", ...],
  "metaSounds":    ["/Game/Audio/Tara/MS_TaraPracticeBed", ...] }
```

This is the honest seam noted above: the manifest is real and committed, but the
asset _paths_ it names point at art that is authored in UE and stored in
Perforce — they are not in this repository. The manifest is the contract; the
art is the fulfillment, and the two are versioned in different systems on
purpose.

### The commandlets that build and verify them

Two real commandlets in `V3/ue/Source/V3Editor/Private/` turn the manifests into
engine state. `UV3GenerateGameFeatureDataCommandlet::Main`
(`V3GenerateGameFeatureDataCommandlet.cpp:251`) walks every `*.uplugin` under
the `GameFeatures` folder, and for each one: reads + validates the manifest
(`ReadManifest`, rejecting empty arrays and a plugin-name mismatch against the
descriptor), writes a `Config/Tags/<Plugin>Tags.ini` gameplay-tag file
(`SaveGameplayTagsFile`), then materializes a `GameFeatureData.uasset` via
`UV3GameFeatureData::ConfigureFromManifest` followed by a
`HasCompleteRegistrationManifest` completeness gate before `SavePackage`
(`SaveGameFeatureDataAsset`). It is idempotent — `AssetMatchesManifest` short-
circuits a re-save when the existing asset already matches.

`UV3GameFeatureData` itself (`V3/ue/Source/V3Core/Public/V3GameFeatureData.h`)
is a real subclass of the engine's `UGameFeatureData`, carrying `FeatureName`,
`ActivationPolicy`, `RegisteredGameplayTags`, `AbilitySetIds`,
`WorldTemplateIds`, `NiagaraSystemIds`, and `MetaSoundIds`, and overriding
`GetPrimaryAssetId()` so the plugin participates in the asset-manager
primary-asset graph.

The verifier earns the most trust. `UV3VerifyGameFeaturesCommandlet::Main`
(`V3VerifyGameFeaturesCommandlet.cpp:69`) does not read files — it exercises the
**live engine**. For each plugin it calls
`UGameFeaturesSubsystem::Get().ChangeGameFeatureTargetState(..., EGameFeatureTargetState::Active)`,
asserts `IsGameFeaturePluginActive`, then transitions back to `Registered`, with
a 10-second timeout (`V3GameFeatureTransitionTimeoutSeconds`) that ticks
`GEditor` while it waits. A plugin that can't actually load and unload fails the
commandlet. The 21 resulting `GameFeatureData.uasset` files are committed via an
explicit `.gitignore` exception (`.gitignore:327` whitelists
`V3/ue/Plugins/GameFeatures/**/Content/GameFeatureData.uasset` against the
global `*.uasset` ignore).

```mermaid
flowchart LR
    manifest["GameFeatureData_*.v3asset.json<br/><sub>declarative manifest (git)</sub>"]
    gen["UV3GenerateGameFeatureDataCommandlet<br/><sub>V3Editor</sub>"]
    tags["&lt;Plugin&gt;Tags.ini<br/><sub>gameplay tags</sub>"]
    asset["GameFeatureData.uasset<br/><sub>committed (gitignore exception)</sub>"]
    verify["UV3VerifyGameFeaturesCommandlet<br/><sub>Active → Registered, live subsystem</sub>"]
    manifest --> gen
    gen --> tags
    gen -->|ConfigureFromManifest +<br/>HasCompleteRegistrationManifest| asset
    asset --> verify
    verify -->|"load/unload smoke test (10s)"| ok([green])
```

### Install-first vs. streamed: pak chunking

Because tenants are plugins, the _install_ can be partitioned along the same
boundary. The chunk plan lives at
`V3/ue/Build/Chunking/tenant-mode-pak-chunking.json` with a typed mirror at
`libs/oshun/analytics/src/v3-tenant-mode-pak-chunking.ts`, and is enforced by
`scripts/v3/verify-v3-tenant-mode-chunking.mjs`
(`pnpm verify:v3 tenant-mode-chunking`). The install-first set is the base shell
plus the Tara tenant and its four room modes (`pakchunk0-base-shell`,
`pakchunk20-tenant-tara-studio`, `pakchunk21..24` for the Tara modes), while
`V3Tenant_LilithCommons` and `V3Tenant_SaraswatiStage` are **streamed** chunks
pulled on demand — so a mobile install (`ios-forward-plus`,
`android-vulkan-forward-plus`) ships small and grows only when a user actually
enters Commons or a concert. This is the build-side payoff of the plugin model:
the same boundary that hot-loads a world at runtime also slices the download.

## The authoring split: web Studio vs. UE Editor

Most V3 creators — Tara instructors, Saraswati editorial staff, partner studios
— do not have UE5 installed and shouldn't need to. Lilith Studio therefore
exposes authoring on **two surfaces** with a hard split, documented exhaustively
in the monolith's "Lilith Studio Authoring Split" table. The rule of thumb:
anything that is _data_ (sequences, schedules, metadata, consent, taxonomy) is
authored on the **web**; anything that is _art or cinematics_ (venue levels,
Niagara, camera cue tracks, MetaHumans) is authored in **UE Editor**.

### What the web owns

The web surface is a real Next.js app section at
`apps/oshun/web/src/app/lilith-studio/`, with nine committed `page.tsx` routes:
the Studio shell (`page.tsx`) plus `asana/`, `tara/`, `concerts/`, `music/`,
`personas/`, `scenes/`, `avatar-costume/`, and `provenance/`. A Tara instructor
authoring an `AsanaSequence`, their profile, schedule, and recording opt-in
never leaves these routes; the UE5 client renders the _published_ sequence
manifest at runtime, so no UE install is required of the instructor.

The depth behind the web routes lives in `libs/v3/tara-studio` (19 domain
modules, not CRUD). The flagship is the **canonical asana library**
(`src/asana-library.ts`): 30 asana seeds crossed with 10 lineage variants
(Iyengar, Ashtanga, viniyoga, trauma-informed, chair-accessible, …) produce the
`TARA_CANONICAL_ASANA_LIBRARY`, and `validateTaraCanonicalAsanaLibrary`
(`asana-library.ts:240`) enforces a real GA gate — `≥ 300` entries, every entry
editorial-signed _and_ design-approved, with ≥3 lineage variants, ≥4
modifications, ≥2 contraindications, and a ≥3-cue Aja alignment bundle — failing
loudly via `assertTaraCanonicalAsanaLibraryGaGate`. Alongside it sit
domain-specific guardrails: instructor TTS-voice consent/contract/scope-lock/
royalty (`tts-voice-*.ts`), Sophia-grounded lineage claims
(`lineage-grounding.ts`), an invitational-language linter, an eyes-open default,
and physical-adjustment explicit consent — each a published web-authorable
policy the UE client honors, not engine code. The capability ledger in
`libs/v3/tara-studio/src/index.ts` enumerates all 22 of these as a typed
`V3PackageDescriptor`.

Persistence is contract-first. `libs/v3/tara-studio/prisma/schema.prisma`
(machine- generated by
`libs/oshun/persistence/scripts/generate-v3-prisma-schemas.ts`) defines
`V3AsanaSequence`, `V3Asana`, `V3LiveClassSession`, `V3OnDemandClassRecording`,
`V3InstructorProfile`, `V3InstructorCredential`, `V3AjaCueEvent`, and
`V3PracticePlan` — each row carrying `contractSchema`, `contractVersion`, and a
`payloadHash`, mapped to a `v3_*` table. That hash column is the integrity
spine: a published manifest's bytes are provable against its row, so the JSON
the UE client pulls is the same artifact the web authored.

### What the UE Editor owns

The UE-side authoring tooling is the `V3Editor` module
(`V3/ue/Source/V3Editor/`). Its `Build.cs` declares the editor-only dependency
set that gives it teeth — `AssetRegistry`, `Blutility`, `GameFeatures`, `Json`,
`UnrealEd`, `UMG`, and the V3 runtime modules `V3Core`/`V3UI`. Beyond the two
Game-Feature commandlets above, it contributes the design-token toolchain and an
in-editor `UV3DesignTokenReloaderWidget` (an `EditorUtilityWidget` exposing
`ReloadGeneratedTokens` with `LastReloadHash` / `LastAppliedWidgetCount`
readback, so an artist can re-apply token changes live without restarting).
Venue levels, Niagara systems, MetaHumans, and Sequencer cue tracks are authored
here in the conventional UE workflows; as noted, their assets are
Perforce-resident and not in this tree.

### Design tokens: the one bridge that crosses both

Design tokens are where the two surfaces _must_ agree, and the only place an
asset is generated from data on the UE side. The web emits a manifest to
`V3/ue/Content/UI/DesignTokens/Generated/manifest.json` — brushes, font faces,
and widget styles keyed by theme/role (e.g. `dark.accent.primary` → `#20C9D8`),
plus a top-level `driftHash`. `UV3GenerateDesignTokenAssetsCommandlet`
(`V3GenerateDesignTokenAssetsCommandlet.cpp:817`) parses it and materializes
real Slate assets — `USlateBrushAsset` (via `FSlateRoundedBoxBrush`),
`UFontFace` (loading the actual fallback font bytes off disk), and
`USlateWidgetStyleAsset` containers for
TextBlock/Button/EditableTextBox/ProgressBar styles — converting sRGB hex to
linear color and saving each package. The companion
`UV3VerifyDesignTokensCommandlet` (`:839`) runs the _same_ pass with
`bWriteAssets=false`, so any asset that has **drifted** from the manifest fails
verification instead of being silently rewritten (`RunDesignTokenAssetPass`
returns the drift error per token). ~107 generated token `.uasset`s are
committed under a second `.gitignore` exception (`.gitignore:328`), and the loop
is gated by `pnpm verify:v3 ue-design-tokens`. This is the inverse of the asana
flow — there, data renders at runtime; here, data is _baked_ into engine assets
at build time — but both keep web and UE reading one canonical source.

## Content pipeline and source control

The pipeline reconciles two source-control worlds. Per the monolith's "Content
Pipeline and Source Control" section, UE binary assets (`*.uasset`, `*.umap`)
under `V3/ue/Content/` are versioned in **Perforce** (binary support, per-file
locking — standard UE practice), while code modules (`V3/ue/Source/`) and the
Game Feature plugins (`V3/ue/Plugins/`) live in **git** alongside the monorepo
and are mirrored to Perforce for coexistence. The repository's `.gitignore`
encodes exactly this division: it ignores `*.uasset`/`*.umap` globally
(`.gitignore:324-325`) and then _whitelists_ only the two machine-generated
classes — the 21 `GameFeatureData.uasset` registration assets and the design-
token assets. So git holds the deterministic, regenerable assets (and proves
them in CI); Perforce holds the hand-authored art. That is the honest shape of
"source control" here: the git side is complete and checked in; the Perforce
side is a documented posture with no committed config in this tree.

The downstream stages are described but not separately committed as jobs:
**publish** flows from the UE Editor through V1's Studio editorial workflow
(staging → preview → prod via environment-promotion gates); **hot-reload** has
UE clients pick up Game Feature plugins on a release cadence while the
[world server](./world-server-and-gateway.md) polls the asset manifest every 30
s to invalidate fallback bakes; and the nightly **fallback bake** consumes UE
cooked assets to emit glTF + KTX2 + lightmap variants for the
[Tier-2 fallback client](./tier2-fallback-web-client.md). Treat those three as
architecture intent backed by the monolith, not as code you can point at here.

Where authored content actually meets a client is the **launch BFF**. The routes
`apps/lilith/bff/src/routes/v3-lilith-launch-route.ts` and
`v3-lilith-launch-distribution.ts` (with `openapi/v3-lilith-launch.openapi.yaml`
and an integration test under `src/__tests__/integration/`) implement
`POST /api/v3/lilith/launch`, returning a launch decision — native, Pixel
Streaming, Tier-2 fallback, or static landing — with `decisionId`,
`selectedSurface`, `reasonCodes`, and a telemetry-ready `LilithLaunchDecision`
event (documented at `V3/api/v3-bff-openapi.md`). The router that decides _which
surface_ gets the authored world is covered in
[Tier 1 UE5 Client](./tier1-ue5-client.md) and the tier-routing page; this page
ends at the point the right tenant plugin is selected for delivery.

## Edge cases & connections

- **Descriptor ≠ generator.** `libs/v3/isis-world-asset/src/index.ts` looks like
  a world-asset _pipeline_ but is a typed **capability descriptor** — three keys
  (`asset-brief`, `lod-budget`, `provenance`) with operational-metric strings
  and a `v3IsisWorldAssetReadinessScore` that ratios matched capabilities. It is
  the readiness _contract_ for Isis world-asset generation, not the generation
  runtime; do not cite it as proof that V3 procedurally builds venues.
- **Tenancy and isolation continue elsewhere.** Tenants here are the UE
  _packaging_ units. Their RBAC, row-level `tenant_id` scoping, room-boundary
  segregation, and residency rules are modeled in V1's tenancy graph and are
  detailed in the monolith's "Data Architecture and Tenancy" section and the
  planned sibling [Data, Tenancy & Residency](./data-tenancy-and-residency.md).
  A tenant plugin and a tenant _realm_ are deliberately the same word for the
  same world, split across two layers.
- **Why the split is the source of truth.** The authoring table exists precisely
  so a new surface can't be added ambiguously — every authoring task has exactly
  one primary surface and one persistence format, which is what keeps "who edits
  what where" from forking between web and UE teams during a cross-team handoff
  (e.g. Saraswati editorial proposing a setlist on the web, the
  concert-production team assembling Sequencer cue tracks in UE).
- **Platform context.** The domain libraries cited here
  (`@oshun/tenant-tara-studio`, `@oshun/isis-world-asset`, and the V1 substrate
  they compose over) are catalogued in
  [Oshun Domain Libraries](../../platform/oshun-domain-libraries.html).

Together these three mechanisms — plugin packaging, the web/UE authoring split,
and the git/Perforce pipeline — are what let V3 treat a "world" as a unit that
can be authored by non-engineers, built into a chunked install, shipped or
withheld per region, and hot-loaded into a live room, all without an engine
rebuild.
