Skip to content

Ledger and lims now both hand-write a base-version precondition check, with diverging shapes — extract a shared vocabulary or document the divergence #227

Description

@Yaraslaut

The re-entry trigger has fired

This issue was filed deliberately parked-shaped, with an explicit re-entry
condition: "A second rung independently hand-writes a base-version (or
equivalent precondition) comparison against a queued write."
That condition
is now satisfied, and by an implementation that predates this issue's own
filing:

  • examples/ledger (rung 5), RuleModel::execute(UpdateRule)
    (examples/ledger/src/models/rule_model.cpp:88-96) and
    UpdateRule::expectedVersion
    (examples/ledger/include/ledger/dto/rule_dto.hpp:26-39): an optional
    client-supplied version compared against the stored row's version, refusing
    with a typed VersionConflict on mismatch and applying unconditionally when
    disengaged. Landed via commit 75a485a2, merged to master via PR ledger: rung 5 of the application ladder (Tasks 1-16 of 26) #132 on
    2026-08-21
    — two days before this issue was even filed
    (2026-08-23T21:16). The original "one rung, unmerged" premise was already
    stale at the moment of filing.
  • examples/lims (rung 6), SampleModel::execute()
    (examples/lims/src/models/sample_model.cpp:586-646) and
    QueuedCapture::baseVersion / ConflictInfo::{baseVersion,serverVersion}
    (examples/lims/include/lims/dto/offline_dto.hpp:125-189): the mechanism
    this issue originally documented, at the time via unmerged PR lims: rung 6 — a LIMS, and the forms subsystem at full depth #177. That PR
    has since merged, so the "unmerged" half of the original premise is
    doubly gone.

Both are genuinely independent implementations, not copies of a shared idea:
ledger's version pinning traces to #144 / SYNC-BENCHMARK.md §10 (Scenario
B), with no reference to lims or #203; lims's traces to its own README's
self-posed question. Two different authors-in-time, two different shapes,
same underlying idea — and that divergence is itself the signal that
justifies extracting a real vocabulary now rather than continuing to wait:

ledger UpdateRule lims SampleModel
precondition field expectedVersion (optional int32, std::nullopt = unconditional) baseVersion (required SampleVersion)
granularity whole row (one rule) whole entity (one sample)
on mismatch throws typed VersionConflict returns a ConflictInfo{baseVersion, serverVersion} outcome, no throw
ledger/table none — checked inline against the live row separate OfflineConflictRecord conflict table + ResolveConflict actions

Scope check: still a 2-occurrence pattern, not a 6-occurrence one

A broad sweep of examples/*/include/*/{models,dto}/** and
examples/*/src/models/** for baseVersion|expectedVersion|VersionConflict| checkPrecondition|assumedMasterState and near-spellings found no third rung
implementing this shape. kanban's and bookmarks' version/opId-shaped hits
are the separate exactly-once / op-id-ledger pattern, already tracked as
#226 — not a precondition/base-version check. So this remains a
genuine 2-occurrence pattern (not #226's 6-occurrence situation), but two
independently-arrived-at, already-shipped, already-diverging implementations
is real material for a design decision — the same bar that justified
splitting #226 (replay-time exactly-once) out of #203 after kanban and lims
converged there.

#226 and this issue are related but distinct: #226 is about the
op-id/exactly-once replay ledger (has this write already been applied?);
this issue is about base-version precondition checking (does this write's
assumption about server state still hold?). Both share the shape "extract a
shared vocabulary from divergent rung implementations," but they are
different subsystems and different subject matter — do not conflate them.

What needs deciding

Should ledger and lims converge on one shared precondition vocabulary — and
if so, whose shape wins, or is a synthesis needed — or is the divergence
itself acceptable/intentional given the two domains (financial ledger vs.
lab sample tracking)? Concretely, whoever designs this should resolve:

  • Field shape: a typed action field per DTO (as both rungs do today) vs.
    a generic envelope/metadata slot the framework provides.
  • Mismatch handling: throw a typed error (ledger's VersionConflict) vs.
    report a conflict outcome for the model to reconcile (lims's
    ConflictInfo). Note docs/spec/offline/offline.md's existing
    onBackendChanged() conflict-checker/resolver hooks ("Conflict resolution
    on replay") already cover the model-driven merge/discard half of this —
    what's missing is the field-level precondition-assertion half, which is
    application code in RuleModel/SampleModel today.
  • Granularity: ledger is row-level, lims is whole-entity. ODK's own
    baseVersion (cited below) is also per-entity; per-field granularity is
    not attested anywhere in this codebase yet.
  • Required vs. optional: ledger's field is optional (unconditional apply
    when absent); lims's is required.

Background: comparable offline-first sync engines

morph's conflict checker (the onBackendChanged() replay path,
docs/spec/offline/offline.md) sees an opaque std::string payload and
nothing else — no framework notion of a base version, a precondition, a
revision, or a per-field change set. Every comparable offline-first sync
engine supplies at least a vocabulary for this even when it delegates the
merge policy: ODK's baseVersion, RxDB's assumedMasterState, Watermelon's
lastPulledAt, CouchDB's _rev are the same idea in four shapes — the queued
write states what it assumed, and the server checks it. lims's own
divergences from ODK's actual design (client-side prediction of server
version arithmetic rather than a server-issued branchId/trunkVersion; no
held-submission window for out-of-order arrival; whole-entity rather than
per-property conflict granularity) remain relevant prior art for whoever
designs the shared vocabulary.

Origin

Split out of #203 (gap 2), part of the offline-vs-sync-engines survey. See
#203 for the full comparative document and the ODK/RxDB/Watermelon/CouchDB
precondition-vocabulary examples in fuller detail.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: offlineSubsystem: offlineenhancementNew feature or requesttriage: parkedDeferred with an explicit re-entry condition

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions