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
arcpyfor reading the conflict table and applying resolutions programmatically once a decision exists.
Schema-Aware Review Protocol — Run Before Deciding Anything
- 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.
- 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.
- 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.
- 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.
- 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
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.
Production Deployment Pattern
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
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.
Related
- Up to the parent topic: Branch Versioning & Conflict Resolution
- Up to the section: Topology & Tracing Workflows
- Reconcile & Post Automation for Utility Network Edits
- Version vs Snapshot Isolation for Batch Jobs
- Syncing Work Orders Between ArcGIS and Maximo
For authoritative reference, consult the ArcGIS Pro conflict-resolution documentation and the Python dataclasses library.