Offline Mobile Edits & Conflict Reconciliation

An offline edit is a measurement taken at one moment against a copy of the network taken at another, submitted into a network that has moved on since both. That is not a flaw in offline working; it is what offline working is. The engineering question is what happens when the crew’s observation meets the office changes made while they were out — and the answer most estates default to, whichever side the tool favours, quietly discards whichever source it was configured against. Losing office corrections is annoying. Losing field observations is corrosive, because the crew learns that recording things is pointless. This guide sets out per-field reconciliation between field and office, what to do when both sides are observations, and when to stop deciding and dispatch a check. It applies the ownership discipline from field data capture and mobile sync to the specific case of an offline shift.

Environment Prerequisites

  • A replica or offline map area with a recorded check-out moment, because the conflict set is everything that changed in the network since that moment.
  • Per-field ownership declared between field and office, and stored as configuration rather than embedded in a synchronisation setting.
  • Provenance on office changes. The reconciler needs to know whether an office value came from a human, a bulk import, or a telemetry feed, because a telemetry value is itself an observation.
  • Timestamps that are comparable. Field capture time, office edit time and telemetry event time must share a clock, or recency comparisons are meaningless.
  • A held-conflict queue with a service level, and a dispatch path for the checks it raises.
  • Python 3.11 with pandas>=2.0; this layer reasons about records and provenance rather than geometry, so it needs no spatial stack.

Schema-Aware Reconciliation Protocol — Run Before Applying Anything

  1. Establish the check-out moment and the conflict window. Everything the office changed between check-out and synchronisation is in scope; anything outside it is not a conflict no matter how similar it looks.
  2. Reduce each conflict to fields. A record-level conflict is almost always a small number of differing fields, and the ownership map only applies at field level.
  3. Attach provenance to the office side. A steward’s correction and a SCADA update are both “the office” and they resolve differently against a field observation.
  4. Normalise clocks before comparing recency. Mobile devices, the enterprise geodatabase and the telemetry historian frequently disagree by minutes; decide which is authoritative and convert.
  5. Identify contradictions, not just differences. A crew recording a valve as closed when the record says open is a contradiction about the physical world. A crew recording a condition grade the office never held is simply new information.

Minimal Reproducible Implementation

The reconciler below takes a batch of field edits, the office changes made during the window, and the ownership map, and produces a per-field decision with its reason. It applies nothing itself; it returns decisions for the applier to act on.

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from datetime import datetime

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

FIELD_OWNED = {"position", "condition_grade", "operable", "operational_status"}
OFFICE_OWNED = {"asset_class", "asset_type", "network_association", "work_order_status"}
OBSERVED_SOURCES = {"SCADA", "FIELD_INSPECTION"}     # office values that are measurements


@dataclass
class Change:
    field_name: str
    value: object
    at: datetime
    source: str            # FIELD, STEWARD, SCADA, IMPORT, FIELD_INSPECTION


@dataclass
class Decision:
    asset_id: str
    field_name: str
    outcome: str           # TAKE_FIELD, TAKE_OFFICE, HOLD
    reason: str


def reconcile_field(asset_id: str, field_change: Change,
                    office_change: Change | None) -> Decision:
    """Decide one field where a field edit may meet an office change.

    The office side may be absent, in which case the field edit simply applies. Where
    both exist, ownership decides — except when both sides are observations, where the
    newer one wins, and except where the two contradict each other about something
    physical, which is held for a check.
    """
    name = field_change.field_name

    if office_change is None:
        return Decision(asset_id, name, "TAKE_FIELD", "no competing office change")

    both_observed = office_change.source in OBSERVED_SOURCES
    same_value = field_change.value == office_change.value

    if same_value:
        return Decision(asset_id, name, "TAKE_FIELD", "both sides agree")

    if name in FIELD_OWNED:
        if both_observed:
            newer = "TAKE_FIELD" if field_change.at >= office_change.at else "TAKE_OFFICE"
            return Decision(asset_id, name, newer,
                            f"both are observations — newer wins ({office_change.source})")
        return Decision(asset_id, name, "TAKE_FIELD",
                        f"field-owned; office change from {office_change.source} logged")

    if name in OFFICE_OWNED:
        # A field edit that contradicts an office-owned value is information, not noise.
        return Decision(asset_id, name, "HOLD",
                        f"field contradicts office-owned {name} — dispatch a check")

    return Decision(asset_id, name, "HOLD", f"no ownership declared for {name}")


