Resolving Branch Version Conflicts in Utility Networks

Every conflict is a question about the physical world wearing the costume of a data problem. Two editors changed the same valve; the tool asks which row to keep, but the real question is which one describes the valve that is actually in the ground. Treating conflicts as a merge exercise — favour the target, favour the edit, take the newest — answers the costume rather than the question, and produces a network that is internally consistent and externally wrong. This guide sets out how to decide conflicts consistently: which classes may be resolved by declared rule, which must reach a person, what evidence that person needs, and how the decision is recorded so the same disagreement is not settled differently next month. It is the human half of the loop that reconcile and post automation deliberately leaves open.

Environment Prerequisites

  • Branch versioning enabled on the utility network’s feature dataset, with the conflict table readable by the review tooling.
  • A per-field ownership map, version controlled, covering every attribute that two systems can write. A field absent from the map is not auto-resolvable by definition.
  • Both sides of every conflict retrievable — the value in the version and the value in the default version — because a review with one side visible is a rubber stamp.
  • A route to a field check. Some conflicts cannot be settled from a desk, and a process with no way to ask the field will settle them by guessing.
  • A decision log keyed by feature, so a resolution and its reasoning survive the version that applied it.
  • Python 3.11 with arcpy for reading the conflict table and applying resolutions programmatically once a decision exists.

Schema-Aware Review Protocol — Run Before Deciding Anything

  1. Confirm the conflict is real, not a reconcile artefact. A row touched by topology validation on both sides can appear conflicted with no human edit involved. Compare the actual values before treating it as a disagreement.
  2. Establish which fields differ. A conflict flagged at row level frequently involves one field. Reducing it to the differing fields is what makes the ownership map applicable.
  3. Check whether an association is implicated. A containment or connectivity row in the conflict changes the stakes: resolving it wrongly leaves a feature attached to nothing, which the asset hierarchy rules treat as a defect.
  4. Identify the editors and their context. A conflict between a bulk import and a field edit is usually resolved in favour of the field edit; a conflict between two field edits needs the later observation. The metadata answers this before any judgement is required.
  5. Ask whether the answer is knowable from the desk. If it is not, dispatch a check rather than choosing. A wrong resolution applied confidently is more expensive than a conflict left open for a day.

Minimal Reproducible Implementation

The helper below reads the conflict table, reduces each conflict to its differing fields, applies the ownership map where it can, and emits the remainder as review items carrying everything a person needs. It applies no resolution it cannot justify from the map.

from __future__ import annotations

import logging
from dataclasses import dataclass, field

import arcpy

logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
LOG = logging.getLogger("conflict-resolver")

# field -> the system that owns it; anything absent is never auto-resolved
OWNERSHIP = {
    "SHAPE": "GIS",
    "ASSETGROUP": "GIS",
    "ASSETTYPE": "GIS",
    "OPERATIONALSTATUS": "FIELD",
    "WORKORDERSTATUS": "CMMS",
    "LASTINSPECTION": "CMMS",
}


@dataclass
class Conflict:
    feature_class: str
    global_id: str
    version: str
    fields: dict[str, tuple[object, object]]     # field -> (version value, default value)
    is_association: bool = False


@dataclass
class Resolution:
    conflict: Conflict
    decided: dict[str, str] = field(default_factory=dict)   # field -> side taken
    review: list[str] = field(default_factory=list)         # fields needing a person

    @property
    def automatic(self) -> bool:
        return not self.review


def resolve(conflict: Conflict, side_of: dict[str, str]) -> Resolution:
    """Apply the ownership map to one conflict.

    ``side_of`` maps an owning system to the side that carries its edits in this run —
    for example {"GIS": "VERSION", "CMMS": "DEFAULT"}. A field with no declared owner,
    a differing geometry, or any association conflict is routed to review untouched.
    """
    out = Resolution(conflict=conflict)

    if conflict.is_association:
        out.review.append("association")
        return out

    for name, (in_version, in_default) in conflict.fields.items():
        if in_version == in_default:
            continue
        owner = OWNERSHIP.get(name.upper())
        if owner is None:
            out.review.append(name)
            continue
        if name.upper() == "SHAPE":
            out.review.append(name)          # geometry is never merged automatically
            continue
        side = side_of.get(owner)
        if side is None:
            out.review.append(name)
            continue
        out.decided[name] = side

    return out


