///|
/// A supported consumer version bound to one declared contract and historical
/// snapshot. Labels are supplied by the caller, not authenticated provenance.
pub struct ConfigConsumerBinding {
consumer : String
version : String
baseline_id : String
contract : ConfigConsumerContract
baseline : ConfigValue
}
///|
pub fn ConfigConsumerBinding::new(
consumer : String,
version : String,
baseline_id : String,
contract : ConfigConsumerContract,
baseline : ConfigValue,
) -> Result[ConfigConsumerBinding, String] {
if consumer.trim().length() == 0 ||
version.trim().length() == 0 ||
baseline_id.trim().length() == 0 {
return Err("consumer, version and baseline_id must not be blank")
}
Ok({ consumer, version, baseline_id, contract, baseline, })
}
///|
/// A comparison under the SAME contract on both snapshots. Issue identity is
/// (path, kind), not just path. Persistent issues do not imply unchanged values.
pub struct ConfigConsumerRegression {
consumer : String
version : String
baseline_id : String
introduced : Array[ConfigConsumerIssue]
persistent : Array[ConfigConsumerIssue]
resolved : Array[ConfigConsumerIssue]
effective_comparable : Bool
effective_changes : Array[ConfigEffectiveChange]
}
///|
pub fn ConfigConsumerRegression::consumer(
self : ConfigConsumerRegression,
) -> String {
self.consumer
}
///|
pub fn ConfigConsumerRegression::version(
self : ConfigConsumerRegression,
) -> String {
self.version
}
///|
pub fn ConfigConsumerRegression::baseline_id(
self : ConfigConsumerRegression,
) -> String {
self.baseline_id
}
///|
pub fn ConfigConsumerRegression::introduced(
self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
self.introduced.copy()
}
///|
pub fn ConfigConsumerRegression::persistent(
self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
self.persistent.copy()
}
///|
pub fn ConfigConsumerRegression::resolved(
self : ConfigConsumerRegression,
) -> Array[ConfigConsumerIssue] {
self.resolved.copy()
}
///|
/// False if either snapshot violates the contract: an empty change array then
/// means unknown, NOT unchanged. Callers must check this before interpreting it.
pub fn ConfigConsumerRegression::effective_comparable(
self : ConfigConsumerRegression,
) -> Bool {
self.effective_comparable
}
///|
pub fn ConfigConsumerRegression::effective_changes(
self : ConfigConsumerRegression,
) -> Array[ConfigEffectiveChange] {
self.effective_changes.copy()
}
///|
/// A separate, opt-in behavior-preservation gate. Candidate acceptance via
/// passes() retains its original meaning. Unknown comparisons fail closed.
pub fn ConfigConsumerRegression::preserves_effective_values(
self : ConfigConsumerRegression,
) -> Bool {
self.effective_comparable &&
self.effective_changes.all(change => !change.value_changed)
}
///|
pub fn ConfigConsumerReleaseReport::preserves_effective_values(
self : ConfigConsumerReleaseReport,
) -> Bool {
self.entries.all(entry => entry.preserves_effective_values())
}
///|
/// Strict candidate acceptance: historical violations are NOT grandfathered.
pub fn ConfigConsumerRegression::passes(
self : ConfigConsumerRegression,
) -> Bool {
self.introduced.is_empty() && self.persistent.is_empty()
}
///|
pub struct ConfigConsumerReleaseReport {
entries : Array[ConfigConsumerRegression]
}
///|
pub fn ConfigConsumerReleaseReport::entries(
self : ConfigConsumerReleaseReport,
) -> Array[ConfigConsumerRegression] {
self.entries.copy()
}
///|
/// All supplied versions are supported and must accept the candidate. This
/// does not include structural gates or prove unchanged effective values.
pub fn ConfigConsumerReleaseReport::passes(
self : ConfigConsumerReleaseReport,
) -> Bool {
self.entries.all(entry => entry.passes())
}
///|
/// Reject empty support sets and duplicate (consumer, version) identities.
/// Results are ordered lexically by consumer then version (not semver).
/// Per-category issues retain the checker's stable path order.
pub fn ConfigConsumerReleaseReport::check(
bindings : Array[ConfigConsumerBinding],
candidate : ConfigValue,
) -> Result[ConfigConsumerReleaseReport, String] {
if bindings.is_empty() {
return Err("at least one supported consumer version is required")
}
let ordered = bindings.copy()
ordered.sort_by((a, b) => {
let order = a.consumer.lexical_compare(b.consumer)
if order == 0 {
a.version.lexical_compare(b.version)
} else {
order
}
})
for index in 1.. {
let historical = binding.contract.check(binding.baseline)
let proposed = binding.contract.check(candidate)
let effective_comparable = historical.is_empty() && proposed.is_empty()
let effective_changes = if effective_comparable {
consumer_effective_changes(binding.contract, binding.baseline, candidate)
} else {
[]
}
let introduced = proposed.filter(issue => !historical.contains(issue))
let persistent = proposed.filter(issue => historical.contains(issue))
let resolved = historical.filter(issue => !proposed.contains(issue))
ConfigConsumerRegression::{
consumer: binding.consumer,
version: binding.version,
baseline_id: binding.baseline_id,
introduced,
persistent,
resolved,
effective_comparable,
effective_changes,
}
})
Ok({ entries, })
}