Skip to content

Fork, Rebase & Conflict Resolution

Every Element and Scene instance, and every campaign fork of a Project, starts from a frozen, immutable version — nothing drifts live underneath something already delivered — and catching up to a newer base is an explicit rebase: swap in the new base, replay the fork's Change[] diff on top of it.

Fork and rebase

A fork is a frozen base version plus a Change[] against it (The Change Model → Two words, defined once), whether that fork is a draft of the next version (its parentVersionId recording the lineage, Identity, Versions & Schema Versioning) or a placement's overrides in the Scene or Project that placed it. Either way an already-delivered creative cannot change underneath a client without an explicit action — directly protecting against a recurrence of the original BSR incident, one level up. Catching up to a newer base is an explicit rebase: seed a new fork from the newer frozen base and replay the old fork's Change[] on top of it. A committed version's own changes are never mutated by this; they are read and replayed into the new fork. For a placement, the repoint is itself a Change against the containing document — rebaseInstance (The Change Model) — since the containing version's own content is frozen.

This is the same proven pattern showing up a third time, not a novel mechanism: a Campaign already pins an exact templateVersionId and shows a "Template was updated" banner with an explicit Reload action when the base moves; retail_template_versions is already DB-enforced immutable (Storage & Versioning); a Template Preset freezes at drag-drop the way a fork freezes at commit.

flowchart LR
    B1["base v1"] --> F["fork: v1 + Change[]"]
    B1 --> B2["base v2"]
    B2 --> RB["rebase: v2 + replayed Change[]"]
    F -. "explicit rebase" .-> RB
    RB --> CS["confidence-scored auto-resolution<br/>(render-and-compare)"]
    CS -->|"below threshold"| M["manual choice per item<br/>use this instead · approve · discard"]
    CS -->|"all high confidence"| A["Approve Migration<br/>(always, never skipped)"]
    M --> A
    A --> C["commit"]

Conflict resolution

Decided

The system attempts confidence-scored auto-resolution of every changed reference; anything below the confidence threshold is surfaced individually with real choices (use this instead / approve / discard); and — regardless of whether anything needed manual attention — a mandatory final Approve Migration step gates every rebase commit. Nothing that picks up an upstream change ever commits without a human seeing it.

The exemption is keyed on nothing upstream having moved, not on the parent pointer alone: a commit whose parent is already currentVersionId and whose diff repoints nothing has nothing to reconcile and needs no such step (Identity, Versions & Schema Versioning → Commit rule). A diff containing a rebaseInstance is a rebase commit for this gate's purposes even when its own parent is current, because it is pulling in a version the author wasn't watching (The Change Model).

"Approve Migration" is the UI label for this gate on every rebase, whatever moved the base — a content edit, a brand-style change, or a schema-version migration. It is not specific to the schema sense of "migration" used on the identity page or to NEO-1657's tooling.

The confidence score has a concrete mechanism, or at minimum a primary component: the visual-regression render-and-compare in Testing, Preview & Incident Response decides whether a changed reference needs a human's attention — the same category of manual step git requires when a rebase produces a real conflict rather than a clean fast-forward. That comparison is agnostic to why a base changed, so a migration-driven rebase is scored exactly like a content-edit-driven one. Structural signals feed it too: a tracked rename resolves with high confidence (the NEO-1669 opaque-config-key precedent, where a reference survives a base reshape by re-keying rather than by name), while a removed reference with no trace of a replacement scores low. Exactly which renders are compared is still open, below.

What the hybrid delivers is zero manual conflict-resolution for most rebases — not zero attention: every rebase, including a clean one, still requires the explicit Approve Migration click. That's a deliberate trade for the safety property that nothing commits without human approval, made without the heaviest cost of a full staged-rebase UI (a mandatory three-way diff on every rebase — the default view is a summary, with drill-down only into whatever's flagged).

Decided: the fleet-scale burden of the approval gate is deliberately not pre-planned

Per direct instruction, the aggregate cost of many forks each needing their own rebase-and-approve is something to work out from real operational experience once it's a live problem, not something designable in the abstract now.

Staleness detection

Checked lazily and cheaply — a version pointer per instance ("is my pinned version behind my identity's currentVersionId?") — not via an eager reverse-dependency graph. The expensive "who's affected, proactively" version of this problem is deferred past alpha.

Decided: staleness surfaces as a badge

Not a dashboard, not a push notification — cheapest to build and sufficiently visible for a small team. Two states, because two are all the model defines: stale (a newer version exists, and it can be rebased) and current. Retention and archival aren't designed — see Open questions — so there is deliberately no third badge state to render.

Because staleness is defined as "behind currentVersionId", anything that advances the pointer — an ordinary commit, or a confirmed schema migration run from an item's catalogue view (Identity, Versions & Schema Versioning) — surfaces this same badge on every existing instance, with no second mechanism.

One thing the lazy check deliberately doesn't provide is a reverse lookup from a version to everything referencing it. Two mechanisms elsewhere in this brief need exactly that, on demand rather than proactively: narrowing an Element's sharing list has to know which containers reference it (Brand Scoping & Brand Style), and incident response wants to know which campaign forks pin a bad version (below). Both need a query over reachable references, not the eager push-based notifier that's deferred.

Abandoned or stale in-progress rebases

Decided: no special handling for alpha

If the base changes again before a user hits Approve, the stale in-progress rebase is simply discarded; the user re-runs rebase from scratch against the new base.

Risks

  • A race between two near-simultaneous rebases against the same base version. The same class of problem covers two users independently triggering "run migration" against the same stale item at once — not a separate race to design for; the commit rule (Identity, Versions & Schema Versioning) makes the second one rebase again rather than clobber.
  • A revert moves the pointer backwards, which the badge doesn't see. Instances pinned to the reverted-away version are not "behind" the restored one, so staleness alone never flags them (Testing, Preview & Incident Response).
  • An on-demand reverse-reference query is alpha-scope, not deferred (above), even though the proactive notifier built on the same data isn't.

Open questions

  • Whether versions are ever archived, and what that means. Every version is immutable and kept, which is what audit and revert depend on, but nothing says whether an old version is ever retired from a catalogue view, whether a reference pinned to an archived version still resolves at render, or who may archive one. Until that's designed there is no archived state, and the badge carries two values.
  • What the render-and-compare actually compares. A rebase exists to pick up an upstream change, so a naive before/after of the rebased piece diffs most exactly when the rebase is most legitimate (a brand primary colour changing flags every Project on every frame), while a dropped override that happens to be visually small scores high. The comparison that matches the intent is "does the fork's contributed delta survive" — new-base-with-changes against new-base-alone, compared to old-base-with-changes against old-base-alone. Not decided; the per-Change-type confidence rule beyond this is undesigned.

Done when

  • A fork can be rebased onto a newer base, with the replayed Change[] compared by the visual-regression mechanism under whichever comparison rule the open question above settles, low-confidence items surfaced individually, and no rebase commit possible without the Approve Migration step — verified by ordinary tests of the confidence-scoring rule's verdicts (Testing, Preview & Incident Response).