use crate::root_cause_taxonomy::RootCauseFamily;
use serde::{Deserialize, Serialize};
pub const DEPENDENCY_PIN_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct KnownIssue {
pub id: &'static str,
pub package: &'static str,
pub symptom: &'static str,
pub family: RootCauseFamily,
pub fixed_in_rev: &'static str,
}
pub const KNOWN_ISSUES: &[KnownIssue] = &[
KnownIssue {
id: "fsqlite-openread-001",
package: "frankensqlite",
symptom: "OpenRead error opening main DB",
family: RootCauseFamily::FrankensqliteStorage,
fixed_in_rev: "a4923d4",
},
KnownIssue {
id: "fsqlite-fts-002",
package: "frankensqlite",
symptom: "FTS query fails while plain reads succeed",
family: RootCauseFamily::FrankensqliteStorage,
fixed_in_rev: "a4923d4",
},
KnownIssue {
id: "frankensearch-tantivy-001",
package: "frankensearch",
symptom: "missing Tantivy metadata / segment error",
family: RootCauseFamily::FrankensearchSearch,
fixed_in_rev: "be455cc",
},
];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinObservation {
pub package: String,
pub pinned_rev: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_local_rev: Option<String>,
pub local_dirty: bool,
pub checkout_present: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum PinState {
Current,
Stale,
DirtyLocalPatch,
MissingCheckout,
Unverified,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinAssessment {
pub schema_version: u32,
pub package: String,
pub pinned_rev: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub observed_local_rev: Option<String>,
pub pin_state: PinState,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub known_issue_ids: Vec<String>,
pub upstream_fix_possibly_missing: bool,
pub recommended_validation: String,
}
fn reconcile_state(obs: &PinObservation) -> PinState {
if !obs.checkout_present {
return PinState::MissingCheckout;
}
if obs.local_dirty {
return PinState::DirtyLocalPatch;
}
match &obs.observed_local_rev {
None => PinState::Unverified,
Some(local) if revs_match(local, &obs.pinned_rev) => PinState::Current,
Some(_) => PinState::Stale,
}
}
fn revs_match(a: &str, b: &str) -> bool {
let (a, b) = (a.trim(), b.trim());
if a == b {
return true;
}
let looks_hashy = |s: &str| s.len() >= 7 && s.chars().all(|c| c.is_ascii_hexdigit());
if looks_hashy(a) && looks_hashy(b) {
let (short, long) = if a.len() <= b.len() { (a, b) } else { (b, a) };
return long.starts_with(short);
}
false
}
fn issues_for_package(package: &str) -> impl Iterator<Item = &'static KnownIssue> {
let pkg = package.to_ascii_lowercase();
KNOWN_ISSUES
.iter()
.filter(move |issue| issue.package.eq_ignore_ascii_case(&pkg))
}
pub fn assess_pin(obs: &PinObservation) -> PinAssessment {
let pin_state = reconcile_state(obs);
let issues: Vec<&KnownIssue> = issues_for_package(&obs.package).collect();
let known_issue_ids: Vec<String> = issues.iter().map(|i| i.id.to_string()).collect();
let effective_rev = obs.observed_local_rev.as_deref().unwrap_or(&obs.pinned_rev);
let any_fix_unconfirmed = issues.iter().any(|issue| {
!revs_match(effective_rev, issue.fixed_in_rev)
|| matches!(
pin_state,
PinState::DirtyLocalPatch | PinState::MissingCheckout | PinState::Unverified
)
});
let upstream_fix_possibly_missing = !issues.is_empty() && any_fix_unconfirmed;
let recommended_validation = recommend(obs, pin_state);
PinAssessment {
schema_version: DEPENDENCY_PIN_SCHEMA_VERSION,
package: obs.package.clone(),
pinned_rev: obs.pinned_rev.clone(),
observed_local_rev: obs.observed_local_rev.clone(),
pin_state,
known_issue_ids,
upstream_fix_possibly_missing,
recommended_validation,
}
}
pub fn assess_pins(observations: &[PinObservation]) -> Vec<PinAssessment> {
observations.iter().map(assess_pin).collect()
}
fn recommend(obs: &PinObservation, state: PinState) -> String {
match state {
PinState::Current => format!(
"pin confirmed at {}; no action (re-run `cargo update -p {}` only to advance)",
obs.pinned_rev, obs.package
),
PinState::Stale => format!(
"local {} differs from pin {}; run `cargo update -p {} --precise <rev>` or rebuild against the pinned rev",
obs.observed_local_rev.as_deref().unwrap_or("rev"),
obs.pinned_rev,
obs.package
),
PinState::DirtyLocalPatch => format!(
"local checkout of {} is dirty; `git -C ../{} status` and commit/stash so the running code matches a known rev",
obs.package, obs.package
),
PinState::MissingCheckout => format!(
"no local checkout of {}; building from the pinned git rev {} (clone the sibling to verify against source)",
obs.package, obs.pinned_rev
),
PinState::Unverified => format!(
"cannot read local rev for {} (no-network/unreadable); verify with `git -C ../{} rev-parse HEAD`",
obs.package, obs.package
),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn obs(
pkg: &str,
pinned: &str,
local: Option<&str>,
dirty: bool,
present: bool,
) -> PinObservation {
PinObservation {
package: pkg.to_string(),
pinned_rev: pinned.to_string(),
observed_local_rev: local.map(str::to_string),
local_dirty: dirty,
checkout_present: present,
}
}
#[test]
fn current_pin_matches_and_needs_no_action() {
let a = assess_pin(&obs(
"frankensqlite",
"a4923d4",
Some("a4923d4"),
false,
true,
));
assert_eq!(a.pin_state, PinState::Current);
assert!(!a.upstream_fix_possibly_missing);
assert!(!a.known_issue_ids.is_empty());
}
#[test]
fn old_pin_is_stale_and_flags_missing_fix() {
let a = assess_pin(&obs(
"frankensqlite",
"a4923d4",
Some("0000abc"),
false,
true,
));
assert_eq!(a.pin_state, PinState::Stale);
assert!(a.upstream_fix_possibly_missing);
assert!(
a.known_issue_ids
.contains(&"fsqlite-openread-001".to_string())
);
assert!(a.recommended_validation.contains("cargo update"));
}
#[test]
fn dirty_local_patch_is_unverifiable() {
let a = assess_pin(&obs(
"frankensqlite",
"a4923d4",
Some("a4923d4"),
true,
true,
));
assert_eq!(a.pin_state, PinState::DirtyLocalPatch);
assert!(a.upstream_fix_possibly_missing);
assert!(a.recommended_validation.contains("dirty"));
}
#[test]
fn missing_sibling_checkout_is_flagged() {
let a = assess_pin(&obs("frankensqlite", "a4923d4", None, false, false));
assert_eq!(a.pin_state, PinState::MissingCheckout);
assert!(a.upstream_fix_possibly_missing);
assert!(a.recommended_validation.contains("no local checkout"));
}
#[test]
fn no_network_mode_is_unverified_when_checkout_present_but_rev_unknown() {
let a = assess_pin(&obs("frankensearch", "be455cc", None, false, true));
assert_eq!(a.pin_state, PinState::Unverified);
assert!(a.upstream_fix_possibly_missing);
assert!(a.recommended_validation.contains("rev-parse"));
}
#[test]
fn unverified_with_pin_at_fixed_rev_still_lists_issues() {
let a = assess_pin(&obs("frankensearch", "be455cc", None, false, true));
assert!(
a.known_issue_ids
.contains(&"frankensearch-tantivy-001".to_string())
);
}
#[test]
fn package_without_known_issues_has_no_fix_flag() {
let a = assess_pin(&obs(
"asupersync",
"deadbeef0",
Some("cafef00d1"),
false,
true,
));
assert_eq!(a.pin_state, PinState::Stale); assert!(a.known_issue_ids.is_empty());
assert!(!a.upstream_fix_possibly_missing);
}
#[test]
fn rev_match_tolerates_short_long_hash_prefixes() {
assert!(revs_match("a4923d4", "a4923d4097899e6e"));
assert!(revs_match("a4923d4097899e6e", "a4923d4"));
assert!(!revs_match("a4923d4", "b0000004"));
assert!(revs_match("1.0.28", "1.0.28"));
assert!(!revs_match("1.0.28", "1.0.29"));
assert!(!revs_match("a492", "a4923d4097"));
}
#[test]
fn current_pin_at_fixed_rev_confirms_fix_present() {
let a = assess_pin(&obs(
"frankensqlite",
"a4923d4097899e6e9805cefe67bce70e1b04a289",
Some("a4923d4097899e6e9805cefe67bce70e1b04a289"),
false,
true,
));
assert_eq!(a.pin_state, PinState::Current);
assert!(
!a.upstream_fix_possibly_missing,
"fix rev prefix-matches the pin"
);
}
#[test]
fn assessment_serializes_with_stable_fields_and_round_trips() {
let a = assess_pin(&obs(
"frankensqlite",
"a4923d4",
Some("0000abc"),
false,
true,
));
let value = serde_json::to_value(&a).unwrap();
assert_eq!(value["schema_version"], DEPENDENCY_PIN_SCHEMA_VERSION);
assert_eq!(value["package"], "frankensqlite");
assert_eq!(value["pin_state"], "stale");
assert_eq!(value["observed_local_rev"], "0000abc");
assert_eq!(value["upstream_fix_possibly_missing"], true);
assert!(!value["known_issue_ids"].as_array().unwrap().is_empty());
let back: PinAssessment = serde_json::from_value(value).unwrap();
assert_eq!(back, a);
}
#[test]
fn pin_state_wire_values_are_kebab_case() {
for (state, wire) in [
(PinState::Current, "current"),
(PinState::Stale, "stale"),
(PinState::DirtyLocalPatch, "dirty-local-patch"),
(PinState::MissingCheckout, "missing-checkout"),
(PinState::Unverified, "unverified"),
] {
assert_eq!(
serde_json::to_string(&state).unwrap(),
format!("\"{wire}\"")
);
}
}
#[test]
fn known_issues_reference_real_root_cause_families() {
for issue in KNOWN_ISSUES {
assert!(
issue.family.is_external_to_cass(),
"known dependency issue {} should attribute outside CASS",
issue.id
);
assert!(!issue.fixed_in_rev.is_empty());
assert!(!issue.id.is_empty());
}
}
#[test]
fn assess_pins_maps_all_observations() {
let observations = vec![
obs("frankensqlite", "a4923d4", Some("a4923d4"), false, true),
obs("frankensearch", "be455cc", None, false, false),
];
let assessments = assess_pins(&observations);
assert_eq!(assessments.len(), 2);
assert_eq!(assessments[0].pin_state, PinState::Current);
assert_eq!(assessments[1].pin_state, PinState::MissingCheckout);
}
}