///|
priv struct SourceDecision {
path : String
metadata : Json?
reason : String
}
///|
priv struct ScanDecision {
path : String
retained : Bool
reason : String
}
///|
priv struct EffectiveDecision {
path : String
key : String
value : Json?
reason : String
}
///|
/// Explicit decisions bound to three complete immutable snapshots. Complete
/// source replacements (null removes a source) cannot affect unnamed sources.
/// Effective assertions compare exact JSON tokens, including absent vs null.
pub struct MergeResolution {
priv base : DatasetIndex
priv left : DatasetIndex
priv right : DatasetIndex
priv sources : Array[SourceDecision]
priv scans : Array[ScanDecision]
priv effective : Array[EffectiveDecision]
}
///|
fn resolution_object(
value : Json,
keys : Array[String],
location : String,
) -> Map[String, Json] raise TraceError {
guard value is Object(fields) && sorted_keys(fields) == keys else {
reject(
"invalid_merge_resolution",
location,
"Required controls: " +
keys.join(", ") +
"; unknown or missing controls are rejected.",
)
abort("reject raises")
}
fields
}
///|
fn decision_reason(
fields : Map[String, Json],
path : String,
) -> String raise TraceError {
guard fields.get("reason") is Some(String(reason)) &&
!reason.trim().is_empty() &&
reason.length() <= 4096 else {
reject(
"invalid_resolution_reason", path, "Each decision needs a nonblank reason of at most 4,096 UTF-16 units.",
)
abort("reject raises")
}
reason
}
///|
fn decision_path(
fields : Map[String, Json],
source~ : Bool,
) -> String raise TraceError {
guard fields.get("path") is Some(String(path)) else {
reject(
"invalid_merge_resolution", "", "Decision path must be a supported relative path string.",
)
abort("reject raises")
}
ignore(selector(path, sidecar=source))
path
}
///|
fn resolution_coordinate(path : String, key : String) -> String {
canonical_json(Json::array([Json::string(path), Json::string(key)]))
}
///|
/// Strict v1 control grammar. Up to 256 source/scan decisions combined and
/// 1,024 effective field assertions. Snapshot limits remain independently enforced.
pub fn MergeResolution::from_json(
text : String,
) -> MergeResolution raise TraceError {
let fields = resolution_object(
parse_source(
"",
text,
container_limit=80,
text_limit=16777216,
),
["base", "effective", "left", "right", "scans", "sources"],
"",
)
guard fields["sources"] is Array(sources) &&
fields["scans"] is Array(scans) &&
fields["effective"] is Array(effective) else {
reject(
"invalid_merge_resolution", "", "sources, scans and effective must be arrays.",
)
abort("reject raises")
}
if sources.length() + scans.length() > 256 || effective.length() > 1024 {
reject(
"resolution_limit", "", "At most 256 source/scan decisions combined and 1,024 effective assertions; narrow the review scope.",
)
}
let seen_sources : Map[String, Bool] = Map([])
let source_decisions = sources.map(value => {
let entry = resolution_object(
value,
["metadata", "path", "reason"],
"/sources",
)
let path = decision_path(entry, source=true)
if seen_sources.contains(path.to_lower()) {
reject(
"duplicate_resolution", path, "Each source path can be decided only once, including case variants.",
)
}
seen_sources[path.to_lower()] = true
let metadata = match entry["metadata"] {
Null => None
Object(_) as obj => {
// Reapply sidecar depth and text limits, not just envelope limits.
let parsed = parse_source(path, canonical_json(obj))
Some(parsed)
}
_ => {
reject(
"invalid_merge_resolution", path, "metadata must be a complete object, or null to remove the source.",
)
None
}
}
{ path, metadata, reason: decision_reason(entry, path), }
})
let seen_scans : Map[String, Bool] = Map([])
let scan_decisions = scans.map(value => {
let entry = resolution_object(
value,
["include", "path", "reason"],
"/scans",
)
let path = decision_path(entry, source=false)
if seen_scans.contains(path.to_lower()) {
reject(
"duplicate_resolution", path, "Each scan path can be decided only once, including case variants.",
)
}
seen_scans[path.to_lower()] = true
guard entry["include"] is (True | False) else {
reject(
"invalid_merge_resolution", path, "include must be a boolean; no coercion is performed.",
)
abort("reject raises")
}
{
path,
retained: entry["include"] == Json::boolean(true),
reason: decision_reason(entry, path),
}
})
let seen_effective : Map[String, Bool] = Map([])
let effective_decisions = effective.map(value => {
let entry = resolution_object(
value,
["key", "path", "present", "reason", "value"],
"/effective",
)
let path = decision_path(entry, source=false)
guard entry["key"] is String(key) && entry["present"] is (True | False) else {
reject(
"invalid_merge_resolution", path, "Effective assertions require a string key and boolean present.",
)
abort("reject raises")
}
let present = entry["present"] == Json::boolean(true)
let coordinate = resolution_coordinate(path, key)
if seen_effective.contains(coordinate) {
reject(
"duplicate_resolution", path, "Each effective path/key pair can occur only once.",
)
}
seen_effective[coordinate] = true
if !present && entry["value"] != Json::null() {
reject(
"invalid_merge_resolution", path, "An absent-field assertion requires value:null; unsupported data is never discarded.",
)
}
let value = if present {
Some(parse_source(path, canonical_json(entry["value"])))
} else {
None
}
{ path, key, value, reason: decision_reason(entry, path), }
})
source_decisions.sort_by((a, b) => lexical_compare(a.path, b.path))
scan_decisions.sort_by((a, b) => lexical_compare(a.path, b.path))
effective_decisions.sort_by((a, b) => {
let path = lexical_compare(a.path, b.path)
if path == 0 {
lexical_compare(a.key, b.key)
} else {
path
}
})
let plan : MergeResolution = {
base: DatasetIndex::from_json(canonical_json(fields["base"])),
left: DatasetIndex::from_json(canonical_json(fields["left"])),
right: DatasetIndex::from_json(canonical_json(fields["right"])),
sources: source_decisions,
scans: scan_decisions,
effective: effective_decisions,
}
if plan.to_json().length() > 16777216 {
reject(
"resolution_limit", "", "Canonical resolution exceeds 16,777,216 UTF-16 units.",
)
}
plan
}
///|
pub fn MergeResolution::to_json(self : MergeResolution) -> String {
canonical_json(
Json::object({
"base": self.base.manifest_value(),
"left": self.left.manifest_value(),
"right": self.right.manifest_value(),
"sources": Json::array(
self.sources.map(d => {
Json::object({
"path": Json::string(d.path),
"metadata": d.metadata.unwrap_or(Json::null()),
"reason": Json::string(d.reason),
})
}),
),
"scans": Json::array(
self.scans.map(d => {
Json::object({
"path": Json::string(d.path),
"include": Json::boolean(d.retained),
"reason": Json::string(d.reason),
})
}),
),
"effective": Json::array(
self.effective.map(d => {
Json::object({
"path": Json::string(d.path),
"key": Json::string(d.key),
"present": Json::boolean(d.value is Some(_)),
"value": d.value.unwrap_or(Json::null()),
"reason": Json::string(d.reason),
})
}),
),
}),
)
}
///|
/// A directly editable binding document; empty decisions do not waive conflicts.
pub fn MergeReport::resolution_template(self : MergeReport) -> String {
let plan : MergeResolution = {
base: self.base,
left: self.left,
right: self.right,
sources: [],
scans: [],
effective: [],
}
plan.to_json()
}
///|
fn MergeResolution::source_at(
self : MergeResolution,
path : String,
) -> SourceDecision? {
self.sources.search_by(d => d.path == path).map(i => self.sources[i])
}
///|
fn MergeResolution::scan_at(
self : MergeResolution,
path : String,
) -> ScanDecision? {
self.scans.search_by(d => d.path == path).map(i => self.scans[i])
}
///|
fn MergeResolution::has_effective(
self : MergeResolution,
path : String,
key : String,
) -> Bool {
self.effective.any(d => d.path == path && d.key == key)
}
///|
fn MergeResolution::check_effective(
self : MergeResolution,
candidate : DatasetIndex,
) -> Unit raise TraceError {
for decision in self.effective {
let result = candidate.resolve(decision.path)
if !merge_equal(result.values.get(decision.key), decision.value) {
reject(
"resolution_value_mismatch",
decision.path,
"Effective assertion for " +
Json::string(decision.key).stringify() +
" does not match the resolved candidate, including exact number tokens and presence; revise sources or the assertion explicitly.",
)
}
}
}
///|
fn paths_in_inputs(
base : DatasetIndex,
left : DatasetIndex,
right : DatasetIndex,
path : String,
source~ : Bool,
) -> Bool {
[base, left, right].any(index => {
if source {
index.sidecars.any(s => s.selector.path == path)
} else {
index.file_positions.contains(path)
}
})
}