Let the application supply the auto-merge policy; add Repo::merge_heads - #45
Merged
Merged
Conversation
Auto-merge resolves concurrent heads with the policy named in the genesis metadata, and the only one that exists is last-writer-wins over the whole payload: the newest head's payload is copied into the merge node. That is right when the payload is one value. It is wrong when the payload is several values with different convergence rules — a content body next to an access policy, say. A head that only advanced the policy still carries the body it inherited; a concurrent head that only changed the body still carries the policy it inherited. Whichever is newer wins both fields, and the other head's change is lost even though nothing competed with it. The library cannot merge such a payload field by field: it does not know the fields. So it now lets the application say how. - `Repo::with_merge_policy(Box<dyn MergePolicy<P>>)` installs a policy that is used for every auto-merge in place of the named one. Nothing changes for callers that do not install one. - `ResolveInput` gains `parent_payloads`: the payloads of the head's parents. A field-aware policy needs to know what a head changed, not just what it holds, and only the parent can tell it. `LwwMergePolicy` ignores the field; a parent missing from storage (ancestry not yet synced) is skipped rather than an error. The policy is process-local and never travels with the data, so every replica must install the same one — replicas merging the same heads under different rules produce different merge nodes and keep re-merging. That is documented on the method. Tests: the installed policy is used over the genesis's "lww" and sees each head's parents (repo); the resolver attaches parent payloads (resolver); the named policy still applies when nothing is installed (repo). Co-Authored-By: Claude Code <noreply@anthropic.com>
Auto-merge runs lazily, inside the next commit. A caller that wants to *read* the converged state before committing — to copy the current payload into a new version, or to evaluate a policy it carries — gets nothing from `latest()` but one of the concurrent heads picked by timestamp. Any value it copies from there is one branch's, and the merge that follows inside its commit resolves the *old* heads, not the value it just wrote on top of one of them. With a field-wise merge policy that is exactly the case that matters: the reader sees the branch that lacks the other branch's field. `merge_heads(genesis)` runs the same merge the lazy path would, commits it in its own batch, and returns the single head (or the existing one when there was nothing to merge). `heads(genesis)` exposes the current head set for callers that want to know whether a merge is pending. Both are idempotent. Co-Authored-By: Claude Code <noreply@anthropic.com>
somasekimoto
marked this pull request as ready for review
September 19, 2026 04:11
Member
|
元々がそうなっていたってのがあるけど、現状はnodeを作成するタイミングにマージポリシーを適用させる状態になっていると思うのですが、こちらについてどう思いますか? |
Contributor
Author
|
指摘のとおり、今回の実装では注入したpolicyがgenesisのpolicy_typeを無視するため、同じ履歴に別の規則を黙って適用できる状態になっていました。 ノード作成時にマージ結果を保存すること自体よりも、「その履歴で使うと記録された規則」と「実際に使う実装」が結び付いていない点が問題だと考え、2528d75で修正しました。 責務は次のように分けています。
新規作成時には選択したpolicyをgenesisへ記録し、以後のマージはその指定に従います。既存LWW履歴は、独自policyを注入してもLWWのまま扱います。また、同期先でも同じmetadata・CIDを復元できるよう、operationにその情報を保持させました。 ただし、ライブラリが検証するのは規則の名前と実装の対応であり、同じ名前で提供される実装の中身まで同一かは検証しません。そのため、挙動を変える場合は別のpolicy名にするなど、実装の互換性はアプリ側で管理する、という分担です。 |
Yu-da-1
approved these changes
Sep 22, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Auto-merge resolves concurrent heads with the policy named in the genesis metadata, and the only one is last-writer-wins over the whole payload. That is right when the payload is one value. It is wrong when it is several with different convergence rules — Monas's payload is a content body next to an access policy. A head that only advanced the policy still carries the body it inherited; a concurrent head that only changed the body still carries the policy it inherited. Whichever is newer wins both fields, and the other head's change is lost although nothing competed with it. In Monas this is a revoke being erased by a concurrent write (Monas-project/monas,
docs/revoke-write-bypass-investigation.mdonfeat/policy-aware-merge).The library cannot merge such a payload field by field — it does not know the fields — so it now lets the application say how.
What
Repo::with_merge_policy(Box<dyn MergePolicy<P>>): installs a policy used for every auto-merge in place of the genesis-named one. Nothing changes for callers that do not install one.ResolveInput::parent_payloads: the head's parents' payloads, so a field-aware policy can tell what a head changed.LwwMergePolicyignores it; a parent missing from storage is skipped.Repo::merge_heads(genesis): run the auto-merge now and return the single head. The lazy merge inside the next commit is too late for a caller that wants to read the converged state first —latest()alone picks one concurrent head by timestamp.Repo::heads(genesis)exposes the current head set.The policy is process-local and never travels with the data; every replica must install the same one (documented on the method).
Tests
merge_headsconverges two heads, is idempotent, returnsNonefor unknown content110 tests pass, clippy clean.