package storage import ( "strconv" "strings" "github.com/metazla/meta-core/internal/claim" ) // Vote is one signer's claim on one key, as the merge sees it. Pid identifies // the signer (one vote per signer per key per group); Uid is carried for the // stateless /api/merge, where claims arrive without a local pid. type Vote struct { Pid int64 Uid string Value claim.Value TS uint64 } // MergeResult is the merged value of one key. type MergeResult struct { Value string Nsrc int // signers agreeing on the winning value OK bool } // Merge is the one merge rule (spec §7), shared by the apply loop, the // ?as= overlay and POST /api/merge: // // 1. a signer counts once per key: its newest vote wins (it may have voted on // several cids of the group); // 2. the viewer's own live text vote wins outright (viewer == "" for the // shared view); // 3. retractions and ciphertext drop out — only text merges; // 4. support: votes grouped by normalised value, most signers wins; // 5. tie → the value first seen (oldest ts) — stable, and rewriting does not // win; // 6. tie → byte-wise smallest value, so every peer agrees. // // The stored value is the exact text of the oldest vote of the winning group. func Merge(votes []Vote, viewer string) MergeResult { newest := make(map[string]Vote, len(votes)) for _, v := range votes { id := signerID(v) if cur, ok := newest[id]; !ok || v.TS > cur.TS { newest[id] = v } } type bucket struct { n int minTS uint64 exemplar string } buckets := make(map[string]*bucket) for _, v := range newest { if v.Value.Kind != claim.KindText { continue } if viewer != "" && v.Uid == viewer { return MergeResult{Value: v.Value.Text, Nsrc: 1, OK: true} } norm := normalizeValue(v.Value.Text) b := buckets[norm] if b == nil { buckets[norm] = &bucket{n: 1, minTS: v.TS, exemplar: v.Value.Text} continue } b.n++ if v.TS < b.minTS || (v.TS == b.minTS && v.Value.Text < b.exemplar) { b.minTS, b.exemplar = v.TS, v.Value.Text } } var best *bucket for _, b := range buckets { switch { case best == nil, b.n > best.n, b.n == best.n && b.minTS < best.minTS, b.n == best.n && b.minTS == best.minTS && b.exemplar < best.exemplar: best = b } } if best == nil { return MergeResult{} } return MergeResult{Value: best.exemplar, Nsrc: best.n, OK: true} } func signerID(v Vote) string { if v.Uid != "" { return v.Uid } return "#" + strconv.FormatInt(v.Pid, 10) } // normalizeValue folds case and surrounding whitespace so "The Matrix" and // "the matrix " count as the same value. (Unicode NFC folding is not applied: // the module carries no normalisation table; values written by one plugin // family are already consistent.) func normalizeValue(s string) string { return strings.ToLower(strings.TrimSpace(s)) }