Conflict policy — frozen as lastWriteWins, and DECLARED rather than defaulted.
This is what the code already does, but by accident: every strategy fell through to a
forced overwrite. The change is not the outcome, it is that the conflict becomes
detectable and logged, because the push now carries baseVersion.
The rationale is the field: an operator is the authority on what they have just
observed, and a blocking dialog raised offline, alone on site, gets clicked at random.
Its twin in editor, and what their relationship had no home to say
⚠️ A SECOND type describes the same arbitration, under another name and in another
package: ConflictStrategy (@geoleaf-plugins/editor,
persistence/conflict-resolution.ts), which is "client-wins" | "server-wins" | "prompt". Neither the compiler nor any gate can flag that they speak of the same
thing: two differently-named types on either side of a package boundary, which is
also a review boundary.
The split as MEASURED on 17/08/2026 — each governs its own path, no hierarchy:
Path
Governed by
Applied in
offline drain (outbox queue)
ConflictPolicy
capabilities/offline/write/push-engine
interactive submission (form)
ConflictStrategy
editor — persistence/submit.ts
🛑 What this split produces, and what was written nowhere: an integrator who sets
modules.editor.persistence.conflictResolution: "prompt" does get a prompt on the
interactive path — and does NOT get one on the offline drain, which decides by
lastWriteWins without consulting it. Measured: push-engine.ts carries no
reference to conflictResolution nor to ConflictStrategy. That is not a defect — it
is even the deliberate motive recalled above (a blocking prompt raised offline, alone
in the field, gets clicked at random). But it happens silently, and that is the
one thing the relationship between the two types had to say.
📌 Neither "wins" over the other — the question "which one wins when the two paths
cross" is not decided here, and it is named rather than invented: deciding it would
mean choosing whether a replayed offline capture should reopen a prompt after the
fact. Not instructed, not urgent, no requester.
⚠️ ConflictPolicy has NO consumer in source: the policy is applied by the BEHAVIOUR
of push-engine.ts (which names it in its log), not by a reference to the type. The
type declares the intent; it does not enforce it. Knowing that prevents believing
that changing its value would change anything.
Conflict policy — frozen as
lastWriteWins, and DECLARED rather than defaulted.This is what the code already does, but by accident: every strategy fell through to a forced overwrite. The change is not the outcome, it is that the conflict becomes detectable and logged, because the push now carries
baseVersion.The rationale is the field: an operator is the authority on what they have just observed, and a blocking dialog raised offline, alone on site, gets clicked at random.
Its twin in
editor, and what their relationship had no home to say⚠️ A SECOND type describes the same arbitration, under another name and in another package:
ConflictStrategy(@geoleaf-plugins/editor,persistence/conflict-resolution.ts), which is"client-wins" | "server-wins" | "prompt". Neither the compiler nor any gate can flag that they speak of the same thing: two differently-named types on either side of a package boundary, which is also a review boundary.The split as MEASURED on 17/08/2026 — each governs its own path, no hierarchy:
ConflictPolicycapabilities/offline/write/push-engineConflictStrategyeditor—persistence/submit.ts🛑 What this split produces, and what was written nowhere: an integrator who sets
modules.editor.persistence.conflictResolution: "prompt"does get a prompt on the interactive path — and does NOT get one on the offline drain, which decides bylastWriteWinswithout consulting it. Measured:push-engine.tscarries no reference toconflictResolutionnor toConflictStrategy. That is not a defect — it is even the deliberate motive recalled above (a blocking prompt raised offline, alone in the field, gets clicked at random). But it happens silently, and that is the one thing the relationship between the two types had to say.📌 Neither "wins" over the other — the question "which one wins when the two paths cross" is not decided here, and it is named rather than invented: deciding it would mean choosing whether a replayed offline capture should reopen a prompt after the fact. Not instructed, not urgent, no requester.
⚠️
ConflictPolicyhas NO consumer in source: the policy is applied by the BEHAVIOUR ofpush-engine.ts(which names it in its log), not by a reference to the type. The type declares the intent; it does not enforce it. Knowing that prevents believing that changing its value would change anything.