No block is ever modified
You cannot edit a block, and neither can anyone else — not yours, not one you pulled from someone else’s brain. This is not a permission the protocol withholds; there is no operation that could express it. Four things stand in the way, and each one is enough on its own:- The object is frozen. Block models are immutable —
block.statement = "something else"raises. block_idis the hash of the envelope. Write the “same” block with a different statement and you getsha256:c261…where you hadsha256:ad3d…. That is not the block modified; it is a different block.- The store cannot file bytes under a foreign identity.
put_bytesreturns the hash of what you handed it. There is no way to say store these bytes as that identity. - Tampering is caught on read. Corrupt a blob out of band and resolving it raises
stored bytes for block_id sha256:ad3d… hash to sha256:c261…: the store is corrupt. A consumer recomputes the hash, so a publisher cannot serve different bytes under a known identity either.
None of them touches the original. That is why reconciling a module is set arithmetic over identifiers, and
why it converges whichever side you call yours.
One property that surprises people coming from git: if two people make the same correction
independently, they produce the same bytes, therefore the same
block_id, therefore nothing to reconcile.
Identical work converges for free rather than conflicting.The whole structural change
On the wire, one parent is written as the scalar
parent and two or more as the list parents. So a
linear history keeps the exact bytes — and the exact digest — it had before parents existed, and a client
that knows nothing of reconciliation stops being able to read a brain only at the point where that brain
genuinely reconciled something. That is the same oldest-that-fits rule block schemas follow.The four steps
ReconciliationHaltedError without writing anything — see
Resolving what did not apply.
The plan is the review
plan_reconcile does not ask which strategy you picked. The composition is identical under all three, so
its job is to inform that choice.
The ancestor cannot be skipped
Without it, a block present in one composition and absent from the other is ambiguous between they added it and I dropped it, and those demand opposite outcomes. Two histories that share no ancestor raiseNoCommonAncestorError rather than being merged on a guess.
You choose: merge, rebase or squash
All three produce the same set of blocks. There is nothing to replay sequentially, because a snapshot is a complete statement of composition rather than a patch. What differs is only the lineage recorded — and therefore who remains on record as the author.ReconcileRequest.strategy is required. There is no default and no policy-level default — you choose per
operation, the way you write git merge or git rebase.
This SDK implements no signing yet, so
their_signatures_survive describes what will hold. It is
reported now because the decision is made now: a report that appeared only once authenticity shipped would
arrive after the only moment it could have informed anything.Rebase preserves granularity only where it can
Exclusion wins, and that is a feature
A module that is absent is not a module that was emptied. A selective install does not hold every module,
and only blocks missing from a composition both sides hold count as removals.
The conflicts that are real
Because no textual conflict is representable, what remains is semantic — and the case that proves reconciliation cannot be purely structural is this one:Branch A drops canonical blockEach module’s set arithmetic is individually correct: the drop is respected in the canonical composition, the addition in the semantic one. The result still violates R1, because a derived block now cites evidence that is not in the composition. The invariant that broke lives between modules, and a set operation never crosses that boundary. So the structural result is validated as if it were an ingestion:C. Branch B adds a semantic block derived fromC.
A conflict in this protocol is a validation failure, not a differencing failure.Every question these conflicts raise — does this evidence exist, does this contradict what is held, is this relation admissible, does the payload satisfy a registered schema — is already asked on the ingestion path. No new checks are introduced.
VALIDATED enter on their own. If anything did not apply cleanly, nothing is
written — the reconciliation stops and waits for you. The same holds if it would take work out of your
brain; see removals.
Every case, and what you do about it
Automatic unless the last column says otherwise. Nothing here is a merge conflict in the git sense — there is no half-merged state on disk and nothing to hand-edit, because the unit is an immutable block and the only questions are whether it enters and, where two histories disagree, which one prevails.Only the rows that touch what you already hold, or that the protocol declines to decide, stop the
operation. A contribution whose every block applies commits in one call.
Resolving what did not apply
reconcile pointer next to head, so it survives the process: a decision taken today
is still there tomorrow, and a separate tool can read what is open. While one is unresolved, ordinary writes
are refused — the decisions were taken against a particular head, and a commit underneath them would leave
them describing a reconciliation that no longer exists.
The plan is recomputed on every call, not remembered. It is a deterministic function of your head, the other
history, the ancestor and the blocks in the store, so recomputing it costs little and cannot report a
judgment that has since stopped holding. What gets persisted is what a person decided.
The three decisions, and the one that is not on offer
Admitting a contradiction is legitimate: a contradiction is information, not a defect, and holding two claims
that disagree is a state the protocol permits without deciding between them.
Admitting a rejection is not, and this is the one place the model departs from version control on purpose. In
git you can force anything into a commit. Here a derived block whose evidence the composition does not hold
cannot be audited against its source,
verify() would not catch it — it recomputes hashes and compositions,
not citations across modules — and so the option does not exist:
Settling precedence
When the reconciliation removes your work
Exclusion has precedence in Equation 1, so a block the other history dropped leaves your composition too. That is deliberate — it is what stops a drop from being undone by whoever still held the block — and it is also a decision about your work, so it does not happen without being seen:reconcile_abort(). Re-admitting a removed block afterwards
remains possible and remains an ordinary commit — doing it inside a reconciliation would make the arithmetic
depend on who was resolving it.
Nothing is destroyed. The bytes stay in the store, older retained roots still name the block and still
verify, and the removal is auditable without a record written here: the history that dropped it wrote one,
and Equation 1 keeps it like any other provenance block.
The acceptance records what it covered. If the plan is recomputed and something else would now leave — you
fetched the rest of a partial history, say — the earlier statement no longer answers the question and
removals_accepted goes back to False.Withdrawn evidence takes what cites it
verify() would not catch it — it recomputes hashes and compositions, not citations across modules.
So the reconciliation runs the same cascade a drop runs, and the dependent follows its evidence out. The
validation gate catches the mirror case, where the derived block is the incoming one; it cannot catch this
one, because nobody proposed the block — it was already here.
The consequence gets its own removal record, attributed to whoever accepted the removals: the other history
recorded withdrawing the evidence, and nothing yet recorded what that cost here.
The record lands in the same version as the exclusion it explains. That matters most for a rebase, which
writes one version per version it replays: the step that withdrew the evidence is the step the dependent
leaves on, and the steps before it keep both. Applying the whole contribution’s cascade to every step would
publish versions excluding a block whose evidence they still hold, with nothing on record saying why — an
unexplained removal, which is the one thing an auditable history cannot contain.
Abandoning
reconcile_abort() discards the decisions. Nothing is undone, because nothing was written: a halted
reconciliation never touched a composition or the head pointer. The blocks it fetched stay in the store,
unreachable from any root, for a prune to reclaim — the ordinary fate of anything no version names.
Missing evidence has two causes, and the diagnosis matters
The block is rejected either way. Why the evidence is absent determines what the contributor should be told, and provenance already holds the answer, because it is the removal ledger.
Same verdict, opposite advice. Collapsing the two discards legitimate work over a packaging mistake.
Rejection is not final: if a replacement source is later registered,
rederive can rebuild what was lost
against it. That is a separate, deliberate step, and it is never folded into a reconciliation — one that
quietly re-derived rejected blocks against substituted evidence would be inventing knowledge in the middle
of an operation you believe to be mechanical.
A contribution needs no new object
Public read, restricted write. A contribution is a snapshot in another repository whose parent belongs to the maintainer’s history — the parent pointer is the entire link, exactly as a branch is in version control. There is no proposal object and no pending state, and this protocol defines no notification mechanism, in the same way version control defines no pull requests.Publishing back from a partial install
A snapshot produced by a partial install names roots only for the modules that install holds, so publishing it over the tag it came from would drop the rest. Reconciliation resolves this rather than working around it:What this does not do
No signing exists in the SDK yet, so two rows of the protocol’s conflict table are out of reach: histories carrying different trust roots must not be reconciled automatically — unioning two key lists grants the union of both sides’ permissions — and apropose-scoped snapshot must not be treated as a brain’s current
state. Both arrive with authenticity.