Files
beaver_project/app-instance/backend/beaver/plugins/tree_merge.py

66 lines
2.3 KiB
Python

"""Deterministic path-level three-way merge for plugin supporting files."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass(frozen=True, slots=True)
class SupportingFileDecision:
path: str
source: str
def to_dict(self) -> dict[str, Any]:
return {"path": self.path, "source": self.source}
@dataclass(frozen=True, slots=True)
class SupportingFileConflict:
path: str
reason: str
def to_dict(self) -> dict[str, Any]:
return {"path": self.path, "reason": self.reason}
@dataclass(frozen=True, slots=True)
class SupportingFileMergePlan:
files: dict[str, SupportingFileDecision] = field(default_factory=dict)
conflicts: list[SupportingFileConflict] = field(default_factory=list)
def to_dict(self) -> dict[str, Any]:
return {
"files": {path: decision.to_dict() for path, decision in sorted(self.files.items())},
"conflicts": [conflict.to_dict() for conflict in self.conflicts],
}
def merge_supporting_file_trees(
*,
base: dict[str, Any],
local: dict[str, Any],
upstream: dict[str, Any],
) -> SupportingFileMergePlan:
decisions: dict[str, SupportingFileDecision] = {}
conflicts: list[SupportingFileConflict] = []
for path in sorted({*base.keys(), *local.keys(), *upstream.keys()} - {"SKILL.md"}):
b = base.get(path)
l = local.get(path)
u = upstream.get(path)
if l == u and l is not None:
decisions[path] = SupportingFileDecision(path=path, source="local")
elif l == b and u is not None:
decisions[path] = SupportingFileDecision(path=path, source="upstream")
elif u == b and l is not None:
decisions[path] = SupportingFileDecision(path=path, source="local")
elif b is None and l is None and u is not None:
decisions[path] = SupportingFileDecision(path=path, source="upstream")
elif b is None and u is None and l is not None:
decisions[path] = SupportingFileDecision(path=path, source="local")
elif b is not None and l is None and u is None:
continue
else:
conflicts.append(SupportingFileConflict(path=path, reason="divergent supporting-file change"))
return SupportingFileMergePlan(files=decisions, conflicts=conflicts)