def partition(conflicts: list[Conflict], side_of: dict[str, str]):
    """Split a run's conflicts into automatic resolutions and review items."""
    automatic, review = [], []
    for c in conflicts:
        r = resolve(c, side_of)
        (automatic if r.automatic else review).append(r)
    LOG.info("%d conflict(s): %d automatic, %d for review",
             len(conflicts), len(automatic), len(review))
    return automatic, review
How a single conflict is decided, in the order the questions must be asked The first question is whether the two sides touched different fields of the same row. If they did, and both fields have a declared owner, the merge is deterministic and needs no human. If they touched the same field, or the conflict involves geometry, a deletion, or an association, the question becomes which side reflects the physical world — and that is a question about the field, not about the data. The rule that keeps this safe is that no automatic resolution is permitted to choose between two claims about physical reality. Two versions changed the same feature different fields Both fields have a declared owner? yes Merge by ownership — deterministic, logged no No owner declared — hold, and declare one same field, geometry, delete or association Which side matches the field? known Field evidence decides — record what it was unknown Unknown — keep both as a flagged pair, dispatch a check No automatic rule may choose between two claims about the physical world.

Note what the code refuses to do. Geometry is listed in the ownership map and still routed to review, because knowing that the geographic system owns location does not tell you which of two surveyed positions is correct. Ownership decides who is authoritative for a field; it does not decide between two claims from the same authority.

A worked ownership map for the fields that conflict most often Ownership is declared per field and derived from which system can actually verify the value. Location and network association are verifiable only against the network model, so the geographic system owns them. Work status, labour and actuals are verifiable only against the work that was done, so the work-management system owns them. Device state is verifiable against telemetry or a field inspection, and the field wins over both offices. Lifecycle status is owned by the state machine rather than by any editor, because it is derived from the transitions that were authorised. Field Owner Verifiable against Conflict resolution Location and geometry GIS survey and topology GIS side wins Network association GIS connectivity rules GIS side wins Operational status field / SCADA telemetry, inspection newest field report wins Work status and actuals work management the work order CMMS side wins Lifecycle status state machine authorised transitions neither — replay the log Ownership follows verifiability, not seniority and not recency.

Production Deployment Pattern

  1. Run the partition immediately after each reconcile. Conflicts are cheapest to decide while both editors still remember the change, and the automatic half should never wait for the review half.
  2. Apply automatic resolutions with their rule recorded. Every merged field should carry the ownership entry that decided it, so an unexpected value is traceable to a policy rather than a mystery.
  3. Give the review queue a service level. A conflict left open blocks a post, and a blocked post eventually becomes a version nobody reconciles. A day is a workable target; a week is not.
  4. Attach both values and both editors to every review item. A review that shows only the incoming value is a rubber stamp, and rubber stamps are how field corrections get discarded.
  5. Dispatch field checks rather than guessing. Where the answer is a physical fact, a check is cheap next to the cost of a wrong barrier state or a wrong location in an isolation trace.
  6. Record the decision against the feature, not the version. The version is deleted after the post; the reasoning has to outlive it, or the same conflict is re-decided differently the next time it arises.
The path a held conflict takes from detection to a recorded decision A conflict the automation refuses to resolve becomes a queue item carrying both sides of the change, the versions and editors they came from, and the feature. An engineer reviews it with both values visible, and where the answer depends on the physical world, a field check is dispatched rather than guessed. The decision is applied in the version that will post, and the reasoning is recorded against the feature so the same conflict is not re-litigated the next time the two systems disagree about it. Reconcile job Review queue Engineer Field check held conflict with both sides attached assigned by conflict class physical question → dispatch a check observed state returned decision applied in the posting version reasoning recorded against the feature A decision with its reasoning attached is one that does not have to be made twice.

Conclusion

Conflict resolution is where a versioned estate either preserves or destroys the field knowledge its editors put in. Declaring ownership per field removes the volume that does not need judgement; routing geometry, deletions and associations to a person preserves the judgement that does. The decisions that reach a person are then worth recording against the feature, because a conflict between two systems tends to recur until the underlying disagreement is fixed. Once the policy is stable, most of the remaining versioning cost is contention rather than conflict — which is a question of whether a job needed a version at all, taken up in version versus snapshot isolation.

For authoritative reference, consult the ArcGIS Pro conflict-resolution documentation and the Python dataclasses library.