Base revisions and semantic conflicts¶
Several processes can safely write one local production, but SQLite write serialization alone cannot tell whether a host made a decision from stale knowledge. When a write depends on previously read state, begin its transaction with that state’s revision ID.
A base revision is optimistic context, not a lock. It does not reserve the production or reject unrelated work. Unbased transactions permit independent metadata appends; metadata replacement/removal and root enabling/removal require a base. Other legacy unbased mutation families remain under development review.
Use a base for read–decide–write flows¶
Start with Coherent reads and scoped edits: retain one read view, copy the facts needed for the decision, then edit from its scoped base. This also protects decisions made from the initial empty journal. The older revision-ID recipe below remains available during migration:
The legacy host flow is:
retain the latest revision before reading the relevant objects, then read the latest revision again and discard the reads if it changed;
let the application or user decide what to change;
begin a transaction at that revision;
stage and commit the change;
on conflict, inspect the structured detail and re-read current state; and
retry in a new transaction only after making the decision again.
A base is not a read snapshot. Fetching it only after reading can label an old decision with another writer’s newer revision. The two revision reads provide a conservative fence; they do not lock the UI decision interval. Keep the first base through that interval and use it when committing. In an empty production there is no base: an independent first import can use an ordinary transaction. A decision requiring existing committed objects must first have those objects and a retained revision.
Commit receipts identify the production and the revision created by that
transaction. An absent revision explicitly means no new journal entry. C uses
pp_transaction_commit with a required receipt output; C++, Rust and Python
use commit(). A later head read can belong to another writer; use the atomic
receipt for attribution.
sequenceDiagram
participant A as Host A
participant P as Local production
participant B as Host B
A->>P: Read fact K at revision R
B->>P: Read fact K at revision R
A->>P: Commit K with base R
P-->>A: Success at revision S
B->>P: Commit K with base R
P-->>B: Conflict: key K, base R, superseding S
B->>P: Re-read K and revision S
B->>P: Explicit retry with base S
P-->>B: Success or a new conflict
The example deliberately makes one writer stale, inspects the semantic key and both revisions, then performs an explicit retry:
static pp_error_code_t decision_base(pp_production_t *production,
pp_decision_base_t *out_base,
pp_error_t **error) {
pp_read_session_t *view = NULL;
pp_error_code_t status = pp_production_read_session(production, &view, error);
if (status == PP_OK)
status = pp_read_session_decision_base(view, out_base, error);
pp_read_session_release(view);
return status;
}
static pp_error_code_t
update_from_a_base_revision(pp_production_t *production, pp_error_t **error) {
pp_media_root_id_t root_id = {0};
pp_decision_base_t base = {0};
pp_revision_id_t superseding_id;
uint64_t superseding_sequence = 0;
pp_transaction_t *transaction = NULL;
/* Saved UUID text is parsed into its semantic type at interchange. */
pp_media_root_id_t parsed_root;
char *root_text = NULL;
pp_error_code_t parsed = pp_media_root_id_parse(
"00000000-0000-0000-0000-000000000001", &parsed_root, error);
if (parsed == PP_OK)
parsed = pp_media_root_id_format(parsed_root, &root_text, error);
if (parsed == PP_OK && strcmp(root_text,
"00000000-0000-0000-0000-000000000001")) parsed = PP_ERROR_INTERNAL;
pp_string_release(root_text);
if (parsed != PP_OK) return parsed;
pp_error_code_t status =
pp_production_begin_edit(production, NULL, NULL, &transaction, error);
if (status == PP_OK) {
status = pp_transaction_add_media_root(transaction, "rushes", NULL, 0,
&root_id, error);
}
if (status == PP_OK) {
pp_commit_receipt_t commit_receipt;
status = pp_transaction_commit(transaction, &commit_receipt, error);
}
pp_transaction_release(transaction);
transaction = NULL;
if (status == PP_OK) {
status = decision_base(production, &base, error);
}
if (status == PP_OK) {
status = pp_production_begin_edit(production, &base, NULL,
&transaction, error);
}
if (status == PP_OK) {
status = pp_transaction_set_media_root_enabled(transaction, root_id, 0,
error);
}
if (status == PP_OK) {
pp_commit_receipt_t receipt;
status = pp_transaction_commit(transaction, &receipt, error);
if (status == PP_OK && receipt.outcome != PP_COMMIT_REVISION_CREATED)
status = PP_ERROR_INTERNAL;
if (status == PP_OK) {
superseding_id = receipt.revision_id;
superseding_sequence = receipt.revision_sequence;
}
}
pp_transaction_release(transaction);
transaction = NULL;
if (status == PP_OK) {
status = pp_production_begin_edit(production, &base, NULL,
&transaction, error);
}
if (status == PP_OK) {
status = pp_transaction_set_media_root_enabled(transaction, root_id, 1,
error);
}
if (status == PP_OK) {
pp_error_t *conflict_error = NULL;
pp_transaction_conflict_t conflict;
/* Status-only commit deliberately discards any receipt; this call must fail. */
status = pp_transaction_commit(transaction, &(pp_commit_receipt_t){0}, &conflict_error);
if (status == PP_ERROR_CONFLICT &&
pp_error_transaction_conflict(conflict_error, &conflict) != 0 &&
conflict.kind == PP_CONFLICT_MEDIA_ROOT &&
conflict.target.kind == 0 &&
memcmp(conflict.media_root_id.bytes, root_id.bytes, sizeof(root_id.bytes)) ==
0 &&
memcmp(conflict.base_revision_id.bytes, base.revision_id.bytes,
sizeof(base.revision_id.bytes)) == 0 &&
conflict.base_revision_sequence == base.revision_sequence &&
memcmp(conflict.superseding_revision_id.bytes, superseding_id.bytes,
sizeof(superseding_id.bytes)) == 0 &&
conflict.superseding_revision_sequence == superseding_sequence) {
status = PP_OK;
}
pp_error_release(conflict_error);
}
pp_transaction_release(transaction);
transaction = NULL;
/* Retry only after re-reading and deciding that enabling is still right. */
pp_decision_base_t refreshed = {0};
if (status == PP_OK) {
status = decision_base(production, &refreshed, error);
}
if (status == PP_OK) {
status = pp_production_begin_edit(production, &refreshed, NULL,
&transaction, error);
}
if (status == PP_OK) {
status = pp_transaction_set_media_root_enabled(transaction, root_id, 1,
error);
}
if (status == PP_OK) {
pp_commit_receipt_t commit_receipt;
status = pp_transaction_commit(transaction, &commit_receipt, error);
}
pp_transaction_release(transaction);
return status;
}
void update_from_a_base_revision(postproject::Production &production) {
auto setup = production.edit().value();
const auto root_id = setup.addMediaRoot("rushes").value();
setup.commit().value();
require(postproject::MediaRootId::fromString(root_id.toString().value()).value()
== root_id, "saved root identity");
const auto base = production.readSession().value().decisionBase().value();
auto first_writer = production.edit(base).value();
first_writer.setMediaRootEnabled(root_id, false).value();
const auto receipt = first_writer.commit().value();
require(receipt.production_id == production.id().value(), "receipt production");
require(receipt.revision.has_value(), "created revision");
const auto superseding = *receipt.revision;
auto stale_writer = production.edit(base).value();
stale_writer.setMediaRootEnabled(root_id, true).value();
const auto result = stale_writer.commit();
require(!result, "stale write must conflict");
const auto *conflict = result.error().transactionConflict();
require(conflict != nullptr, "structured conflict detail");
require(conflict->key.kind == postproject::ConflictKeyKind::media_root,
"media-root conflict key");
const auto *conflicting_root =
std::get_if<postproject::MediaRootId>(&conflict->key.target);
require(conflicting_root && *conflicting_root == root_id, "conflicting root");
require(conflict->base_revision_id == base.revision->id, "supplied base revision");
require(conflict->superseding_revision_id == superseding.id,
"superseding revision");
// Retry only after re-reading and deciding that enabling is still right.
const auto refreshed = production.readSession().value().decisionBase().value();
auto retry = production.edit(refreshed).value();
retry.setMediaRootEnabled(root_id, true).value();
retry.commit().value();
}
def update_from_a_base_revision(production: Production) -> None:
with production.edit() as setup:
root_id = setup.add_media_root("rushes")
setup.commit()
with production.read_session() as view:
base = view.decision_base
assert base.revision is not None
with production.edit(base) as first_writer:
first_writer.set_media_root_enabled(root_id, False)
receipt = first_writer.commit()
assert receipt.production_id == production.id
superseding = receipt.revision
assert superseding is not None
stale_writer = production.edit(base)
try:
stale_writer.set_media_root_enabled(root_id, True)
stale_writer.commit()
except ConflictError as error:
conflict = error.conflict
assert conflict is not None
assert conflict.key.kind is ConflictKeyKind.MEDIA_ROOT
assert conflict.key.target == root_id
assert conflict.base_revision_id == base.revision.id
assert conflict.superseding_revision_id == superseding.id
else:
raise AssertionError("stale write did not conflict")
finally:
stale_writer.close()
# Retry only after re-reading and deciding that enabling is still right.
with production.read_session() as refreshed, refreshed.edit() as retry:
retry.set_media_root_enabled(root_id, True)
retry.commit()
fn update_from_a_base_revision(
production: &mut SqliteProduction,
) -> Result<postproject_core::CommitReceipt> {
let root_id = MediaRootId::new();
let root = MediaRoot::new(root_id, "rushes", None, None, 0, true)?;
{
let mut setup = production.begin_transaction()?;
setup.add_media_root(root)?;
setup.commit()?;
}
let base = production.latest_revision()?.expect("setup revision");
let receipt = {
let mut first_writer = production.begin_transaction_at(base.id())?;
first_writer.set_media_root_enabled(root_id, false)?;
first_writer.commit()?
};
assert_eq!(receipt.production_id(), production.production().id());
let superseding = receipt.revision().expect("first update");
{
let mut stale_writer = production.begin_transaction_at(base.id())?;
stale_writer.set_media_root_enabled(root_id, true)?;
let error = stale_writer.commit().expect_err("stale write conflicts");
let conflict = error
.transaction_conflict_detail()
.expect("structured conflict detail");
assert_eq!(conflict.key().kind(), ConflictKeyKind::MediaRoot);
assert_eq!(conflict.key(), &SemanticConflictKey::MediaRoot(root_id));
assert_eq!(conflict.base_revision(), Some(base.id()));
assert_eq!(conflict.superseding_revision(), superseding.id());
}
// Retry only after re-reading and deciding that enabling is still right.
let refreshed = production.latest_revision()?.expect("refreshed revision");
{
let mut retry = production.begin_transaction_at(refreshed.id())?;
retry.set_media_root_enabled(root_id, true)?;
retry.commit()
}
}
postproject init conflicts.pproj
CREATED=$(postproject --json root add conflicts.pproj rushes)
ROOT_ID=$(jq -r .id <<<"$CREATED")
BASE_ID=$(jq -r .commit_receipt.revision.id <<<"$CREATED")
BASE=$(postproject --json inspect conflicts.pproj | jq -r .decision_base)
postproject --decision-base "$BASE" root disable conflicts.pproj "$ROOT_ID"
if postproject --json --decision-base "$BASE" \
root enable conflicts.pproj "$ROOT_ID" >conflict.json; then
echo "stale write unexpectedly succeeded" >&2
exit 1
fi
jq -e --arg root "$ROOT_ID" --arg base "$BASE_ID" \
'.error.transaction_conflict
| .key.kind == "media_root"
and .key.target_id == $root
and .base_revision_id == $base' conflict.json >/dev/null
# Retry only after re-reading and deciding that enabling is still right.
REFRESHED=$(postproject --json inspect conflicts.pproj | jq -r .decision_base)
postproject --decision-base "$REFRESHED" \
root enable conflicts.pproj "$ROOT_ID"
For the CLI, use the production-scoped --decision-base TOKEN from inspect.
With --json, an
optimistic conflict is written to standard output with a failing exit status as
JSON containing key, base_revision_id, base_revision_sequence,
superseding_revision_id, and superseding_revision_sequence.
Conflicts follow semantic facts¶
PostProject does not compare only the production’s latest revision. It checks the non-mergeable facts touched by the transaction:
the locator set of one resource;
one metadata property on one object;
the dependency set of one representation;
one media root;
one exact external-identifier attachment; and
one resource or representation fingerprint algorithm/version domain.
Creating objects with new identities and appending independent facts can still commit from an older base. For example, two hosts may add distinct assets or media roots from the same base. Replacing the same locator set or changing the same media root from that base produces exactly one successful update and one conflict.
The complete per-operation classification is recorded in ADR 0042. Job transitions retain their more specific state, lease, and claim-token guards rather than becoming generic optimistic conflicts.
Metadata appends advance the property version while remaining mergeable. Replacement/removal guard that version, so an intervening append invalidates a destructive decision (ADR 0052). Rejection rolls back the whole edit.
Treat the result as a new decision point¶
Conflict messages are diagnostic and may change. Branch on the stable error category and structured conflict fields instead. The key says which fact changed, while the superseding revision can be loaded or presented alongside its revision origin.
A failed conflict commit changes nothing and closes that transaction. Do not retry it automatically: the new value may represent another application’s or person’s intentional decision. Re-read through the production handle, update the host UI or policy input, and create a new transaction with a refreshed base if the change is still wanted.
Set an OriginIdentity and useful message on every host-authored transaction.
Origin identifies the integrating application and optional version/URI; it is
not an authenticated user identity. It gives revision and conflict UI useful
context about which tool made the superseding change.