use serde::Serialize;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ChangeSignal {
DefaultSensitivityFlipped {
subject: String,
before: String,
after: String,
},
MembershipGuardAdded {
subject: String,
members: Vec<String>,
domain: Vec<String>,
},
PersistedKeyRetired {
key: String,
scope: String,
},
DerivationSourceMoved {
derived: String,
from_key: String,
to_key: String,
},
BoundaryChanged {
subject: String,
},
PermissionPredicateChanged {
subject: String,
},
AggregationIntroduced {
subject: String,
},
TestMovedWithImplementation {
test: String,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum HypothesisWeight {
Low,
Medium,
High,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DefectHypothesis {
pub id: &'static str,
pub question: String,
pub because: String,
pub probes: Vec<String>,
pub weight: HypothesisWeight,
pub confidence: SignalConfidence,
pub evidence: String,
}
impl DefectHypothesis {
#[must_use]
pub fn blocks(&self) -> bool {
self.weight == HypothesisWeight::High && self.confidence == SignalConfidence::Confirmed
}
}
fn hypothesis(
id: &'static str,
weight: HypothesisWeight,
question: String,
because: String,
probes: Vec<String>,
) -> DefectHypothesis {
DefectHypothesis {
id,
question,
because,
probes,
weight,
confidence: SignalConfidence::Inferred,
evidence: String::new(),
}
}
#[must_use]
pub fn hypothesise(signals: &[DetectedSignal]) -> Vec<DefectHypothesis> {
let mut out: Vec<DefectHypothesis> = signals
.iter()
.flat_map(|detected| {
one(&detected.signal).into_iter().map(|mut item| {
item.confidence = detected.confidence;
item.evidence.clone_from(&detected.provenance);
item
})
})
.collect();
out.sort_by(|left, right| {
right
.weight
.cmp(&left.weight)
.then_with(|| left.id.cmp(right.id))
.then_with(|| left.question.cmp(&right.question))
});
out.dedup_by(|left, right| left.id == right.id && left.question == right.question);
out
}
fn one(signal: &ChangeSignal) -> Vec<DefectHypothesis> {
match signal {
ChangeSignal::MembershipGuardAdded {
subject,
members,
domain,
} => vec![membership_hypothesis(subject, members, domain)],
other => simple(other),
}
}
fn membership_hypothesis(subject: &str, members: &[String], domain: &[String]) -> DefectHypothesis {
let mut probes: Vec<String> = domain
.iter()
.filter(|item| !members.contains(item))
.map(|item| format!("`{subject}` = {item} — is the guard's answer right?"))
.collect();
probes.push(format!(
"`{subject}` absent — `has(undefined)` is false, is that intended?"
));
probes.push(format!(
"a value added to the domain later falls outside {{{}}} by default",
members.join(", ")
));
hypothesis(
"WVQ-HYP-002",
HypothesisWeight::High,
format!(
"Is every value of `{subject}` outside {{{}}} genuinely meant to take the other branch?",
members.join(", ")
),
format!("a membership guard on `{subject}` was introduced"),
probes,
)
}
fn simple(signal: &ChangeSignal) -> Vec<DefectHypothesis> {
match signal {
ChangeSignal::MembershipGuardAdded { .. } => Vec::new(),
ChangeSignal::DefaultSensitivityFlipped {
subject,
before,
after,
} => vec![hypothesis(
"WVQ-HYP-001",
HypothesisWeight::High,
format!(
"When `{subject}` is absent, does the new predicate reach the opposite branch?"
),
format!("the predicate changed from `{before}` to `{after}`"),
vec![
format!("`{subject}` undefined — old branch vs new branch"),
format!("`{subject}` explicitly false"),
format!("`{subject}` explicitly true"),
format!("what is the declared default of `{subject}`, and do all readers agree?"),
],
)],
ChangeSignal::PersistedKeyRetired { key, scope } => vec![hypothesis(
"WVQ-HYP-003",
HypothesisWeight::High,
format!("What happens to records already stored with `{key}`?"),
format!("`{key}` was retired from {scope}"),
vec![
format!("load an existing record that still carries `{key}`"),
format!("does anything still read `{key}`, or is it now silently ignored?"),
format!("does the behaviour that `{key}` used to control change for that record?"),
"is a migration needed, or is the change intended to be visible?".into(),
],
)],
ChangeSignal::DerivationSourceMoved {
derived,
from_key,
to_key,
} => vec![hypothesis(
"WVQ-HYP-004",
HypothesisWeight::High,
format!(
"On existing data, do `{from_key}` and `{to_key}` ever disagree about `{derived}`?"
),
format!("`{derived}` now derives from `{to_key}` instead of `{from_key}`"),
vec![
format!("a record where `{from_key}` and `{to_key}` imply different `{derived}`"),
format!("a record carrying `{from_key}` but no `{to_key}`"),
format!("a record carrying neither — what is `{derived}` then?"),
],
)],
ChangeSignal::BoundaryChanged { subject } => vec![hypothesis(
"WVQ-HYP-005",
HypothesisWeight::Medium,
format!("Is `{subject}` correct at the boundary itself, not just either side?"),
format!("a comparison on `{subject}` changed"),
vec![
format!("`{subject}` one below the limit"),
format!("`{subject}` exactly at the limit"),
format!("`{subject}` one above the limit"),
format!("`{subject}` at zero and at its maximum"),
],
)],
ChangeSignal::PermissionPredicateChanged { subject } => vec![hypothesis(
"WVQ-HYP-006",
HypothesisWeight::High,
format!("Does `{subject}` still deny everyone it denied before?"),
format!("a permission predicate on `{subject}` changed"),
vec![
"each role that was previously denied — still denied?".into(),
"tenant mismatch".into(),
"expired or missing credentials".into(),
"the deny path is the one that must keep dynamic coverage".into(),
],
)],
ChangeSignal::AggregationIntroduced { subject } => vec![hypothesis(
"WVQ-HYP-007",
HypothesisWeight::Medium,
format!("Does `{subject}` preserve the total it folds?"),
format!("`{subject}` folds or truncates a collection"),
vec![
"empty collection".into(),
"one element".into(),
"exactly at the fold threshold".into(),
"one past the threshold".into(),
"is the aggregate additive? a non-additive fold cannot be recovered".into(),
],
)],
ChangeSignal::TestMovedWithImplementation { test } => vec![hypothesis(
"WVQ-HYP-008",
HypothesisWeight::Medium,
format!("Does `{test}` assert intended behaviour, or the behaviour it now sees?"),
format!("`{test}` changed in the same commit as the code it exercises"),
vec![
"compare the assertion before and after the change".into(),
"would the old assertion still pass? if not, what decided it was wrong?".into(),
],
)],
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SignalConfidence {
Inferred,
Confirmed,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DetectedSignal {
pub signal: ChangeSignal,
pub confidence: SignalConfidence,
pub provenance: String,
}
impl ChangeSignal {
#[must_use]
pub fn inferred(self) -> DetectedSignal {
DetectedSignal {
signal: self,
confidence: SignalConfidence::Inferred,
provenance: "matched in the diff text".into(),
}
}
#[must_use]
pub fn subject(&self) -> &str {
match self {
Self::DefaultSensitivityFlipped { subject, .. }
| Self::MembershipGuardAdded { subject, .. }
| Self::BoundaryChanged { subject }
| Self::PermissionPredicateChanged { subject }
| Self::AggregationIntroduced { subject } => subject,
Self::PersistedKeyRetired { key, .. } => key,
Self::DerivationSourceMoved { derived, .. } => derived,
Self::TestMovedWithImplementation { test } => test,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct GraphFacts {
pub permission_symbols: Vec<String>,
pub limit_symbols: Vec<String>,
pub persisted_keys: Vec<String>,
pub domains: std::collections::BTreeMap<String, Vec<String>>,
pub changed_symbols: Vec<String>,
}
impl GraphFacts {
fn touched(&self, subject: &str) -> bool {
self.changed_symbols.iter().any(|item| item == subject)
}
}
#[must_use]
pub fn corroborate(signal: ChangeSignal, facts: &GraphFacts) -> DetectedSignal {
let subject = signal.subject().to_owned();
let confirmed_by = match &signal {
ChangeSignal::PermissionPredicateChanged { .. } => facts
.permission_symbols
.contains(&subject)
.then(|| format!("graph places `{subject}` on an authorization path")),
ChangeSignal::BoundaryChanged { .. } => facts
.limit_symbols
.contains(&subject)
.then(|| format!("graph knows `{subject}` as a named limit")),
ChangeSignal::PersistedKeyRetired { .. } => facts
.persisted_keys
.contains(&subject)
.then(|| format!("graph shows `{subject}` reaching persisted storage")),
ChangeSignal::MembershipGuardAdded { .. } => facts
.domains
.get(&subject)
.filter(|domain| !domain.is_empty())
.map(|domain| format!("graph enumerates {} values for `{subject}`", domain.len())),
ChangeSignal::DefaultSensitivityFlipped { .. }
| ChangeSignal::DerivationSourceMoved { .. } => facts
.touched(&subject)
.then(|| format!("graph confirms `{subject}` changed in this revision")),
ChangeSignal::TestMovedWithImplementation { .. }
| ChangeSignal::AggregationIntroduced { .. } => None,
};
match confirmed_by {
Some(provenance) => DetectedSignal {
signal,
confidence: SignalConfidence::Confirmed,
provenance,
},
None => signal.inferred(),
}
}
#[must_use]
pub fn blocking_questions(hypotheses: &[DefectHypothesis]) -> Vec<&DefectHypothesis> {
hypotheses
.iter()
.filter(|item| {
item.weight == HypothesisWeight::High && item.confidence == SignalConfidence::Confirmed
})
.collect()
}