You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ledger and lims now both hand-write a base-version precondition check, with diverging shapes — extract a shared vocabulary or document the divergence #227
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:
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.
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) andUpdateRule::expectedVersion(
examples/ledger/include/ledger/dto/rule_dto.hpp:26-39): an optionalclient-supplied version compared against the stored row's version, refusing
with a typed
VersionConflicton mismatch and applying unconditionally whendisengaged. Landed via commit
75a485a2, merged to master via PR ledger: rung 5 of the application ladder (Tasks 1-16 of 26) #132 on2026-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) andQueuedCapture::baseVersion/ConflictInfo::{baseVersion,serverVersion}(
examples/lims/include/lims/dto/offline_dto.hpp:125-189): the mechanismthis 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 (ScenarioB), 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:
UpdateRuleSampleModelexpectedVersion(optionalint32,std::nullopt= unconditional)baseVersion(requiredSampleVersion)VersionConflictConflictInfo{baseVersion, serverVersion}outcome, no throwOfflineConflictRecordconflict table +ResolveConflictactionsScope check: still a 2-occurrence pattern, not a 6-occurrence one
A broad sweep of
examples/*/include/*/{models,dto}/**andexamples/*/src/models/**forbaseVersion|expectedVersion|VersionConflict| checkPrecondition|assumedMasterStateand near-spellings found no third rungimplementing this shape. kanban's and bookmarks'
version/opId-shaped hitsare 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.
#226and this issue are related but distinct: #226 is about theop-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:
a generic envelope/metadata slot the framework provides.
VersionConflict) vs.report a conflict outcome for the model to reconcile (lims's
ConflictInfo). Notedocs/spec/offline/offline.md's existingonBackendChanged()conflict-checker/resolver hooks ("Conflict resolutionon 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/SampleModeltoday.baseVersion(cited below) is also per-entity; per-field granularity isnot attested anywhere in this codebase yet.
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 opaquestd::stringpayload andnothing 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'sassumedMasterState, Watermelon'slastPulledAt, CouchDB's_revare the same idea in four shapes — the queuedwrite 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; noheld-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.