1use crate::store::{Store, StoreError};
10use lex_vcs::{OpId, OpLog, StageTransition};
11use serde::{Deserialize, Serialize};
12use std::collections::BTreeMap;
13use std::fs;
14use std::path::PathBuf;
15
16#[derive(Debug)]
22pub enum CasFailed {
23 #[allow(dead_code)] Mismatch { actual: Option<OpId> },
32 UnknownBranch(String),
34 Io(String),
39}
40
41pub const DEFAULT_BRANCH: &str = "main";
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
47struct HeadSnapshot {
48 head_op: OpId,
49 map: BTreeMap<String, String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
53pub struct Branch {
54 pub name: String,
55 pub parent: Option<String>,
56 #[serde(default)]
60 pub head_op: Option<OpId>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub predicate: Option<serde_json::Value>,
67 #[serde(default)]
69 pub merges: Vec<MergeRecord>,
70 pub created_at: u64,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
90 pub last_gate_checkpoint: Option<OpId>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct MergeRecord {
95 pub src: String,
96 pub at: u64,
97 pub merged: usize,
98 pub conflicts: usize,
99}
100
101#[derive(Debug, Clone, Serialize)]
102pub struct MergeReport {
103 pub summary: MergeSummary,
104 pub merged: Vec<MergeEntry>,
105 pub conflicts: Vec<MergeConflict>,
106 #[serde(default)]
113 pub removed: Vec<String>,
114}
115
116#[derive(Debug, Clone, Serialize, Default)]
117pub struct MergeSummary {
118 pub total_sigs: usize,
119 pub clean: usize,
120 pub conflicts: usize,
121 pub base: Option<String>,
122 #[serde(default)]
123 pub src: String,
124 #[serde(default)]
125 pub dst: String,
126}
127
128#[derive(Debug, Clone, Serialize)]
129pub struct MergeEntry {
130 pub sig_id: String,
131 pub stage_id: String,
132 pub from: &'static str, }
134
135#[derive(Debug, Clone, Serialize)]
136pub struct MergeConflict {
137 pub sig_id: String,
138 pub kind: &'static str,
139 pub base: Option<String>,
140 pub src: Option<String>,
141 pub dst: Option<String>,
142}
143
144impl Store {
145 fn branches_dir(&self) -> PathBuf { self.root().join("branches") }
146 fn branch_path(&self, name: &str) -> PathBuf {
147 self.branches_dir().join(format!("{name}.json"))
148 }
149 fn current_branch_path(&self) -> PathBuf {
150 self.root().join("current_branch")
151 }
152
153 pub fn current_branch(&self) -> String {
154 match fs::read_to_string(self.current_branch_path()) {
155 Ok(s) => s.trim().to_string(),
156 Err(_) => DEFAULT_BRANCH.to_string(),
157 }
158 }
159
160 pub fn set_current_branch(&self, name: &str) -> Result<(), StoreError> {
161 if name != DEFAULT_BRANCH && self.get_branch(name)?.is_none() {
162 return Err(StoreError::UnknownBranch(name.into()));
163 }
164 fs::write(self.current_branch_path(), name)?;
165 Ok(())
166 }
167
168 pub fn list_branches(&self) -> Result<Vec<String>, StoreError> {
169 let mut out: Vec<String> = vec![DEFAULT_BRANCH.into()];
170 let dir = self.branches_dir();
171 if !dir.exists() { return Ok(out); }
172 for entry in fs::read_dir(&dir)? {
173 let entry = entry?;
174 let path = entry.path();
175 if path.extension().is_some_and(|e| e == "json") {
176 if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
177 if name != DEFAULT_BRANCH { out.push(name.to_string()); }
178 }
179 }
180 }
181 out.sort();
182 Ok(out)
183 }
184
185 pub fn get_branch(&self, name: &str) -> Result<Option<Branch>, StoreError> {
186 let path = self.branch_path(name);
187 if !path.exists() { return Ok(None); }
188 let raw = fs::read_to_string(&path)?;
189 let b: Branch = serde_json::from_str(&raw)?;
190 Ok(Some(b))
191 }
192
193 fn head_snapshot_path(&self, name: &str) -> PathBuf {
194 self.branches_dir().join(format!("{name}.head_snapshot.json"))
195 }
196
197 fn load_head_snapshot(&self, name: &str) -> Option<HeadSnapshot> {
203 let raw = fs::read_to_string(self.head_snapshot_path(name)).ok()?;
204 serde_json::from_str(&raw).ok()
205 }
206
207 fn save_head_snapshot(&self, name: &str, head_op: &OpId, map: &BTreeMap<String, String>) {
214 let snap = HeadSnapshot { head_op: head_op.clone(), map: map.clone() };
215 if let Ok(s) = serde_json::to_string(&snap) {
216 let _ = fs::write(self.head_snapshot_path(name), s);
217 }
218 }
219
220 pub fn branch_head(&self, name: &str) -> Result<BTreeMap<String, String>, StoreError> {
243 let b = match self.get_branch(name)? {
244 Some(b) => b,
245 None if name == DEFAULT_BRANCH => return Ok(BTreeMap::new()),
246 None => return Err(StoreError::UnknownBranch(name.into())),
247 };
248 let Some(head) = b.head_op else { return Ok(BTreeMap::new()); };
249 let log = OpLog::open(self.root())?;
250
251 if let Some(snap) = self.load_head_snapshot(name) {
252 if snap.head_op == head {
253 return Ok(snap.map);
254 }
255 if let Some(new_records) = log.walk_forward_since(&head, &snap.head_op)? {
256 let mut map = snap.map;
257 for rec in &new_records {
258 apply_transition(&mut map, &rec.produces);
259 }
260 self.save_head_snapshot(name, &head, &map);
261 return Ok(map);
262 }
263 }
266
267 let mut map = BTreeMap::new();
268 for rec in log.walk_forward(&head, None)? {
269 apply_transition(&mut map, &rec.produces);
270 }
271 self.save_head_snapshot(name, &head, &map);
272 Ok(map)
273 }
274
275 pub fn branch_log(&self, name: &str) -> Result<Vec<MergeRecord>, StoreError> {
276 match self.get_branch(name)? {
277 Some(b) => Ok(b.merges),
278 None if name == DEFAULT_BRANCH => Ok(Vec::new()),
279 None => Err(StoreError::UnknownBranch(name.into())),
280 }
281 }
282
283 pub fn create_branch(&self, name: &str, from: &str) -> Result<(), StoreError> {
285 if name.is_empty() || name.contains('/') || name.contains('\\') {
286 return Err(StoreError::InvalidTransition(
287 format!("branch name `{name}` rejected (empty or path-like)")));
288 }
289 if self.branch_path(name).exists() {
290 return Err(StoreError::InvalidTransition(
291 format!("branch `{name}` already exists")));
292 }
293 let head_op = self.get_branch(from)?.and_then(|b| b.head_op);
294 fs::create_dir_all(self.branches_dir())?;
295 let b = Branch {
296 name: name.into(),
297 parent: Some(from.into()),
298 head_op,
299 predicate: None,
300 merges: Vec::new(),
301 created_at: now(),
302 last_gate_checkpoint: None,
303 };
304 fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
305 Ok(())
306 }
307
308 pub fn create_predicate_branch(
314 &self,
315 name: &str,
316 predicate: serde_json::Value,
317 ) -> Result<(), StoreError> {
318 if name.is_empty() || name.contains('/') || name.contains('\\') {
319 return Err(StoreError::InvalidTransition(
320 format!("branch name `{name}` rejected (empty or path-like)")));
321 }
322 if self.branch_path(name).exists() {
323 return Err(StoreError::InvalidTransition(
324 format!("branch `{name}` already exists")));
325 }
326 fs::create_dir_all(self.branches_dir())?;
327 let b = Branch {
328 name: name.into(),
329 parent: None,
330 head_op: None,
331 predicate: Some(predicate),
332 merges: Vec::new(),
333 created_at: now(),
334 last_gate_checkpoint: None,
335 };
336 fs::write(self.branch_path(name), serde_json::to_string_pretty(&b)?)?;
337 Ok(())
338 }
339
340 pub fn delete_branch(&self, name: &str) -> Result<(), StoreError> {
341 if name == DEFAULT_BRANCH {
342 return Err(StoreError::InvalidTransition(
343 "cannot delete the default branch".into()));
344 }
345 if self.current_branch() == name {
346 return Err(StoreError::InvalidTransition(format!(
347 "cannot delete `{name}`; check out another branch first")));
348 }
349 let path = self.branch_path(name);
350 if !path.exists() {
351 return Err(StoreError::UnknownBranch(name.into()));
352 }
353 fs::remove_file(path)?;
354 Ok(())
355 }
356
357 pub(crate) fn set_branch_head_op(
379 &self,
380 name: &str,
381 head_op: OpId,
382 ) -> Result<(), StoreError> {
383 let mut b = match self.get_branch(name)? {
384 Some(b) => b,
385 None if name == DEFAULT_BRANCH => Branch {
386 name: DEFAULT_BRANCH.into(),
387 parent: None,
388 head_op: None,
389 predicate: None,
390 merges: Vec::new(),
391 created_at: now(),
392 last_gate_checkpoint: None,
393 },
394 None => return Err(StoreError::UnknownBranch(name.into())),
395 };
396 b.head_op = Some(head_op.clone());
402 b.last_gate_checkpoint = Some(head_op);
403 fs::create_dir_all(self.branches_dir())?;
404 write_branch_atomic(&self.branch_path(name), &b)?;
405 Ok(())
406 }
407
408 pub(crate) fn set_branch_head_op_cas(
426 &self,
427 name: &str,
428 expected: Option<OpId>,
429 new: OpId,
430 ) -> Result<(), CasFailed> {
431 fs::create_dir_all(self.branches_dir())
436 .map_err(|e| CasFailed::Io(e.to_string()))?;
437 let lock_path = self.branches_dir().join(format!("{name}.lock"));
438 let lock_file = fs::OpenOptions::new()
439 .create(true)
440 .truncate(false)
441 .read(true)
442 .write(true)
443 .open(&lock_path)
444 .map_err(|e| CasFailed::Io(e.to_string()))?;
445 use fs2::FileExt;
446 lock_file.lock_exclusive()
447 .map_err(|e| CasFailed::Io(e.to_string()))?;
448
449 let result = (|| -> Result<(), CasFailed> {
453 let actual = self.get_branch(name)
454 .map_err(|e| CasFailed::Io(format!("{e}")))?
455 .and_then(|b| b.head_op);
456 if actual != expected {
457 return Err(CasFailed::Mismatch { actual });
458 }
459 let mut b = match self.get_branch(name)
460 .map_err(|e| CasFailed::Io(format!("{e}")))?
461 {
462 Some(b) => b,
463 None if name == DEFAULT_BRANCH => Branch {
464 name: DEFAULT_BRANCH.into(),
465 parent: None,
466 head_op: None,
467 predicate: None,
468 merges: Vec::new(),
469 created_at: now(),
470 last_gate_checkpoint: None,
471 },
472 None => return Err(CasFailed::UnknownBranch(name.into())),
473 };
474 b.head_op = Some(new.clone());
475 b.last_gate_checkpoint = Some(new);
476 write_branch_atomic(&self.branch_path(name), &b)
477 .map_err(|e| CasFailed::Io(format!("{e}")))?;
478 Ok(())
479 })();
480 let _ = fs2::FileExt::unlock(&lock_file);
482 result
483 }
484
485 pub fn invalidate_gate_checkpoints(&self) -> Result<usize, StoreError> {
491 let dir = self.branches_dir();
492 if !dir.exists() {
493 return Ok(0);
494 }
495 let mut updated = 0usize;
496 for entry in fs::read_dir(&dir)? {
497 let entry = entry?;
498 let path = entry.path();
499 if path.extension().is_none_or(|e| e != "json") { continue; }
500 let bytes = fs::read(&path)?;
501 let mut b: Branch = match serde_json::from_slice(&bytes) {
502 Ok(b) => b,
503 Err(_) => continue,
507 };
508 if b.last_gate_checkpoint.is_some() {
509 b.last_gate_checkpoint = None;
510 write_branch_atomic(&path, &b)?;
511 updated += 1;
512 }
513 }
514 Ok(updated)
515 }
516}
517
518pub(crate) fn apply_transition(map: &mut BTreeMap<String, String>, t: &StageTransition) {
521 match t {
522 StageTransition::Create { sig_id, stage_id }
523 | StageTransition::Replace { sig_id, to: stage_id, .. } => {
524 map.insert(sig_id.clone(), stage_id.clone());
525 }
526 StageTransition::Remove { sig_id, .. } => {
527 map.remove(sig_id);
528 }
529 StageTransition::Rename { from, to, body_stage_id } => {
530 map.remove(from);
531 map.insert(to.clone(), body_stage_id.clone());
532 }
533 StageTransition::ImportOnly => {}
534 StageTransition::Merge { entries } => {
535 for (sig, stage) in entries {
536 match stage {
537 Some(s) => { map.insert(sig.clone(), s.clone()); }
538 None => { map.remove(sig); }
539 }
540 }
541 }
542 }
543}
544
545fn write_branch_atomic(path: &std::path::Path, b: &Branch) -> Result<(), StoreError> {
546 use std::io::Write;
547 let bytes = serde_json::to_vec_pretty(b)?;
548 let tmp = path.with_extension("json.tmp");
549 let mut f = fs::File::create(&tmp)?;
550 f.write_all(&bytes)?;
551 f.sync_all()?;
552 fs::rename(&tmp, path)?;
553 Ok(())
554}
555
556fn now() -> u64 {
557 use std::time::{SystemTime, UNIX_EPOCH};
558 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
559}
560
561impl Store {
562 pub fn merge(&self, src: &str, dst: &str) -> Result<MergeReport, StoreError> {
563 let log = OpLog::open(self.root())?;
564 let src_head = self.get_branch(src)?.and_then(|b| b.head_op);
565 let dst_head = match self.get_branch(dst)? {
566 Some(b) => b.head_op,
567 None if dst == DEFAULT_BRANCH => None,
568 None => return Err(StoreError::UnknownBranch(dst.into())),
569 };
570 let out = lex_vcs::merge(&log, src_head.as_ref(), dst_head.as_ref())?;
571
572 let mut report = MergeReport {
573 summary: MergeSummary {
574 base: out.lca.clone(),
575 src: src.into(),
576 dst: dst.into(),
577 ..Default::default()
578 },
579 merged: Vec::new(),
580 conflicts: Vec::new(),
581 removed: Vec::new(),
582 };
583 for o in out.outcomes {
584 match o {
585 lex_vcs::MergeOutcome::Both { sig_id, stage_id } => {
586 if let Some(stage_id) = stage_id {
587 report.merged.push(MergeEntry { sig_id, stage_id, from: "both" });
588 }
589 }
590 lex_vcs::MergeOutcome::Src { sig_id, stage_id } => match stage_id {
591 Some(stage_id) => report.merged.push(MergeEntry { sig_id, stage_id, from: "src" }),
592 None => report.removed.push(sig_id),
593 },
594 lex_vcs::MergeOutcome::Dst { sig_id, stage_id } => match stage_id {
595 Some(stage_id) => report.merged.push(MergeEntry { sig_id, stage_id, from: "dst" }),
596 None => report.removed.push(sig_id),
597 },
598 lex_vcs::MergeOutcome::Conflict { sig_id, kind, base: base_stage, src: src_stage, dst: dst_stage } => {
599 if let (lex_vcs::ConflictKind::ModifyModify, Some(b), Some(s), Some(d)) =
607 (&kind, &base_stage, &src_stage, &dst_stage)
608 {
609 if let Some(merged_id) =
610 self.try_semantic_body_merge(dst, &sig_id, b, d, s)?
611 {
612 report.merged.push(MergeEntry {
613 sig_id,
614 stage_id: merged_id,
615 from: "semantic",
616 });
617 continue;
618 }
619 }
620 let kind: &'static str = match kind {
621 lex_vcs::ConflictKind::ModifyModify => "modify-modify",
622 lex_vcs::ConflictKind::ModifyDelete => "modify-delete",
623 lex_vcs::ConflictKind::DeleteModify => "delete-modify",
624 lex_vcs::ConflictKind::AddAdd => "add-add",
625 };
626 report.conflicts.push(MergeConflict {
627 sig_id, kind, base: base_stage, src: src_stage, dst: dst_stage,
628 });
629 }
630 }
631 }
632 report.summary.clean = report.merged.len();
633 report.summary.conflicts = report.conflicts.len();
634 report.summary.total_sigs = report.merged.len() + report.conflicts.len();
635 Ok(report)
636 }
637
638 pub fn commit_merge(&self, dst: &str, report: &MergeReport) -> Result<(), StoreError> {
639 if !report.conflicts.is_empty() {
640 return Err(StoreError::InvalidTransition(format!(
641 "{} conflicts; resolve before committing", report.conflicts.len())));
642 }
643 let dst_head_map = self.branch_head(dst)?;
644 let mut entries: BTreeMap<String, Option<String>> = BTreeMap::new();
645 for m in &report.merged {
646 let cur = dst_head_map.get(&m.sig_id);
647 if cur != Some(&m.stage_id) {
648 entries.insert(m.sig_id.clone(), Some(m.stage_id.clone()));
649 }
650 }
651 for sig in &report.removed {
655 if dst_head_map.contains_key(sig) {
656 entries.insert(sig.clone(), None);
657 }
658 }
659 let src_head = self.get_branch(&report.summary.src)?.and_then(|b| b.head_op);
660 let dst_head_op = self.get_branch(dst)?.and_then(|b| b.head_op);
661
662 match (src_head.clone(), dst_head_op.clone()) {
663 (Some(s), None) => {
665 self.set_branch_head_op(dst, s)?;
666 }
667 (Some(s), Some(d)) if s == d => { }
670 (Some(s), Some(d)) => {
671 let op = lex_vcs::Operation::new(
677 lex_vcs::OperationKind::Merge { resolved: entries.len() },
678 [d, s],
679 );
680 let t = lex_vcs::StageTransition::Merge { entries };
681 let _ = self.apply_merge_op_gated(dst, op, t)?;
684 }
685 (None, _) => { }
687 }
688
689 let mut b = self.get_branch(dst)?
702 .ok_or_else(|| StoreError::UnknownBranch(dst.into()))?;
703 if !report.summary.src.is_empty() {
704 b.merges.push(MergeRecord {
705 src: report.summary.src.clone(),
706 at: now(),
707 merged: report.merged.len(),
708 conflicts: 0,
709 });
710 write_branch_atomic(&self.branch_path(dst), &b)?;
711 }
712 Ok(())
713 }
714}
715
716#[cfg(test)]
717mod branch_head_snapshot_tests {
718 use super::*;
719 use lex_vcs::{Operation, OperationKind};
720 use std::collections::BTreeSet;
721
722 fn add(store: &Store, sig: &str, stg: &str) -> OpId {
723 let parent = store.get_branch(DEFAULT_BRANCH).unwrap().and_then(|b| b.head_op);
724 let op = Operation::new(
725 OperationKind::AddFunction {
726 sig_id: sig.into(),
727 stage_id: stg.into(),
728 effects: BTreeSet::new(),
729 budget_cost: None,
730 },
731 parent.into_iter().collect::<Vec<_>>(),
732 );
733 let transition = StageTransition::Create { sig_id: sig.into(), stage_id: stg.into() };
734 store.apply_operation(DEFAULT_BRANCH, op, transition).unwrap()
735 }
736
737 #[test]
747 fn branch_head_falls_back_to_full_walk_when_snapshot_predates_a_reset() {
748 let tmp = tempfile::tempdir().unwrap();
749 let store = Store::open(tmp.path()).unwrap();
750
751 add(&store, "fn::a", "stage_a");
752 add(&store, "fn::b", "stage_b");
753 let snapshotted = store.branch_head(DEFAULT_BRANCH).unwrap();
754 assert_eq!(snapshotted.len(), 2, "sanity: snapshot covers both ops");
755
756 let reset_op = Operation::new(
759 OperationKind::AddFunction {
760 sig_id: "fn::reset_only".into(),
761 stage_id: "stage_reset".into(),
762 effects: BTreeSet::new(),
763 budget_cost: None,
764 },
765 Vec::new(), );
767 let reset_op_id = reset_op.op_id();
768 let reset_record = lex_vcs::OperationRecord::new(
769 reset_op,
770 StageTransition::Create {
771 sig_id: "fn::reset_only".into(),
772 stage_id: "stage_reset".into(),
773 },
774 );
775 let log = OpLog::open(store.root()).unwrap();
776 log.put(&reset_record).unwrap();
777 store.set_branch_head_op(DEFAULT_BRANCH, reset_op_id).unwrap();
778
779 let after_reset = store.branch_head(DEFAULT_BRANCH).unwrap();
780 assert_eq!(
781 after_reset.len(), 1,
782 "stale snapshot must not be reused across a non-ancestor head change: {after_reset:?}"
783 );
784 assert_eq!(after_reset.get("fn::reset_only"), Some(&"stage_reset".to_string()));
785 assert!(!after_reset.contains_key("fn::a"));
786 assert!(!after_reset.contains_key("fn::b"));
787
788 let again = store.branch_head(DEFAULT_BRANCH).unwrap();
791 assert_eq!(after_reset, again);
792 }
793}