def reconcile_batch(field_changes: list[Change], office_changes: list[Change],
                    asset_id: str) -> list[Decision]:
    """Reconcile every field a crew touched on one asset."""
    office_by_field = {c.field_name: c for c in office_changes}
    decisions = [reconcile_field(asset_id, fc, office_by_field.get(fc.field_name))
                 for fc in field_changes]
    held = sum(d.outcome == "HOLD" for d in decisions)
    LOG.info("%s: %d field(s) reconciled, %d held", asset_id, len(decisions), held)
    return decisions
Where an offline edit sits relative to the office edits it will meet The replica is taken at check-out and the crew works against that state for the shift. Meanwhile the office continues: a SCADA-driven status update at mid-morning, a data steward correcting a classification at lunchtime, another crew posting a nearby edit in the afternoon. When the replica synchronises at the end of the shift, the field edit meets all three at once. Which of them it conflicts with depends on the fields each touched, not on the order they happened in. Check out replica taken at this state 07:00 SCADA update device status changes in the office 10:20 Steward edit classification corrected 12:45 Field capture crew records condition + position 15:10 Sync meets all three at once 17:30 the conflict set is decided by which fields were touched, not by the clock A whole shift of office change arrives at the moment the crew synchronises.

The clause worth arguing about is the one that holds when a field observation contradicts an office-owned value. It would be simpler to let the office win — that is what ownership says — but a crew reporting that a valve is a different type than the record claims has produced evidence the catalogue cannot. Holding it costs one review; discarding it silently costs the observation and, eventually, the crew’s willingness to make another.

Field against office, resolved per field rather than per record A field observation is the better evidence for anything the crew could see: the device position, its condition, whether it is operable. The office is the better source for anything derived from records the crew does not carry: classification against the engineering catalogue, network association, work-order status. Splitting per field means a single record can take the condition grade from the crew and the classification from the office, which is the correct answer and the one a record-level rule cannot produce. Field Field wins Office wins Why Position yes the crew measured it Condition grade yes the crew saw it Operable flag yes the crew turned it Asset classification yes catalogue, not sight Network association yes topology decides Work-order status yes the work system owns it Per-field ownership lets one record take the best of both sides.

Production Deployment Pattern

  1. Reconcile per shift, not per edit. A crew’s submission is one unit of work, and a reconciliation report covering the shift is far more useful to them than fifty notifications.
  2. Log the losing side always. Every decision should record what the other side held, so a later dispute has both values rather than one and a memory.
  3. Give held conflicts a dispatch path. A hold with no way to raise a check becomes a queue entry that ages. The check is usually five minutes of a crew’s time on their next pass.
  4. Report contradiction rates by asset class. A class where field observations frequently contradict the record is telling you the record is wrong at scale, which is a data-quality programme rather than a series of individual conflicts.
  5. Never reconcile silently on both sides. If both the field edit and the office change are discarded in favour of a merge, nobody can reconstruct what either party asserted.
  6. Feed the outcome into the crew application. The disposition path is the same one the validator uses, and reusing it means one channel rather than two.
What happens to a field edit that meets an office change on the same field When both sides touched the same field, ownership decides — unless the office change came from a source that is itself a field observation, such as a SCADA-reported device state. In that case the newer observation wins, because both are measurements and recency is the only discriminator available. Where neither side is a measurement, or where the two disagree about something physical that cannot be resolved from a desk, the conflict is held and a check is dispatched rather than a coin flipped. Field and office both changed the same field field owns it Is the office value also an observation? no No — the field edit wins, office change logged yes Yes (SCADA) — the newer observation wins office owns it Does the field contradict the record? no No — office value stands, field edit logged contradiction Yes — hold and dispatch a check A contradiction between record and observation is information, not noise.

Conclusion

Offline reconciliation is where an estate decides whose knowledge it values. Per-field ownership resolves most of the volume without a person; treating telemetry as an observation rather than as office data resolves most of the rest; and holding genuine contradictions preserves the field evidence that a record-level rule would discard. What makes the whole arrangement work is that every decision is logged with both values, so a disagreement becomes something to investigate rather than something to argue about. The remaining source of field-versus-record disagreement is position, and that is governed by what the receiver was capable of — the subject of GNSS accuracy requirements.

For authoritative reference, consult the Python datetime library and the pandas merge guide.