///|
/// Parse explicit decisions: {path, choice: base/ours/theirs/custom/delete}.
/// A custom choice requires value; JSON null remains a present value.
pub fn parse_decisions(value : Json) -> Array[Decision] raise ConfigError {
guard value is Array(items) else {
raise InvalidResolution("decisions must be an array")
}
if items.length() > 10000 {
raise LimitExceeded("decisions exceed 10000 entries")
}
items.map(item => {
guard item is Object(o) else {
raise InvalidResolution("decision must be an object")
}
let path = required_string(o, "path")
let _ = pointer_tokens(path)
let choice = match required_string(o, "choice") {
"base" => Base
"ours" => Ours
"theirs" => Theirs
"delete" => Delete
"custom" => {
let value = input_document(o, "value") catch {
_ => raise InvalidResolution("invalid custom JSON at: " + path)
}
validate_document(value)
Set(value)
}
other => raise InvalidResolution("unknown choice: " + other)
}
{ path, choice, }
})
}
///|
/// The leading root test rejects any semantic change to the reviewed base.
/// Number spelling and object order are deliberately not byte-level guards.
pub fn guarded_diff(
base : Json,
target : Json,
) -> Array[Operation] raise ConfigError {
let changes = diff(base, target)
let ops : Array[Operation] = [Test("", clone_json(base))]
// Reserve one operation for the guard, staying within apply's limit.
if changes.length() >= 10000 {
ops.push(Replace("", clone_json(target)))
} else {
for change in changes {
ops.push(change)
}
}
ops
}
///|
/// Recompute conflicts from the three documents, never trust a supplied preview.
/// Missing/duplicate/unknown decisions fail without exposing a partial result.
pub fn resolve(
base : Json,
ours : Json,
theirs : Json,
decisions : Array[Decision],
) -> ResolvedMerge raise ConfigError {
if decisions.length() > 10000 {
raise LimitExceeded("decisions exceed 10000 entries")
}
let preview = merge(base, ours, theirs)
let conflicts : Map[String, Conflict] = Map([])
for conflict in preview.conflicts {
conflicts[conflict.path] = conflict
}
let choices : Map[String, Choice] = Map([])
for decision in decisions {
let _ = pointer_tokens(decision.path)
if !conflicts.contains(decision.path) {
raise InvalidResolution("unknown conflict path: " + decision.path)
}
if choices.contains(decision.path) {
raise InvalidResolution("duplicate decision: " + decision.path)
}
match decision.choice {
Set(value) => validate_document(value)
Delete if decision.path == "" =>
raise InvalidResolution("the document root cannot be deleted")
_ => ()
}
choices[decision.path] = decision.choice
}
// Check completeness before editing anything.
for conflict in preview.conflicts {
if !choices.contains(conflict.path) {
raise InvalidResolution("missing decision: " + conflict.path)
}
}
let edits : Array[Operation] = []
for conflict in preview.conflicts {
let selected = match choices[conflict.path] {
Base => conflict.base
Ours => conflict.ours
Theirs => conflict.theirs
Set(custom) => Some(custom)
Delete => None
}
match selected {
Some(v) => edits.push(Add(conflict.path, v))
None if conflict.base is Some(_) => edits.push(Remove(conflict.path))
None => ()
}
}
let value = apply(preview.value, edits)
validate_document(value)
let patch = guarded_diff(base, value)
if !same(apply(base, patch), value) {
raise InvalidResolution(
"patch replay did not match the final configuration",
)
}
{ value, patch, }
}
///|
pub fn resolved_json(result : ResolvedMerge) -> Json {
Json::object(
Map([
("clean", Json::boolean(true)),
("verified", Json::boolean(true)),
("value", clone_json(result.value)),
("patch", patch_json(result.patch)),
]),
)
}