# Branched Component Variants

> A `remits-cli` skill reference. **Load this when** one account needs different behavior from a component another account owns, or you are working from a non-trunk checkout, or you are promoting a branch back to trunk.
>
> The table of contents below carries **real line numbers** (`- L84  Some Heading`), resolved when
> this file is installed, so they are never stale. Read the head, pick your sections, and offset-read
> only those. The entry text is the heading verbatim, so it also greps.

## Table of Contents

- [Branched Component Variants (per-account component overrides)](#branched-component-variants-per-account-component-overrides)
  - [Which world does your working tree resolve? (read this before you run anything)](#which-world-does-your-working-tree-resolve-read-this-before-you-run-anything)
  - [Two levers, two different questions](#two-levers-two-different-questions)
  - [The SDLC is identical on a variant branch](#the-sdlc-is-identical-on-a-variant-branch)
  - [Where am I in the promotion loop?](#where-am-i-in-the-promotion-loop)
  - [Subscribing, unsubscribing, retiring](#subscribing-unsubscribing-retiring)
  - [Danger profile on a variant branch (different, not absent)](#danger-profile-on-a-variant-branch-different-not-absent)
  - [Inspecting branches and drift](#inspecting-branches-and-drift)
  - [Diagnosing a variant](#diagnosing-a-variant)

## Branched Component Variants (per-account component overrides)

When one account — often a customer nested several levels down — needs *slightly* different behavior from
a component owned by its platform or product account, the answer is a **branch variant**: a durable,
branch-scoped overlay of that component, resolved only by accounts subscribed to that branch. The wrong
answer is per-account `if/then` logic inside the origin component.

> **`features/subscriber-branch-promotions.md` (`mcp_get_guide`) is the guide for this.** It owns the
> mental model, the five scenarios you will actually meet, the full promotion procedure, what happens to a
> `new_` component across a promotion, merge-conflict resolution file by file, removals, and the approval
> gates for running a promotion as a coding agent. **Load it before promoting anything.** This section
> covers only the CLI surface and the traps that bite at the command line.

Three facts that everything else follows from:

- **A branch is not an account.** Subscription is many-to-many, it applies to the subscribing account
  *and its descendants*, and a subscribing account still has its own trunk components which merge on top.
- **A variant keeps the origin's component id** — it is an overlay, not a copy. That is what makes drift
  computable.
- **Sparseness is computed at sync time, not declared.** A git branch physically contains every file; only
  the ones whose content *differs from trunk* become variants. A file that is absent becomes a
  **tombstone**; a file whose id prefix is not a live trunk id (`new_Foo.groovy`) becomes a **branch-only**
  component keyed by name.

### Which world does your working tree resolve? (read this before you run anything)

You will work from **different checkouts of the same repo**, and they behave differently on both ends of
the loop. **You only ever stage the edits you intend to test** — the world beneath them comes from the
account, not from what you named your git branch:

| Working tree | Staging scope | A run resolves | `components sync`/`commit` writes |
|---|---|---|---|
| **trunk** (`main`, or whatever `account-info.json` says) | that branch | trunk + **each account's subscribed** variant branch (production semantics) | the **live component rows** — full reconcile, creates/updates/**deletes** |
| **a variant branch** (it has committed variants, or an account subscribes to it — e.g. `forked`) | that branch | trunk + **that branch's** overlays | **`ComponentVariant` overlays on that branch only** — never touches trunk rows |
| **any other branch** (`codex/x`, `feature/y`) | that branch | the **same world as trunk**: trunk + the account's subscribed branch (`forked` for its subscriber) | **refused** (`feature_branch_landing`) — merge into the branch it resolves and land from there |

Use those names in handoffs: **git branch** for the checkout, **staging lane** for the temporary
account/user/git branch/workspace overlay, and **component branch** for the durable variant world. A
support ticket, component branch, and git branch can all be different at the same time.

A branch like that is a **feature branch**, and it is safe to *run* from: stage only the components you edited,
and runs still resolve every `forked` overlay for the subscriber. Do **not** stage untouched components to
"restore" something reported missing — check `components status` first. It is never a place to *land* from.
For parallel agents the preferred shape is still the real branch name plus a workspace; see
`features/multi-agent-development.md` ("Per-agent git branches: safe to run, never to land").

Do not infer this from the branch name. Ask:

```bash
remits-cli components status
```

```
Working tree: VARIANT BRANCH "feature_branch"  (trunk is "main")
  runs resolve:   trunk + the 'feature_branch' variant overlays
  variant world:  feature_branch  [branch-has-variants]
  commit writes:  ComponentVariant overlays on 'feature_branch' (never touches trunk rows)
  variants stored on this branch: 3
  subscribing accounts: 101 (Acme Child)
```

From a branch that is not a variant branch the header reads `FEATURE BRANCH` and `runs resolve` names the
subscribed branch, tagged `[subscription-fallback]`. `test run`, `token` and `tool` responses carry the
same `variantBranch` / `variantBranchSource` fields, and a verification envelope records that world.

**The precedence trap that costs the most time:** a variant branch's world **outranks every account's
subscription**. So running a Test suite that asserts *production* semantics from a **variant checkout**
pins every account in that suite — including fixture accounts subscribed to their own generated branches —
to your branch, where they have no variants, and they all read trunk. The suite fails in a way that looks
exactly like a resolution regression. (Live example: a branch-variant suite scored 4/15 from a variant
checkout and 15/15 from trunk, with no code difference.) **Run branch-variant suites from trunk, or pass
`--variant-branch none`.** Before concluding "variant resolution is broken", re-run from trunk.

**Two other things differ from trunk while you work on a branch:**

- **Keep the origin id in the filename.** `50_ExtractInvoice.groovy` on the branch overlays Action 50.
  That is what preserves component identity and lets drift be computed against the origin.
- **Nothing is renamed.** A variant sync never renames files and never repoints the account's trunk
  branch. A `new_*` file stays `new_*`.

**Start from a current branch, not just an isolated workspace.** Before the first edit in a variant
checkout:

```bash
git fetch origin
git status --porcelain
git log origin/<branch>..<branch>              # must be empty: nothing local-only
git log <branch>..origin/<branch>              # must be empty: not behind the remote
remits-cli components promotion --branch <branch>
```

`components promotion` reads the remote and reports whether the branch is also current with trunk. If it
says `diverged` or `STALE`, merge trunk in or sync first, as instructed, before writing new work. A
workspace lane prevents another agent from overwriting your staged Redis snapshot; it does not say which
commit your files are based on.

**Branch-local `account-info.json` can describe a subscriber.** On a variant branch the repository is
still the OWNER's component repo — files overlay that owner's trunk ids and sync writes overlays owned by
that owner. But when the checkout was synced from a subscribing account reached through an
`AccountRelationship` edge, and the branch has **exactly one** subscriber, the branch's
`account-info.json` is intentionally rooted at that subscriber. With **more than one** subscriber the
refresh is skipped (the sync result says so under `accountInfo.skipped`/`reason`) and the file keeps
describing the owner, which is at least true for all of them. Overlay ownership is unaffected either way.

Read `resolution` before acting from any checkout — in particular `resolution.accountId` (the account this
checkout should be treated as; **read this, not the top-level `id`**), `resolution.role`,
`resolution.componentOwnerAccountId` (whose trunk ids the files overlay and whose overlays a sync writes),
and `resolution.componentBranch` / `resolution.scopeAccountId` (the branch and the path anchor that made
this subscriber resolution possible). If a variant checkout's `account-info.json` is stale
and still names the owner, run the first repair sync with an explicit subscriber target
(`remits-cli components sync --account-id 101`), then `git pull --ff-only`.

### Two levers, two different questions

- **`--as-account <id>`** changes **who** the run executes as, so that account's own edge picks the branch.
  Answers *"what does customer X actually get?"* Only narrows downward (the target must be reachable from
  an account you already have access to). It works for a `parentId`-less, membership-only subscriber too:
  the anchor is derived from the branch you name, or from the account's single branch subscription. It
  **refuses to guess** when an account has several edges each carrying a different branch — the run then
  resolves trunk, and you must name the branch.
- **`--variant-branch <name>`** changes **which branch**, from any checkout. Answers *"what does branch Y
  look like?"* — most useful for verifying a freshly committed branch **before** any edge subscribes to
  it. `--variant-branch none` (or `trunk`) forces production/subscription semantics without leaving the
  branch.

Both work on `remits-cli test run`, `remits-cli token`, `remits-cli tools`, `remits-cli tool`, and
`remits-cli corpus`, so tests, browser URLs, tool discovery, tool execution, and corpus operations can all
inspect the same committed variant world.

The strongest end-to-end proof for a UI-visible variant is a token, not a log line:

```bash
remits-cli token --path embeddable/index/50 --as-account 101   # subscriber -> variant
remits-cli token --path embeddable/index/50                    # owner      -> trunk
```

Each answer carries a **`resolution`** block for the account the token executes as — `role`,
`componentBranch` and its owner, `resolvedDatabaseName`, `resolvedDomainName`, `scopeAccountId`, and a
one-sentence `summary`. Read it before opening the URL: `accountId` alone does not say whether a branch
overlay applies or which storage namespace the page will read, and those are exactly what
`--as-account` is being used to change.

Separate branch fields, because these questions answer differently and can disagree:

| Field | Answers |
|---|---|
| `componentBranch` | what **this token** will resolve |
| `componentBranchSource` | `probe` (an explicit `--variant-branch`), `subscription` (the account's edge), or `trunk` |
| `subscribedComponentBranch` | what the **account graph** says, independent of this token |
| `componentBranchAnchored` | whether **this resolution actually reached** that subscription |

A single `componentBranch` field would have reported `trunk` under `--variant-branch X`, for a token that
resolves `X`.

**Subscribing to a branch and resolving through it are different facts.** An account subscribes on an
edge; a *request* resolves through that edge only when something named the path (`--as-account`, the
edge's own host, an explicit `--variant-branch`), or when its links leave no doubt — a single upward link, or
several with exactly one carrying a branch, which every account beneath it inherits too. An account whose
several links carry several branches refuses to guess a path. So this is a normal, explainable state:

```
"componentBranch": null,              // this token resolves TRUNK
"componentBranchSource": "trunk",
"subscribedComponentBranch": "forked", // ...but the account does subscribe
"componentBranchAnchored": false       // ...and nothing anchored this request to it
```

Reading `componentBranch` alone there tells you the account is on trunk, which is true — and leads you to
conclude it has no branch, which is false. If several edges carry branches, none is picked for you:
`subscribedComponentBranch` is `null` and `subscribedComponentBranches` lists them.

Under a probe, `componentOwnerAccountId` is the **nearest account on the token's resolution path that owns
variant rows for X** — not an edge lookup, which answers `null` for the ordinary case of a variant
committed before anything subscribes to it. `null` means no account on that path owns rows for X, and what
that implies depends on the account, so read `summary`:

- **on trunk** — the page resolves what it would without the probe; the branch is not committed yet.
- **resolving through a subscription** — the probe **suppresses** it. `variantBranch` outranks the
  subscription before it is consulted, so the page resolves **trunk**, not the overlays that account
  normally gets. Drop `--variant-branch` to see the subscription.
- **subscribed but not anchored** — it was already resolving trunk before you probed. Dropping
  `--variant-branch` will *not* by itself show you the branch; name the path as well.

The page itself then states the same facts. Its hidden `remits-session-info` line — which appears in an
accessibility snapshot with no script — carries `Account` (what it runs as), `Addressed Account` (what
the URL/token named), `Data Lane`, `Component Branch`, `Variant Applied`, and `Scope Account`.

**`Component Branch` is the branch SELECTED; `Variant Applied` is what actually overlaid.** They are two
fields because a branch can be selected and overlay nothing: `--variant-branch missing_branch` reports
`Component Branch: missing_branch` while the page renders trunk components. So read `Variant Applied` —
the `ComponentVariant` row id this page's component resolved through, or `none` — when the question is
"did my variant apply?". That is a far sharper signal than inspecting the rendered markup for a style you
expected.

If `components status` shows staged entries that you cannot safely clear, isolate verification in an
unused staging namespace instead of deleting someone else's cache:

```bash
remits-cli test run --test "Invoice Tests" --branch promotion-check-empty --variant-branch none --as-account 101
remits-cli token --path embeddable/index/50 --branch promotion-check-empty --variant-branch none --as-account 101
```

Here `--branch` is only the CLI staging-cache namespace, and `--variant-branch none` keeps runtime
resolution on production/subscription semantics. Do **not** use this pattern with `components sync` or
`components commit`: for those commands `--branch` is the GitHub branch to reconcile.

### The SDLC is identical on a variant branch

```bash
git fetch origin
git checkout feature_branch
git pull --ff-only origin feature_branch
remits-cli components promotion --branch feature_branch
# edit components/actions/50_ExtractInvoice.groovy   (KEEP the trunk id)
remits-cli components stage          # Redis, scoped to this branch — same as always
remits-cli test run --test "Invoice Tests"                    # the feature_branch world
remits-cli test run --test "Invoice Tests" --as-account 101   # ...as the real subscriber
git add -A && git commit -m "..." && git push origin feature_branch
remits-cli components sync --dry-run # inspect overrides/additions/tombstones without writes
remits-cli components sync           # writes ComponentVariant overlays ONLY
```

`components stage` validates changed runtime-compiled source through the active branch/workspace lane,
and `components sync` validates changed/new Groovy source before writing a `ComponentVariant`. This is
intentional: a branch overlay that cannot compile should fail at staging/sync time, not later when the
subscriber's workflow first resolves it. `Agent` files in `components/agents/` are the `Utility`
component kind internally; the CLI and validator normalize both names to the same target.

Promotion back to trunk is a **git** operation followed by a **trunk** sync — the platform merges nothing
for you, and merging a branch promotes its **deletions** as hard deletes. Do not improvise it: follow
`features/subscriber-branch-promotions.md`. For a real trunk promotion with many `new_` files, use
`remits-cli components sync --summary --timeout-ms 300000`; the platform may need longer than the default
60 seconds to create rows, push rename/meta commits, and regenerate account metadata.

### Where am I in the promotion loop?

```bash
remits-cli components promotion            # the branch you are standing on
remits-cli components promotion --branch forked --json
```

**Run this before every promotion step and after every one.** It reports only — no git, no writes — and it
reads the remote (which is what the platform syncs from, not your working tree). It works from either
checkout, because it resolves the branch's owner itself. It exits non-zero while blockers remain, so treat
that as "do not proceed".

| Phase | Meaning | Next |
|---|---|---|
| `converged` | branch == trunk | nothing to do; this is also what a **finished** promotion looks like |
| `ready` | ahead of trunk, current with it | promote |
| `diverged` | behind trunk | `git merge <trunk>` into the branch, re-sync, re-read the plan |
| `awaiting-merge-back` | fully contained in trunk, trunk has moved on | **steps 4–5 are owed** — merge trunk back and re-sync |

Two things it tells you that nothing else does:

- **Which commit the stored counts describe, on BOTH axes.** An overlay is stored only while a file
  differs from trunk, so the stored set is a statement about a (branch, trunk) pair and either side moving
  makes it stale: `[STALE — does not match the branch HEAD]` (the branch was pushed since) or
  `[STALE — matches the branch HEAD, but trunk has moved since]`. The second is the easy one to miss —
  measured live, a branch at its own synced HEAD reported 2 stored overlays against a real plan of 21.
- **That a promotion is unfinished.** `awaiting-merge-back` is what a promotion that *looked* successful
  leaves behind: trunk is correct and its tests pass, while the subscriber silently keeps resolving its
  pre-promotion overlays — including for any fix made afterwards. **A trunk sync succeeding is step 2 of 5,
  not completion.**

### Subscribing, unsubscribing, retiring

```bash
remits-cli components branch <name> --subscribe <accountId> [--parent-account <id>] [--domain <host>] [--dry-run] [--confirm-primary-edge]
remits-cli components branch <name> --unsubscribe <accountId>
remits-cli components branch <name> --retire [--force]
```

Branch administration commands act **as the account you are running from**, which is treated as the branch
**owner** — run them from the owner's trunk checkout. Authorization is downward-only. (`--retire` checks
this and refuses from a non-owner; the read commands below resolve the owner for you.)

- **`--subscribe` sets the branch on an edge that must already exist — it cannot create one.** Failure
  reads *"Account N has no relationship edge to subscribe; add a membership edge first"*. Create it with
  `mcp_account_user_admin` `action:'edge_add'` (`targetAccountId` = the child, `parentAccountId` = the
  owner), which also accepts `branchName` so you can create and subscribe in one call.
  - **Many older accounts have no relationship row at all** — the edge table was added after the fact and
    never backfilled, so an account whose parent link is only `Account.parentId` resolves fine but has
    nothing to attach to. `mcp_account_user_admin` `action:'reparent'` with the account's *current* parent
    is the one-call repair: it creates the missing primary edge and changes nothing else.
- **When the account has several parents, say which edge you mean** with `--parent-account <ownerId>`.
  Without it the CLI picks the edge to the owner whose branch you are managing, then falls back to the
  account's primary edge — which may not be the one you intended. `--subscribers` prints
  `via primary|membership edge -> parent N` so you can confirm.
- **Primary-edge subscriptions are structural.** If the selected edge is the account's `parentId` edge,
  subscribing it makes that account and descendants resolve the branch by default even though `parentId`
  still points at the same parent. The server refuses this write unless you pass
  `--confirm-primary-edge`; run `--dry-run` first and prefer a membership edge for fork/pilot
  subscriptions.
- **Retiring is explicit.** Deleting the *git* branch does **not** remove its overlays; subscribers would
  keep resolving a branch that no longer exists. `--retire` refuses while subscribers remain unless you
  pass `--force`, and it refuses (naming the owner) when run from any account other than the branch owner.
- **Reads work from any checkout.** `components branch <name>` (status, `--diff`, `--subscribers`) and
  `components promotion` resolve the branch owner the account actually resolves the branch from —
  including an inherited subscription held by an ancestor — and report it as `branchOwnerAccountId`.
  `components branches` lists branches this account OWNS; from a subscriber it also names the branch it
  resolves and from whom. If `components status` prints `INHERITED BRANCH`, this account gets the branch
  through an ancestor's subscription: a sync from here varies THIS account's own components (for it and the
  accounts beneath it). To change the owner's components, work in the owner's repository on that branch.
- **A commit must push to the repository the platform syncs.** `components status` names it (`sync reads
  repository`). `components commit` refuses before any git write, and `components sync --safe` refuses,
  when this checkout's `origin` is a different repository; a plain sync warns.
- **Every prompt varies on a branch, including the README and agent prompts.** Edit the repo-root
  `README.md`, an agent's `.md` in `components/agents/`, or a prompt in `components/prompts/` exactly as
  you would any other component file: stage, verify, sync, and subscribers resolve it; merging to trunk
  lands it. The README becomes an overlay of the account's README prompt (a branch without `README.md`
  reverts to trunk's); an agent's prompt travels inside that agent's overlay.

**Every component kind can be varied** — `schema`, `reader`, `action`, `embeddable`, `i18n`, `htmltemplate`,
`rule`, `test`, `utility` (agent), `tool`, `prompt` — including tombstones and branch-only additions. A
**schema** variant deserves the same care as a trunk schema change: it can change the JSON schema *and*
`collectionName`, so it changes validation and where the subscriber's documents physically land. A
branch-only agent (a `new_*` file under `components/agents/`) defaults to `type: AI` so agent lookups
find it.

A variant speaks the same `.meta.yml` vocabulary trunk does; keys outside that set are ignored on purpose,
because trunk cannot express them either — allowing them would mean a branch behaves one way and silently
loses that behavior the moment it is promoted. **One documented exception: an agent's `tools:` list cannot
be overridden by a variant.** It maps to a GORM association that `agent()` populates from the trunk row, so
a `tools:` list on a branch is ignored. Change trunk, or have the agent select tools at runtime.

### Danger profile on a variant branch (different, not absent)

The `component-integrity.md` worst case — *"a missing file hard-deletes a live component"* — **does not
apply on a variant branch.** Variant sync writes overlay rows only; it cannot create, delete, rename, or
overwrite a trunk component. That makes a variant branch a genuinely safer place to iterate.

The analogous hazard is different, and you must still respect it:

- **A trunk component with no file on the branch becomes a TOMBSTONE**, which *hides* that component from
  every subscriber. It is reversible (restore the file, re-sync) and never touches trunk — but to a
  subscribing account it looks exactly like the component was deleted. Confirm every omission is
  deliberate before syncing.
- **Deleting a file means "remove the component", not "stop overriding it."** To withdraw an override,
  make the file identical to trunk again — the sync then removes the variant row.
- **Keep the branch rebased.** A branch physically carries every file, so anything trunk added *after* the
  branch was cut is absent from it. The sync now asks git which of those absences are real deletions
  (comparing against the merge base) and refuses to tombstone the rest, reporting them as
  `skipped: absent from the branch but never deleted on it`. Treat any such entry as "merge trunk in" —
  if an older sync already stored one of those absences as a tombstone, a non-dry-run sync prunes it and a
  dry run reports `wouldPrune: true`. The classification falls back to a conservative ratio guard when
  GitHub's compare is unavailable or its file list comes back truncated, which the sync output says
  explicitly.
- **Use `components sync --dry-run` before risky variant syncs.** It reports `overridden`, `added`,
  `removed`, `unchanged`, `skipped`, and `errors` without writing rows, caching the sync SHA, or clearing
  staging. `overridden`/`added` are the branch's WHOLE overlay set; read the `Overlay changes:` line (or
  `writes` in `--summary`) for what this sync actually changes, and `storedCurrent: true` on an entry for
  an overlay that is already stored as-is. Existing overlay ids appear as `variantId`; an `unchanged` row with `pruned:true` means the
  branch has converged back to trunk and the overlay would be removed. It is rejected on trunk, and
  `components commit --dry-run` is unsupported because `commit` first merge-stages changed source for
  compile validation, then performs local git writes before syncing. That internal stage does not
  reconcile the lane; use `components stage --workset` before behavioral verification.
- **The sync refuses a wholesale removal.** Above roughly a third of a kind — or **100% of a kind at any
  size** — it aborts that kind, reports why, and points at a rebase. Rebasing is almost always the real
  fix. Only when the removals are genuinely deliberate, re-run with `--force-tombstones`.
- **Deleting a trunk component cascades**: its overlays on every branch are removed with it.
- **A removal you did not author usually means TRUNK is drifted**, not that the branch deleted something.
  A component that exists in the DB with **no file on trunk** is missing from every branch too, so it
  shows up as a phantom `removed` in *every* branch preview — while the trunk sync separately tries to
  hard-delete it on every run. Check whether the file exists on trunk before "fixing" it on the branch;
  the repair is to mirror the live DB source back into the trunk repo (`component-integrity.md`).

### Inspecting branches and drift

```bash
remits-cli components branches                                  # branches with variants, counts, drift
remits-cli components branch feature_branch                     # overridden / added / removed + subscribers
remits-cli components branch feature_branch --diff 50 --component-type action
remits-cli components branch feature_branch --subscribers
remits-cli components branch feature_branch --json              # the stored overlay content
```

**Drift is the number to watch.** Each variant records the trunk content hash at the moment it was cut
(`originHash`). When the origin component later changes, the variant is reported **DRIFTED** — the branch
is now based on a stale version and someone should reconcile it. Before editing an origin component, check
whether variants of it exist: the owner account's `account-info.json` carries a `componentBranches`
summary, and `components branches` gives the live view. A change to the origin silently drifts every
branch that overlays it.

**`components branches` only lists branches that already have overlays.** A branch you just pushed is
invisible here until its first sync — that is not an error. Preview it by name (`components sync --dry-run`
from that checkout). Its first real sync (or commit) needs `--create-variant-branch`: until a branch has
variants or a subscriber the platform treats it as a feature branch and refuses to land it, so creating a
new variant branch is always a stated decision, never a side effect of a feature branch's name.

For a nested branch cut from an existing variant branch, seed the platform overlays explicitly after the git
branch is created and pushed:

```bash
remits-cli components branch forked --copy-to sandbox --dry-run
remits-cli components branch forked --copy-to sandbox
remits-cli components sync --safe --branch sandbox
```

The copy is a DB overlay bootstrap, not a git write. It makes inherited overlays from `forked` already
`storedCurrent` on the first `sandbox` sync, so the safe diff gate only has to account for the new branch's
real file changes.

**Copy before you subscribe, or the copy needs `--force`.** The copy refuses a target that already has
stored overlays *or* live subscribers, and that second guard fires even when the target has no overlays at
all — a branch somebody is already resolving is code those accounts are running right now, and replacing it
has to be a stated decision. Seeding first and subscribing after keeps the plain form working. The copy is
also all-or-nothing: the delete of any replaced overlays and every inserted row share one transaction, so a
failure leaves the target exactly as it was rather than half-populated.

Run it as `--dry-run` first. The plan reports `plannedCopies` — the number the real run will write — which
is not always the source branch's row count: a source overlay that has become identical to trunk in both
content and metadata is sparse, is not stored, and is listed as skipped instead of being silently dropped
from the total.

**Trunk moving also invalidates a branch.** Variant sparseness compares branch content against *current*
trunk, so a trunk change can make an overlay obsolete without the branch changing at all. A trunk sync
drops the affected branches' cached sync verdicts, so the next `components sync` on the branch really
re-evaluates instead of answering *"No changes detected"*. After promoting anything, re-sync each live
branch.

**The admin UI has the same surface**, which is what to point a non-CLI user at: the *owner* account page's
**Component branches** box (per-branch counts, drift, subscribers, and Preview / Sync / Retire), and the
*subscriber* account page's **Branch variants** box plus **GitHub → Fetch `<branch>` variants**. Both open
the same result panel — grouped plan, subscribers, a Monaco diff of trunk vs branch per changed field, and
a confirm-gated override when the removal guard refuses. Preview there is the same `--dry-run`.

### Diagnosing a variant

- `remits-cli components branch <name> --json` — the stored overlay content and what it overrides.
- The compile signature `variant:<id>:<hash>` in `Using Cached BCD` logs — proves a variant actually ran.
- A test/tool response reports `testComponentSource` as `staged` | `variant` | `db`, so you can see which
  layer the run resolved without reading logs.

> **A trunk lane never overrides a variant.** Staged edits from your account's trunk branch do not reach an
> account whose branch overrides that component — production behaves the same way — so `test run` and `token`
> list them under **NOT RUN FOR THIS ACCOUNT** (`stagedMaskedByVariant`). To change what that account runs, edit
> the component on its variant branch.
>
> **A populated NON-trunk staging lane makes a variant look broken.** Anything carrying a CLI `TestMode` — a
> `remits-cli token` URL, `/s/<tokenKey>/...`, an `X-Auth-Token` request, a script-loader embed — resolves
> a variant-branch or feature-branch lane's STAGED layer, which outranks the variant. So with components staged under the same branch/user, a
> tokenized page reports `Component Branch: <branch>` but `Variant Applied: none` and renders trunk, while
> an anonymous `?account_id=` request to the same page reports the variant. That is the documented
> precedence (staged -> variant -> trunk) working correctly, and it reads exactly like "tokens break
> variant resolution". **Run `remits-cli components clear --all` before verifying variant resolution
> through any tokenized entry point**, or check `remits-cli components status` first.
