///|
/// A change in recorded resolution, including complete source evidence.
/// None means no recorded decision, never a stored empty cell value.
pub(all) struct ReviewChange {
target : Target
before : Resolution?
after : Resolution?
} derive(Eq, Debug)
///|
pub extend ReviewChange with Eq::{equal, not_equal}
///|
pub extend ReviewChange with @debug.Debug::{to_repr}
///|
fn resolution_changes(
before : MergePlan,
after : MergePlan,
) -> Array[ReviewChange] raise MergeError {
let old : Map[String, Resolution] = Map([])
let current : Map[String, Resolution] = Map([])
let targets : Map[String, Target] = Map([])
for resolution in before.evidence {
let token = resolution.conflict.target.to_json().stringify()
old[token] = resolution
targets[token] = resolution.conflict.target
}
for resolution in after.evidence {
let token = resolution.conflict.target.to_json().stringify()
current[token] = resolution
targets[token] = resolution.conflict.target
}
let ordered = targets.to_array()
ordered.sort_by((a, b) => lexical_string(a.0, b.0))
let changes : Array[ReviewChange] = []
for (token, target) in ordered {
let prior = old.get(token)
let next = current.get(token)
if prior != next {
if changes.length() >= max_cells {
raise LimitExceeded(
"review comparison exceeds 2000000 changed resolutions",
)
}
changes.push({
target,
before: prior.map(copy_resolution),
after: next.map(copy_resolution),
})
}
}
changes
}
///|
/// Compares validated reviews of exactly the same inputs and policies.
/// Added, changed and revoked decisions are sorted by canonical target.
/// This is a comparison, not an authenticated history or a replayable patch.
pub fn ReviewBundle::changes_since(
self : ReviewBundle,
previous : ReviewBundle,
) -> Array[ReviewChange] raise MergeError {
self.require_same_inputs(previous)
resolution_changes(previous.plan(), self.plan())
}
///|
fn resolution_json(resolution : Resolution?) -> Json {
match resolution {
None => Json::null()
Some(r) =>
{ "conflict": r.conflict.to_json(), "choice": r.choice.to_json() }
}
}
///|
/// Emits full before/after evidence or refuses an oversized report.
pub fn ReviewBundle::changes_report(
self : ReviewBundle,
previous : ReviewBundle,
) -> Json raise MergeError {
self.require_same_inputs(previous)
let before = previous.plan()
let after = self.plan()
let changes = resolution_changes(before, after)
let report : Json = {
"format": "mntablemerge/review-changes-1",
"key": Json::array(self.keys.map(Json::string)),
"row_order": self.ordering.name(),
"column_lineage": lineage_json(self.lineage),
"row_lineage": row_lineage_json_unchecked(self.rows),
"before_complete": before.output != None,
"after_complete": after.output != None,
"changes": Json::array(
changes.map(c => {
"target": c.target.to_json(),
"before": resolution_json(c.before),
"after": resolution_json(c.after),
}),
),
}
if !self.evolution.is_empty() {
guard report is Object(fields) else { abort("object literal") }
fields["format"] = "mntablemerge/review-changes-2"
fields["column_evolution"] = evolution_json(self.evolution)
}
if report.stringify().length() > max_chars {
raise LimitExceeded(
"review change report exceeds 16000000 escaped UTF-16 units; use changes_since for complete typed evidence",
)
}
report
}