Skip to main content

choir_queue/
memory.rs

1//! Resolution memory keyed by the conflict triple (rerere-shaped; DECISIONS.md D15: metadata on existing shapes, never a new
2//! merge substrate).
3//!
4//! A conflict's identity already exists in the model: the
5//! `(base, left, right)` manifest-address triple of a
6//! [`TreeEntry::Conflict`]. The memory key is the hash of that entry's
7//! canonical serialization — the exact bytes invariant 3 freezes — so
8//! keying adds no second identity scheme. Resolutions are found through
9//! item A's `Commit::resolves` links: a head-moving op whose commit
10//! links a conflicted commit contributes one remembered resolution per
11//! path that was conflicted there and is a plain file in the resolver.
12//!
13//! A recalled resolution is a **candidate, never a landing**: the queue
14//! replays it into the train, where it runs the same CI verdict as any
15//! other member. It deliberately skips the strategy safety check —
16//! that check polices *strategies* (a resolution must stay inside what
17//! the change proposed), while a genuine conflict resolution edits
18//! beyond both sides by construction and was already committed as a
19//! value by its author. What the replay skips is only the re-derivation
20//! of a resolution the log already holds.
21
22use std::collections::BTreeMap;
23
24use choir_hash::ContentHash;
25use choir_oplog::OpLog;
26use choir_store::{get_blob, put_blob, ChunkStore, ChunkerParams, MemStore};
27use choir_view::{Commit, OpKind, TreeEntry, ViewOp};
28
29/// Previously seen conflict resolutions, keyed by the conflict triple.
30#[derive(Debug, Default)]
31pub struct ResolutionMemory {
32    /// Triple key (hex of the hashed canonical `TreeEntry::Conflict`)
33    /// → the resolved file content.
34    map: BTreeMap<String, String>,
35}
36
37impl ResolutionMemory {
38    /// An empty memory: every lookup misses, the queue behaves as before.
39    pub fn new() -> Self {
40        Self::default()
41    }
42
43    /// Number of remembered resolutions.
44    pub fn len(&self) -> usize {
45        self.map.len()
46    }
47
48    /// Whether nothing has been remembered.
49    pub fn is_empty(&self) -> bool {
50        self.map.is_empty()
51    }
52
53    /// The conflict's identity: the hash of the canonical serialization
54    /// of the `(base, left, right)` triple, spelled as the
55    /// [`TreeEntry::Conflict`] those addresses came from. Reusing the
56    /// persisted shape's bytes means the key inherits invariant 3's
57    /// canonicalization for free and cannot drift from the store's own
58    /// identity for the same conflict.
59    ///
60    /// Contents are addressed with default chunker params; a resolution
61    /// recorded under different params keys differently and simply
62    /// misses — the memory is an optimization, a miss re-conflicts.
63    pub fn key(base: &str, left: &str, right: &str) -> String {
64        let mut scratch = MemStore::new();
65        let mut addr = |text: &str| {
66            put_blob(&mut scratch, text.as_bytes(), ChunkerParams::default())
67                .expect("MemStore writes cannot fail")
68        };
69        let entry = TreeEntry::Conflict {
70            base: Some(addr(base)),
71            left: addr(left),
72            right: addr(right),
73        };
74        let bytes = serde_json::to_vec(&entry).expect("TreeEntry always serializes");
75        ContentHash::blake3(&bytes).to_hex()
76    }
77
78    /// Builds the memory by walking `log` for head-moving ops whose
79    /// commit carries a `resolves` link, and reading both sides from
80    /// `store`. Anything the store cannot supply is skipped — an
81    /// incomplete memory only means fewer recalls.
82    pub fn from_log(log: &dyn OpLog, store: &dyn ChunkStore) -> Self {
83        let mut map = BTreeMap::new();
84        for seq in 0..log.len() {
85            let Some(entry) = log.get(seq) else { continue };
86            let Ok(op) = ViewOp::from_payload(&entry.payload) else {
87                continue;
88            };
89            let commit_id = match &op.kind {
90                OpKind::SetWorkspaceHead { commit, .. } | OpKind::SetRef { commit, .. } => commit,
91                _ => continue,
92            };
93            let Ok(resolver) = Commit::get(store, commit_id) else {
94                continue;
95            };
96            let Some(conflict_id) = &resolver.resolves else {
97                continue;
98            };
99            let Ok(conflicted) = Commit::get(store, conflict_id) else {
100                continue;
101            };
102            for (path, entry) in &conflicted.tree {
103                let TreeEntry::Conflict { .. } = entry else {
104                    continue;
105                };
106                let Some(TreeEntry::File { blob }) = resolver.tree.get(path) else {
107                    continue;
108                };
109                let Ok(bytes) = get_blob(store, blob) else {
110                    continue;
111                };
112                let Ok(text) = String::from_utf8(bytes) else {
113                    continue;
114                };
115                let key_bytes = serde_json::to_vec(entry).expect("TreeEntry always serializes");
116                map.insert(ContentHash::blake3(&key_bytes).to_hex(), text);
117            }
118        }
119        Self { map }
120    }
121
122    /// The remembered resolution for this triple, if any.
123    pub fn recall(&self, base: &str, left: &str, right: &str) -> Option<&str> {
124        self.map
125            .get(&Self::key(base, left, right))
126            .map(String::as_str)
127    }
128}