use serde::{Deserialize, Serialize};
pub const TRUST_SCHEMA_VERSION: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
Trusted,
Likely,
Unverified,
Stale,
Failed,
}
impl TrustTier {
pub fn as_str(self) -> &'static str {
match self {
TrustTier::Trusted => "trusted",
TrustTier::Likely => "likely",
TrustTier::Unverified => "unverified",
TrustTier::Stale => "stale",
TrustTier::Failed => "failed",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustConfidence {
Low,
Medium,
High,
}
impl TrustConfidence {
pub fn as_str(self) -> &'static str {
match self {
TrustConfidence::Low => "low",
TrustConfidence::Medium => "medium",
TrustConfidence::High => "high",
}
}
fn floor(self, other: TrustConfidence) -> TrustConfidence {
if self <= other { self } else { other }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OutcomeMarker {
Landed,
Failed,
Superseded,
Open,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ProofStatus {
Proven,
ProofDebt,
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RealizedMode {
Hybrid,
Semantic,
Lexical,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustSignals {
#[serde(default)]
pub age_days: Option<u64>,
pub workspace_match: bool,
#[serde(default)]
pub linked_commit: Option<String>,
#[serde(default)]
pub linked_closed_bead: Option<String>,
#[serde(default)]
pub linked_lessons: Vec<String>,
pub outcome: OutcomeMarker,
pub proof: ProofStatus,
#[serde(default)]
pub release_tag: Option<String>,
pub source_healthy: bool,
pub realized_mode: RealizedMode,
}
impl Default for TrustSignals {
fn default() -> Self {
TrustSignals {
age_days: None,
workspace_match: true,
linked_commit: None,
linked_closed_bead: None,
linked_lessons: Vec::new(),
outcome: OutcomeMarker::Unknown,
proof: ProofStatus::Unknown,
release_tag: None,
source_healthy: true,
realized_mode: RealizedMode::Hybrid,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustAssessment {
pub schema_version: u32,
pub trust_tier: TrustTier,
pub confidence: TrustConfidence,
pub provenance_refs: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stale_reason: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub recommended_followup: Option<String>,
}
const STALE_AGE_DAYS: u64 = 365;
const AGING_AGE_DAYS: u64 = 180;
const COMMIT_REF_LEN: usize = 12;
fn sanitize_ref_value(value: &str) -> String {
value
.chars()
.filter(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_'))
.take(64)
.collect()
}
fn build_provenance_refs(signals: &TrustSignals) -> Vec<String> {
let mut refs = Vec::new();
if let Some(commit) = &signals.linked_commit {
let short: String = commit
.chars()
.filter(|c| c.is_ascii_hexdigit())
.take(COMMIT_REF_LEN)
.collect();
if !short.is_empty() {
refs.push(format!("commit:{short}"));
}
}
if let Some(bead) = &signals.linked_closed_bead {
let id = sanitize_ref_value(bead);
if !id.is_empty() {
refs.push(format!("bead:{id}"));
}
}
let mut lessons = signals
.linked_lessons
.iter()
.filter_map(|lesson| {
let id = sanitize_ref_value(lesson);
let suffix = id.strip_prefix("lsn-")?;
(id == lesson.trim()
&& suffix.len() == 16
&& suffix.chars().all(|c| c.is_ascii_hexdigit()))
.then(|| format!("lesson:{id}"))
})
.collect::<Vec<_>>();
lessons.sort();
lessons.dedup();
refs.extend(lessons);
if let Some(tag) = &signals.release_tag {
let tag = sanitize_ref_value(tag);
if !tag.is_empty() {
refs.push(format!("release:{tag}"));
}
}
refs
}
pub fn assess_trust(signals: &TrustSignals) -> TrustAssessment {
let provenance_refs = build_provenance_refs(signals);
let has_commit = signals.linked_commit.is_some();
let has_bead = signals.linked_closed_bead.is_some();
let has_release = signals.release_tag.is_some();
let proven = matches!(signals.proof, ProofStatus::Proven);
let has_provenance = has_commit || has_bead;
if matches!(signals.outcome, OutcomeMarker::Failed) {
return TrustAssessment {
schema_version: TRUST_SCHEMA_VERSION,
trust_tier: TrustTier::Failed,
confidence: TrustConfidence::Low,
provenance_refs,
stale_reason: Some("failed_attempt".to_string()),
recommended_followup: Some(
"This was a failed or reverted attempt — prefer a newer landed result.".to_string(),
),
};
}
if matches!(signals.outcome, OutcomeMarker::Superseded) {
return TrustAssessment {
schema_version: TRUST_SCHEMA_VERSION,
trust_tier: TrustTier::Stale,
confidence: TrustConfidence::Low,
provenance_refs,
stale_reason: Some("superseded_by_newer".to_string()),
recommended_followup: Some(
"A newer change supersedes this — look for the superseding fix.".to_string(),
),
};
}
let landed = matches!(signals.outcome, OutcomeMarker::Landed);
let mut tier = if landed && proven && (has_release || has_bead) {
TrustTier::Trusted
} else if has_provenance {
TrustTier::Likely
} else {
TrustTier::Unverified
};
let mut confidence = TrustConfidence::High;
let mut reason: Option<&'static str> = None;
if !signals.source_healthy {
confidence = confidence.floor(TrustConfidence::Low);
reason.get_or_insert("source_unhealthy");
}
if matches!(signals.outcome, OutcomeMarker::Open) {
if tier < TrustTier::Unverified {
tier = TrustTier::Unverified;
}
confidence = confidence.floor(TrustConfidence::Medium);
reason.get_or_insert("linked_bead_open");
}
if let Some(age) = signals.age_days {
if age >= STALE_AGE_DAYS && !has_release {
if tier < TrustTier::Stale {
tier = TrustTier::Stale;
}
confidence = confidence.floor(TrustConfidence::Medium);
reason.get_or_insert("aged_out");
} else if age >= AGING_AGE_DAYS && matches!(tier, TrustTier::Unverified) {
confidence = confidence.floor(TrustConfidence::Medium);
reason.get_or_insert("aged_out");
}
}
if !signals.workspace_match {
confidence = confidence.floor(TrustConfidence::Medium);
reason.get_or_insert("workspace_mismatch");
}
if matches!(signals.realized_mode, RealizedMode::Lexical) {
confidence = confidence.floor(TrustConfidence::Medium);
}
if !has_provenance {
confidence = confidence.floor(TrustConfidence::Medium);
}
if matches!(tier, TrustTier::Unverified) {
confidence = confidence.floor(TrustConfidence::Medium);
}
let recommended_followup = match tier {
TrustTier::Trusted => None,
TrustTier::Likely => Some(
"Likely current — confirm via the cited commit or closed bead before relying on it."
.to_string(),
),
TrustTier::Unverified => Some(
"Unverified — corroborate before reuse (no linked commit or closed bead).".to_string(),
),
TrustTier::Stale => Some(
"Aged out of its useful window — prefer a newer, release-contained result.".to_string(),
),
TrustTier::Failed => None,
};
TrustAssessment {
schema_version: TRUST_SCHEMA_VERSION,
trust_tier: tier,
confidence,
provenance_refs,
stale_reason: reason.map(str::to_string),
recommended_followup,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceTrustKind {
LocalPresent,
RemoteMirror,
ArchiveOnly,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HitTrustContext {
pub created_at_ms: Option<i64>,
pub now_ms: i64,
pub workspace: Option<String>,
pub query_workspace: Option<String>,
pub source_kind: SourceTrustKind,
pub realized_mode: RealizedMode,
pub linked_commit: Option<String>,
pub linked_closed_bead: Option<String>,
pub linked_lessons: Vec<String>,
pub outcome: OutcomeMarker,
pub proof: ProofStatus,
pub release_tag: Option<String>,
}
impl Default for HitTrustContext {
fn default() -> Self {
HitTrustContext {
created_at_ms: None,
now_ms: 0,
workspace: None,
query_workspace: None,
source_kind: SourceTrustKind::LocalPresent,
realized_mode: RealizedMode::Hybrid,
linked_commit: None,
linked_closed_bead: None,
linked_lessons: Vec::new(),
outcome: OutcomeMarker::Unknown,
proof: ProofStatus::Unknown,
release_tag: None,
}
}
}
fn normalize_workspace(ws: &str) -> &str {
ws.trim().trim_end_matches('/')
}
pub fn derive_trust_signals(ctx: &HitTrustContext) -> TrustSignals {
let age_days = ctx.created_at_ms.map(|created| {
let delta_ms = ctx.now_ms.saturating_sub(created).max(0);
(delta_ms / 86_400_000) as u64
});
let workspace_match = match (ctx.query_workspace.as_deref(), ctx.workspace.as_deref()) {
(None, _) => true,
(Some(q), Some(w)) => normalize_workspace(q).eq_ignore_ascii_case(normalize_workspace(w)),
(Some(_), None) => false,
};
let source_healthy = matches!(
ctx.source_kind,
SourceTrustKind::LocalPresent | SourceTrustKind::RemoteMirror
);
TrustSignals {
age_days,
workspace_match,
linked_commit: ctx.linked_commit.clone(),
linked_closed_bead: ctx.linked_closed_bead.clone(),
linked_lessons: ctx.linked_lessons.clone(),
outcome: ctx.outcome,
proof: ctx.proof,
release_tag: ctx.release_tag.clone(),
source_healthy,
realized_mode: ctx.realized_mode,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn landed_release_proven() -> TrustSignals {
TrustSignals {
age_days: Some(10),
workspace_match: true,
linked_commit: Some("ab0d12ef90abcdef1234567890abcdef12345678".to_string()),
linked_closed_bead: Some("xyz123".to_string()),
linked_lessons: vec!["lsn-0123456789abcdef".to_string()],
outcome: OutcomeMarker::Landed,
proof: ProofStatus::Proven,
release_tag: Some("v0.6.15".to_string()),
source_healthy: true,
realized_mode: RealizedMode::Hybrid,
}
}
#[test]
fn landed_commit_is_trusted_high() {
let a = assess_trust(&landed_release_proven());
assert_eq!(a.trust_tier, TrustTier::Trusted);
assert_eq!(a.confidence, TrustConfidence::High);
assert_eq!(a.stale_reason, None);
assert!(
a.provenance_refs
.contains(&"commit:ab0d12ef90ab".to_string())
);
assert!(a.provenance_refs.contains(&"release:v0.6.15".to_string()));
assert!(a.provenance_refs.contains(&"bead:xyz123".to_string()));
assert!(
a.provenance_refs
.contains(&"lesson:lsn-0123456789abcdef".to_string())
);
}
#[test]
fn closed_bead_with_proof_is_trusted() {
let s = TrustSignals {
linked_commit: None,
linked_closed_bead: Some("bead-9".to_string()),
outcome: OutcomeMarker::Landed,
proof: ProofStatus::Proven,
release_tag: None,
age_days: Some(20),
..TrustSignals::default()
};
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Trusted);
assert_eq!(a.confidence, TrustConfidence::High);
assert_eq!(a.provenance_refs, vec!["bead:bead-9".to_string()]);
}
#[test]
fn open_bead_is_unverified_with_reason() {
let s = TrustSignals {
outcome: OutcomeMarker::Open,
linked_closed_bead: None,
proof: ProofStatus::ProofDebt,
age_days: Some(5),
..TrustSignals::default()
};
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Unverified);
assert_eq!(a.stale_reason.as_deref(), Some("linked_bead_open"));
assert!(a.confidence <= TrustConfidence::Medium);
}
#[test]
fn failed_command_is_failed_low() {
let s = TrustSignals {
outcome: OutcomeMarker::Failed,
..TrustSignals::default()
};
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Failed);
assert_eq!(a.confidence, TrustConfidence::Low);
assert_eq!(a.stale_reason.as_deref(), Some("failed_attempt"));
assert!(a.recommended_followup.is_some());
}
#[test]
fn superseded_fix_is_stale() {
let s = TrustSignals {
outcome: OutcomeMarker::Superseded,
linked_commit: Some("deadbeefdeadbeefdeadbeef".to_string()),
..TrustSignals::default()
};
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Stale);
assert_eq!(a.stale_reason.as_deref(), Some("superseded_by_newer"));
assert!(a.provenance_refs.iter().any(|r| r.starts_with("commit:")));
}
#[test]
fn lexical_only_fallback_caps_confidence() {
let mut s = landed_release_proven();
s.realized_mode = RealizedMode::Lexical;
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Trusted);
assert!(a.confidence <= TrustConfidence::Medium);
}
#[test]
fn workspace_mismatch_lowers_confidence_and_sets_reason() {
let mut s = landed_release_proven();
s.workspace_match = false;
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Trusted, "ordering/tier unaffected");
assert!(a.confidence <= TrustConfidence::Medium);
assert_eq!(a.stale_reason.as_deref(), Some("workspace_mismatch"));
}
#[test]
fn aged_unreleased_result_becomes_stale() {
let s = TrustSignals {
age_days: Some(400),
linked_commit: Some("abcabcabcabc".to_string()),
outcome: OutcomeMarker::Landed,
proof: ProofStatus::ProofDebt,
release_tag: None,
..TrustSignals::default()
};
let a = assess_trust(&s);
assert_eq!(a.trust_tier, TrustTier::Stale);
assert_eq!(a.stale_reason.as_deref(), Some("aged_out"));
}
#[test]
fn no_provenance_is_unverified_capped_medium() {
let a = assess_trust(&TrustSignals::default());
assert_eq!(a.trust_tier, TrustTier::Unverified);
assert!(a.provenance_refs.is_empty());
assert!(a.confidence <= TrustConfidence::Medium);
}
#[test]
fn source_unhealthy_dominates_reason_and_drops_confidence() {
let mut s = landed_release_proven();
s.source_healthy = false;
let a = assess_trust(&s);
assert_eq!(a.confidence, TrustConfidence::Low);
assert_eq!(a.stale_reason.as_deref(), Some("source_unhealthy"));
}
#[test]
fn refs_are_sanitized_to_safe_tokens() {
let s = TrustSignals {
linked_closed_bead: Some("bd-1 /home/alice/x 'or' 1=1".to_string()),
release_tag: Some("v1.0 (drop table users);".to_string()),
outcome: OutcomeMarker::Landed,
proof: ProofStatus::Proven,
..TrustSignals::default()
};
let a = assess_trust(&s);
for r in &a.provenance_refs {
let value = r.split_once(':').map(|(_, v)| v).unwrap_or(r);
assert!(
value
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')),
"ref value not token-safe: {r}"
);
}
let joined = a.provenance_refs.join(" ");
assert!(
!joined.contains('/'),
"no path separators in refs: {joined}"
);
assert!(
!joined.contains("table users"),
"injection phrase broken: {joined}"
);
}
#[test]
fn lesson_refs_are_validated_and_citation_only() {
let signals = TrustSignals {
linked_lessons: vec![
"lsn-0123456789abcdef".to_string(),
"lsn-0123456789abcdef".to_string(),
"lsn-0123456789abcdeg".to_string(),
"lsn-0123456789abcdef /home/private".to_string(),
],
..TrustSignals::default()
};
let assessment = assess_trust(&signals);
assert_eq!(assessment.trust_tier, TrustTier::Unverified);
assert_eq!(
assessment.provenance_refs,
vec!["lesson:lsn-0123456789abcdef".to_string()]
);
}
#[test]
fn assessment_json_contract_is_stable_and_round_trips() {
let a = assess_trust(&landed_release_proven());
let value = serde_json::to_value(&a).unwrap();
assert_eq!(value["schema_version"], TRUST_SCHEMA_VERSION);
assert_eq!(value["trust_tier"], "trusted");
assert_eq!(value["confidence"], "high");
assert!(value.get("stale_reason").is_none());
let back: TrustAssessment = serde_json::from_value(value).unwrap();
assert_eq!(back, a);
}
#[test]
fn scoring_is_deterministic() {
let s = landed_release_proven();
assert_eq!(assess_trust(&s), assess_trust(&s));
}
#[test]
fn tier_and_confidence_wire_labels_are_stable() {
assert_eq!(TrustTier::Trusted.as_str(), "trusted");
assert_eq!(TrustTier::Failed.as_str(), "failed");
assert_eq!(TrustConfidence::High.as_str(), "high");
assert!(TrustTier::Trusted < TrustTier::Failed);
assert!(TrustConfidence::Low < TrustConfidence::High);
}
const DAY_MS: i64 = 86_400_000;
#[test]
fn derive_age_days_handles_none_recent_and_future() {
let none = derive_trust_signals(&HitTrustContext {
created_at_ms: None,
now_ms: 100 * DAY_MS,
..HitTrustContext::default()
});
assert_eq!(none.age_days, None);
let recent = derive_trust_signals(&HitTrustContext {
created_at_ms: Some(98 * DAY_MS),
now_ms: 100 * DAY_MS,
..HitTrustContext::default()
});
assert_eq!(recent.age_days, Some(2));
let future = derive_trust_signals(&HitTrustContext {
created_at_ms: Some(200 * DAY_MS),
now_ms: 100 * DAY_MS,
..HitTrustContext::default()
});
assert_eq!(future.age_days, Some(0));
}
#[test]
fn derive_workspace_match_normalizes_and_handles_no_filter() {
let no_filter = derive_trust_signals(&HitTrustContext {
query_workspace: None,
workspace: Some("/proj/a".to_string()),
..HitTrustContext::default()
});
assert!(no_filter.workspace_match);
let normalized = derive_trust_signals(&HitTrustContext {
query_workspace: Some("/Proj/A/".to_string()),
workspace: Some("/proj/a".to_string()),
..HitTrustContext::default()
});
assert!(normalized.workspace_match);
let mismatch = derive_trust_signals(&HitTrustContext {
query_workspace: Some("/proj/a".to_string()),
workspace: Some("/proj/b".to_string()),
..HitTrustContext::default()
});
assert!(!mismatch.workspace_match);
let missing = derive_trust_signals(&HitTrustContext {
query_workspace: Some("/proj/a".to_string()),
workspace: None,
..HitTrustContext::default()
});
assert!(!missing.workspace_match);
}
#[test]
fn derive_source_health_maps_from_kind() {
for (kind, healthy) in [
(SourceTrustKind::LocalPresent, true),
(SourceTrustKind::RemoteMirror, true),
(SourceTrustKind::ArchiveOnly, false),
] {
let s = derive_trust_signals(&HitTrustContext {
source_kind: kind,
..HitTrustContext::default()
});
assert_eq!(s.source_healthy, healthy, "kind {kind:?}");
}
}
#[test]
fn derive_then_assess_recent_unlinked_local_is_unverified() {
let ctx = HitTrustContext {
created_at_ms: Some(99 * DAY_MS),
now_ms: 100 * DAY_MS,
source_kind: SourceTrustKind::LocalPresent,
realized_mode: RealizedMode::Hybrid,
..HitTrustContext::default()
};
let a = assess_trust(&derive_trust_signals(&ctx));
assert_eq!(a.trust_tier, TrustTier::Unverified);
assert!(a.confidence <= TrustConfidence::Medium);
assert!(a.provenance_refs.is_empty());
}
#[test]
fn derive_then_assess_old_archive_only_is_stale() {
let ctx = HitTrustContext {
created_at_ms: Some(0),
now_ms: 500 * DAY_MS,
source_kind: SourceTrustKind::ArchiveOnly,
..HitTrustContext::default()
};
let a = assess_trust(&derive_trust_signals(&ctx));
assert_eq!(a.trust_tier, TrustTier::Stale);
assert_eq!(a.stale_reason.as_deref(), Some("source_unhealthy"));
}
}