use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use uuid::Uuid;
pub type DeviceId = Uuid;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecordSource {
StaticAnalysis,
ClaudeEnrich,
SessionHook,
DeveloperManual,
Import,
}
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum AgentKind {
Claude,
Codex,
Cli,
Supervisor,
Unknown,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Category {
Gotcha,
File,
Decision,
Stage,
Dependency,
DevNote,
Session,
Analytics,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum Priority {
Low,
Normal,
High,
Critical,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum QualityTier {
Suppressed,
Poor,
Acceptable,
Good,
Excellent,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum QualitySignal {
HasImperativeVerb,
HasCausality,
HasSeveritySet,
HasReference,
RuleLengthAdequate,
ReasonLengthAdequate,
AffectedFilesSpecified,
HasSpecificIdentifier,
VaguePhrasing,
NoActionableRule,
NoReason,
TooShort,
DuplicatesFilePurpose,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct QualityScore {
pub value: f32,
pub tier: QualityTier,
pub signals: Vec<QualitySignal>,
pub computed_at: u64,
}
impl QualityScore {
pub fn layer0_default() -> Self {
Self {
value: 0.10,
tier: QualityTier::Suppressed,
signals: vec![],
computed_at: 0,
}
}
pub fn doc_comment_default() -> Self {
Self {
value: 0.40,
tier: QualityTier::Acceptable,
signals: vec![],
computed_at: 0,
}
}
pub fn developer_entry_default() -> Self {
Self {
value: 0.65,
tier: QualityTier::Good,
signals: vec![],
computed_at: 0,
}
}
pub fn cochange_default() -> Self {
Self {
value: 0.40,
tier: QualityTier::Acceptable,
signals: vec![],
computed_at: 0,
}
}
pub fn cochange_strong() -> Self {
Self {
value: 0.60,
tier: QualityTier::Acceptable,
signals: vec![],
computed_at: 0,
}
}
pub fn tier_from_value(value: f32) -> QualityTier {
if !value.is_finite() || value < 0.2 {
QualityTier::Suppressed
} else if value < 0.4 {
QualityTier::Poor
} else if value < 0.7 {
QualityTier::Acceptable
} else if value < 0.9 {
QualityTier::Good
} else {
QualityTier::Excellent
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum StalenessTier {
Fresh,
Aging,
Stale,
Liability,
Tombstone,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum StalenessSignal {
NotAccessedDays(u32),
LinesChangedPct(f32),
EntryPointsChanged(u32),
ImportsChanged(u32),
FileDeleted,
FileRenamed {
new_path: String,
},
DependencyBumped {
dep: String,
old_ver: String,
new_ver: String,
},
LinkedFileChanged {
path: String,
},
CascadeFromDecision(String),
TodosChanged,
UnsafeCountChanged(i32),
UnwrapCountChanged(i32),
GitCommitsSince(u32),
}
impl std::fmt::Display for StalenessSignal {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotAccessedDays(d) => write!(f, "not accessed for {d} days"),
Self::LinesChangedPct(pct) => write!(f, "{:.0}% of lines changed", pct * 100.0),
Self::EntryPointsChanged(n) => write!(f, "{n} entry points changed"),
Self::ImportsChanged(n) => write!(f, "{n} imports changed"),
Self::FileDeleted => write!(f, "source file deleted"),
Self::FileRenamed { new_path } => write!(f, "file renamed to {new_path}"),
Self::DependencyBumped {
dep,
old_ver,
new_ver,
} => write!(f, "{dep} bumped {old_ver} \u{2192} {new_ver}"),
Self::LinkedFileChanged { path } => write!(f, "linked file {path} changed"),
Self::CascadeFromDecision(key) => write!(f, "cascaded from {key}"),
Self::TodosChanged => write!(f, "TODOs changed"),
Self::UnsafeCountChanged(delta) => write!(f, "unsafe count changed by {delta}"),
Self::UnwrapCountChanged(delta) => write!(f, "unwrap count changed by {delta}"),
Self::GitCommitsSince(n) => write!(f, "{n} commits since last confirmation"),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StalenessScore {
pub value: f32,
pub tier: StalenessTier,
pub signals: Vec<StalenessSignal>,
pub computed_at: u64,
pub last_record_sha: String,
}
impl StalenessScore {
pub fn fresh() -> Self {
Self {
value: 0.0,
tier: StalenessTier::Fresh,
signals: vec![],
computed_at: 0,
last_record_sha: String::new(),
}
}
pub fn tier_from_value(value: f32) -> StalenessTier {
if !value.is_finite() {
return StalenessTier::Stale;
}
if value < 0.2 {
StalenessTier::Fresh
} else if value < 0.4 {
StalenessTier::Aging
} else if value < 0.7 {
StalenessTier::Stale
} else if value < 0.9 {
StalenessTier::Liability
} else {
StalenessTier::Tombstone
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ConfidenceScore {
pub value: f32,
pub confirmation_count: u32,
pub contributor_count: u32,
pub last_challenged: Option<u64>,
pub challenge_count: u32,
}
impl ConfidenceScore {
pub fn base_for_source(source: &RecordSource) -> f32 {
match source {
RecordSource::DeveloperManual => 0.80,
RecordSource::Import => 0.70,
RecordSource::ClaudeEnrich => 0.60,
RecordSource::SessionHook => 0.50,
RecordSource::StaticAnalysis => 0.10,
}
}
pub fn for_new_record(source: &RecordSource) -> Self {
Self {
value: Self::base_for_source(source),
confirmation_count: 0,
contributor_count: 1,
last_challenged: None,
challenge_count: 0,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TombstoneReason {
FileDeleted,
FileRenamed { new_path: String },
ManualDeletion,
Superseded,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum RecordLifecycle {
Active,
Tombstoned { reason: TombstoneReason, at: u64 },
Superseded { by_key: String },
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct RecordVersion {
pub device_id: DeviceId,
pub logical_clock: u64,
pub wall_clock: u64,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct Record {
pub key: String,
pub value: String,
pub category: Category,
pub priority: Priority,
pub tags: Vec<String>,
pub created_at: u64,
pub updated_at: u64,
pub ref_url: Option<String>,
pub staleness: StalenessScore,
pub lifecycle: RecordLifecycle,
pub version: RecordVersion,
pub quality: QualityScore,
pub access_count: u32,
pub last_accessed: u64,
pub source: RecordSource,
pub confidence: ConfidenceScore,
pub gap_analysis_score: f32,
#[serde(default)]
pub payload: Option<JsonValue>,
}
impl Record {
pub fn device_id(&self) -> DeviceId {
self.version.device_id
}
pub fn payload_as<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
self.payload
.as_ref()
.and_then(|p| serde_json::from_value(p.clone()).ok())
}
pub fn layer0_file_stub(
key: impl Into<String>,
device_id: DeviceId,
logical_clock: u64,
wall_clock: u64,
) -> Self {
Self {
key: key.into(),
value: String::new(),
category: Category::File,
priority: Priority::Normal,
tags: vec![],
created_at: wall_clock,
updated_at: wall_clock,
ref_url: None,
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id,
logical_clock,
wall_clock,
},
quality: QualityScore::layer0_default(),
access_count: 0,
last_accessed: 0,
source: RecordSource::StaticAnalysis,
confidence: ConfidenceScore::for_new_record(&RecordSource::StaticAnalysis),
gap_analysis_score: 0.0,
payload: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum TodoKind {
Todo,
Fixme,
Hack,
Note,
Deprecated,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
pub struct TodoComment {
pub text: String,
pub line: u32,
pub kind: TodoKind,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FileRecord {
pub path: String,
pub purpose: String,
pub entry_points: Vec<String>,
pub imports: Vec<String>,
pub gotcha_keys: Vec<String>,
pub decision_keys: Vec<String>,
pub todos: Vec<TodoComment>,
pub unsafe_count: u32,
pub unwrap_count: u32,
pub change_frequency: u32,
pub last_author: Option<String>,
pub is_hotspot: bool,
pub token_cost_estimate: u32,
pub last_modified_session: u64,
#[serde(default)]
pub content_hash: Option<String>,
#[serde(default)]
pub line_count: u32,
#[serde(default)]
pub blast_radius: Option<crate::analysis::blast_radius::BlastRadius>,
#[serde(default)]
pub propagated_staleness: Option<crate::analysis::propagation::PropagatedStaleness>,
}
impl FileRecord {
#[allow(clippy::too_many_arguments)]
pub fn layer0_stub(
path: impl Into<String>,
entry_points: Vec<String>,
imports: Vec<String>,
todos: Vec<TodoComment>,
unsafe_count: u32,
unwrap_count: u32,
change_frequency: u32,
last_author: Option<String>,
is_hotspot: bool,
token_cost_estimate: u32,
last_modified_session: u64,
) -> Self {
Self {
path: path.into(),
purpose: String::new(),
entry_points,
imports,
gotcha_keys: vec![],
decision_keys: vec![],
todos,
unsafe_count,
unwrap_count,
change_frequency,
last_author,
is_hotspot,
token_cost_estimate,
last_modified_session,
content_hash: None,
line_count: 0,
blast_radius: None,
propagated_staleness: None,
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GotchaRecord {
pub rule: String,
pub reason: String,
pub severity: Priority,
#[serde(default)]
pub affected_files: Vec<String>,
#[serde(default)]
pub ref_url: Option<String>,
#[serde(default)]
pub discovered_session: u64,
#[serde(default)]
pub confirmed: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StaleReviewEntry {
pub key: String,
pub staleness_value: f32,
pub tier: StalenessTier,
pub last_updated: u64,
pub signals: Vec<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct StaleReviewPayload {
pub session_timestamp: u64,
pub entries: Vec<StaleReviewEntry>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum GapType {
HotFileNoRecord,
HotFileNoPurpose,
HotFileNoGotchas,
FrequentlyReadNoEnrich,
OrphanedDecision,
DependencyUnknown,
CoChangePairUnmapped,
StaleHotspot,
HotFileNoTests,
HighFanInNoContract,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct KnowledgeGap {
pub key: String,
pub gap_type: GapType,
pub risk_score: f32,
pub description: String,
pub action_hint: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct ContextPacket {
pub stage: Option<Record>,
pub critical_gotchas: Vec<Record>,
pub file_records: Vec<FileRecord>,
pub related_decisions: Vec<Record>,
pub recent_session: Option<String>,
pub token_estimate: u32,
pub stale_warnings: Vec<String>,
pub unconfirmed_candidates: Vec<String>,
pub knowledge_gaps: Vec<KnowledgeGap>,
pub compliance_rate: Option<f32>,
pub injection_string: String,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct OnboardingScore {
pub estimated_minutes: f32,
pub critical_files_covered: f32,
pub gotcha_coverage: f32,
pub decision_coverage: f32,
pub avg_confidence: f32,
pub computed_at: u64,
}
#[cfg(test)]
mod tests {
use super::*;
fn device_id() -> DeviceId {
Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()
}
fn sample_record() -> Record {
Record {
key: "gotcha:inference-async".to_string(),
value: "Never call .await inside a rayon::spawn closure — it panics.".to_string(),
category: Category::Gotcha,
priority: Priority::Critical,
tags: vec!["async".to_string(), "rayon".to_string()],
created_at: 1_710_520_800,
updated_at: 1_710_520_800,
ref_url: Some("https://github.com/example/issue/42".to_string()),
staleness: StalenessScore::fresh(),
lifecycle: RecordLifecycle::Active,
version: RecordVersion {
device_id: device_id(),
logical_clock: 1,
wall_clock: 1_710_520_800,
},
quality: QualityScore {
value: 0.85,
tier: QualityTier::Good,
signals: vec![
QualitySignal::HasImperativeVerb,
QualitySignal::HasCausality,
],
computed_at: 1_710_520_800,
},
access_count: 0,
last_accessed: 0,
source: RecordSource::DeveloperManual,
confidence: ConfidenceScore::for_new_record(&RecordSource::DeveloperManual),
gap_analysis_score: 0.0,
payload: None,
}
}
fn sample_file_record() -> FileRecord {
FileRecord {
path: "src/store/db.rs".to_string(),
purpose: "Initialises SurrealKV trees and exposes the Store handle.".to_string(),
entry_points: vec!["Store::open".to_string()],
imports: vec!["surrealkv".to_string()],
gotcha_keys: vec!["gotcha:inference-async".to_string()],
decision_keys: vec![],
todos: vec![TodoComment {
text: "add fsync benchmark".to_string(),
line: 42,
kind: TodoKind::Todo,
}],
unsafe_count: 0,
unwrap_count: 1,
change_frequency: 12,
last_author: Some("ioni".to_string()),
is_hotspot: false,
token_cost_estimate: 180,
last_modified_session: 1_710_520_800,
content_hash: None,
line_count: 0,
blast_radius: None,
propagated_staleness: None,
}
}
fn sample_context_packet() -> ContextPacket {
ContextPacket {
stage: None,
critical_gotchas: vec![sample_record()],
file_records: vec![sample_file_record()],
related_decisions: vec![],
recent_session: Some(
"Implemented storage layer. SurrealKV tree opened cleanly.".to_string(),
),
token_estimate: 420,
stale_warnings: vec![],
unconfirmed_candidates: vec!["file:src/analysis/walker.rs".to_string()],
knowledge_gaps: vec![KnowledgeGap {
key: "file:src/analysis/parser.rs".to_string(),
gap_type: GapType::HotFileNoGotchas,
risk_score: 0.72,
description: "Hot file with 23 commits in 60d and no gotchas".to_string(),
action_hint: "mati gotcha add src/analysis/parser.rs".to_string(),
}],
compliance_rate: None,
injection_string: String::new(),
}
}
fn assert_serde_roundtrip<T>(value: &T)
where
T: Serialize + for<'de> Deserialize<'de>,
{
let json1 = serde_json::to_string(value).expect("serialization failed");
let restored: T = serde_json::from_str(&json1).expect("deserialization failed");
let json2 = serde_json::to_string(&restored).expect("re-serialization failed");
assert_eq!(json1, json2, "serde round-trip produced different JSON");
}
#[test]
fn record_serde_roundtrip() {
assert_serde_roundtrip(&sample_record());
}
#[test]
fn file_record_serde_roundtrip() {
assert_serde_roundtrip(&sample_file_record());
}
#[test]
fn file_record_backward_compat_no_blast_radius() {
let json = r#"{
"path": "src/main.rs",
"purpose": "Entry point",
"entry_points": ["main"],
"imports": [],
"gotcha_keys": [],
"decision_keys": [],
"todos": [],
"unsafe_count": 0,
"unwrap_count": 0,
"change_frequency": 5,
"last_author": "dev",
"is_hotspot": false,
"token_cost_estimate": 100,
"last_modified_session": 1710520800
}"#;
let fr: FileRecord = serde_json::from_str(json).unwrap();
assert!(fr.blast_radius.is_none());
assert_eq!(fr.path, "src/main.rs");
assert_eq!(fr.content_hash, None);
assert_eq!(fr.line_count, 0);
}
#[test]
fn gotcha_record_serde_roundtrip() {
let gotcha = GotchaRecord {
rule: "Never hold a write transaction across an await point.".to_string(),
reason: "SurrealKV write txns are not Send; the future will not compile.".to_string(),
severity: Priority::Critical,
affected_files: vec!["src/store/db.rs".to_string()],
ref_url: Some("https://github.com/example/issue/99".to_string()),
discovered_session: 1_710_520_800,
confirmed: true,
};
assert_serde_roundtrip(&gotcha);
}
#[test]
fn context_packet_serde_roundtrip() {
assert_serde_roundtrip(&sample_context_packet());
}
#[test]
fn record_lifecycle_tombstoned_serde() {
let lifecycle = RecordLifecycle::Tombstoned {
reason: TombstoneReason::FileDeleted,
at: 1_710_520_800,
};
assert_serde_roundtrip(&lifecycle);
}
#[test]
fn record_lifecycle_superseded_serde() {
let lifecycle = RecordLifecycle::Superseded {
by_key: "gotcha:inference-async-v2".to_string(),
};
assert_serde_roundtrip(&lifecycle);
}
#[test]
fn tombstone_reason_file_renamed_serde() {
let reason = TombstoneReason::FileRenamed {
new_path: "src/store/backend.rs".to_string(),
};
assert_serde_roundtrip(&reason);
}
#[test]
fn staleness_signal_dependency_bumped_serde() {
let signal = StalenessSignal::DependencyBumped {
dep: "tokio".to_string(),
old_ver: "1.40".to_string(),
new_ver: "1.50".to_string(),
};
assert_serde_roundtrip(&signal);
}
#[test]
fn staleness_signal_file_renamed_serde() {
let signal = StalenessSignal::FileRenamed {
new_path: "src/store/backend.rs".to_string(),
};
assert_serde_roundtrip(&signal);
}
#[test]
fn staleness_signal_cascade_serde() {
let signal = StalenessSignal::CascadeFromDecision("decision:storage-engine".to_string());
assert_serde_roundtrip(&signal);
}
#[test]
fn staleness_score_fresh_default() {
let s = StalenessScore::fresh();
assert_eq!(s.tier, StalenessTier::Fresh);
assert_eq!(s.value, 0.0);
assert!(s.signals.is_empty());
assert_eq!(s.computed_at, 0, "0 = not yet computed sentinel");
assert!(s.last_record_sha.is_empty());
}
#[test]
fn quality_tier_ranges() {
assert_eq!(QualityScore::tier_from_value(0.00), QualityTier::Suppressed);
assert_eq!(QualityScore::tier_from_value(0.10), QualityTier::Suppressed);
assert_eq!(QualityScore::tier_from_value(0.19), QualityTier::Suppressed);
assert_eq!(QualityScore::tier_from_value(0.20), QualityTier::Poor);
assert_eq!(QualityScore::tier_from_value(0.30), QualityTier::Poor);
assert_eq!(QualityScore::tier_from_value(0.39), QualityTier::Poor);
assert_eq!(QualityScore::tier_from_value(0.40), QualityTier::Acceptable);
assert_eq!(QualityScore::tier_from_value(0.55), QualityTier::Acceptable);
assert_eq!(QualityScore::tier_from_value(0.69), QualityTier::Acceptable);
assert_eq!(QualityScore::tier_from_value(0.70), QualityTier::Good);
assert_eq!(QualityScore::tier_from_value(0.80), QualityTier::Good);
assert_eq!(QualityScore::tier_from_value(0.89), QualityTier::Good);
assert_eq!(QualityScore::tier_from_value(0.90), QualityTier::Excellent);
assert_eq!(QualityScore::tier_from_value(0.95), QualityTier::Excellent);
assert_eq!(QualityScore::tier_from_value(1.00), QualityTier::Excellent);
}
#[test]
fn confidence_base_scores_by_source() {
assert_eq!(
ConfidenceScore::base_for_source(&RecordSource::DeveloperManual),
0.80
);
assert_eq!(
ConfidenceScore::base_for_source(&RecordSource::Import),
0.70
);
assert_eq!(
ConfidenceScore::base_for_source(&RecordSource::ClaudeEnrich),
0.60
);
assert_eq!(
ConfidenceScore::base_for_source(&RecordSource::SessionHook),
0.50
);
assert_eq!(
ConfidenceScore::base_for_source(&RecordSource::StaticAnalysis),
0.10
);
}
#[test]
fn confidence_for_new_record_value_matches_base() {
let source = RecordSource::ClaudeEnrich;
let score = ConfidenceScore::for_new_record(&source);
assert_eq!(score.value, ConfidenceScore::base_for_source(&source));
assert_eq!(score.confirmation_count, 0);
assert_eq!(score.contributor_count, 1);
assert!(score.last_challenged.is_none());
assert_eq!(score.challenge_count, 0);
}
#[test]
fn priority_total_ordering() {
assert!(Priority::Critical > Priority::High);
assert!(Priority::High > Priority::Normal);
assert!(Priority::Normal > Priority::Low);
assert!(Priority::Critical > Priority::Low);
assert_eq!(Priority::High, Priority::High);
}
#[test]
fn record_device_id_accessor_matches_version() {
let rec = sample_record();
assert_eq!(rec.device_id(), rec.version.device_id);
}
#[test]
fn quality_tier_non_finite_is_suppressed() {
assert_eq!(
QualityScore::tier_from_value(f32::NAN),
QualityTier::Suppressed
);
assert_eq!(
QualityScore::tier_from_value(f32::INFINITY),
QualityTier::Suppressed
);
assert_eq!(
QualityScore::tier_from_value(f32::NEG_INFINITY),
QualityTier::Suppressed
);
}
#[test]
fn quality_tier_out_of_range_finite_saturates() {
assert_eq!(QualityScore::tier_from_value(-1.0), QualityTier::Suppressed);
assert_eq!(
QualityScore::tier_from_value(-0.001),
QualityTier::Suppressed
);
assert_eq!(QualityScore::tier_from_value(1.001), QualityTier::Excellent);
assert_eq!(QualityScore::tier_from_value(100.0), QualityTier::Excellent);
}
#[test]
fn layer0_default_quality_is_suppressed_tier() {
let q = QualityScore::layer0_default();
assert_eq!(q.tier, QualityTier::Suppressed);
assert_eq!(q.value, 0.10);
assert!(q.signals.is_empty());
assert_eq!(q.computed_at, 0, "0 = not yet computed sentinel");
}
#[test]
fn confidence_for_new_record_all_sources_correct() {
let cases: &[(RecordSource, f32)] = &[
(RecordSource::DeveloperManual, 0.80),
(RecordSource::Import, 0.70),
(RecordSource::ClaudeEnrich, 0.60),
(RecordSource::SessionHook, 0.50),
(RecordSource::StaticAnalysis, 0.10),
];
for (source, expected) in cases {
let score = ConfidenceScore::for_new_record(source);
assert!(
(score.value - expected).abs() < f32::EPSILON,
"{source:?}: expected {expected}, got {}",
score.value
);
assert_eq!(score.confirmation_count, 0);
assert_eq!(score.contributor_count, 1);
assert!(score.last_challenged.is_none());
assert_eq!(score.challenge_count, 0);
}
}
#[test]
fn confidence_base_scores_are_all_distinct() {
let scores: Vec<f32> = [
RecordSource::DeveloperManual,
RecordSource::Import,
RecordSource::ClaudeEnrich,
RecordSource::SessionHook,
RecordSource::StaticAnalysis,
]
.iter()
.map(ConfidenceScore::base_for_source)
.collect();
for i in 0..scores.len() {
for j in (i + 1)..scores.len() {
assert!(
(scores[i] - scores[j]).abs() > f32::EPSILON,
"sources {i} and {j} have identical base score {}",
scores[i]
);
}
}
}
#[test]
fn priority_exhaustive_pairwise_ordering() {
use std::cmp::Ordering::*;
let pairs = [
(Priority::Low, Priority::Normal, Less),
(Priority::Low, Priority::High, Less),
(Priority::Low, Priority::Critical, Less),
(Priority::Normal, Priority::High, Less),
(Priority::Normal, Priority::Critical, Less),
(Priority::High, Priority::Critical, Less),
(Priority::Low, Priority::Low, Equal),
(Priority::Normal, Priority::Normal, Equal),
(Priority::High, Priority::High, Equal),
(Priority::Critical, Priority::Critical, Equal),
];
for (a, b, expected) in pairs {
assert_eq!(
a.cmp(&b),
expected,
"{a:?}.cmp({b:?}) should be {expected:?}"
);
if expected == Less {
assert_eq!(b.cmp(&a), std::cmp::Ordering::Greater, "{b:?}.cmp({a:?})");
}
}
}
#[test]
fn staleness_all_signal_variants_serde() {
let signals: Vec<StalenessSignal> = vec![
StalenessSignal::NotAccessedDays(30),
StalenessSignal::LinesChangedPct(0.75),
StalenessSignal::EntryPointsChanged(2),
StalenessSignal::ImportsChanged(5),
StalenessSignal::FileDeleted,
StalenessSignal::FileRenamed {
new_path: "src/foo.rs".to_string(),
},
StalenessSignal::DependencyBumped {
dep: "tokio".to_string(),
old_ver: "1.40".to_string(),
new_ver: "1.50".to_string(),
},
StalenessSignal::LinkedFileChanged {
path: "src/bar.rs".to_string(),
},
StalenessSignal::CascadeFromDecision("decision:arch".to_string()),
StalenessSignal::TodosChanged,
StalenessSignal::UnsafeCountChanged(3),
StalenessSignal::UnwrapCountChanged(-2),
StalenessSignal::GitCommitsSince(7),
];
for signal in &signals {
let json = serde_json::to_string(signal).expect("serialize");
let restored: StalenessSignal = serde_json::from_str(&json).expect("deserialize");
let json2 = serde_json::to_string(&restored).expect("re-serialize");
assert_eq!(json, json2, "roundtrip failed for: {json}");
}
}
#[test]
fn tombstone_reason_all_variants_serde() {
let reasons = vec![
TombstoneReason::FileDeleted,
TombstoneReason::FileRenamed {
new_path: "src/new.rs".to_string(),
},
TombstoneReason::ManualDeletion,
TombstoneReason::Superseded,
];
for reason in &reasons {
assert_serde_roundtrip(reason);
}
}
#[test]
fn category_serializes_as_snake_case() {
let cases = [
(Category::Gotcha, "\"gotcha\""),
(Category::File, "\"file\""),
(Category::Decision, "\"decision\""),
(Category::Stage, "\"stage\""),
(Category::Dependency, "\"dependency\""),
(Category::DevNote, "\"dev_note\""),
(Category::Session, "\"session\""),
(Category::Analytics, "\"analytics\""),
];
for (cat, expected_json) in cases {
let json = serde_json::to_string(&cat).unwrap();
assert_eq!(json, expected_json, "Category::{cat:?}");
}
}
#[test]
fn record_source_serializes_as_snake_case() {
let cases = [
(RecordSource::StaticAnalysis, "\"static_analysis\""),
(RecordSource::ClaudeEnrich, "\"claude_enrich\""),
(RecordSource::SessionHook, "\"session_hook\""),
(RecordSource::DeveloperManual, "\"developer_manual\""),
(RecordSource::Import, "\"import\""),
];
for (src, expected_json) in cases {
let json = serde_json::to_string(&src).unwrap();
assert_eq!(json, expected_json, "RecordSource::{src:?}");
}
}
#[test]
fn staleness_tier_serializes_as_snake_case() {
let cases = [
(StalenessTier::Fresh, "\"fresh\""),
(StalenessTier::Aging, "\"aging\""),
(StalenessTier::Stale, "\"stale\""),
(StalenessTier::Liability, "\"liability\""),
(StalenessTier::Tombstone, "\"tombstone\""),
];
for (tier, expected_json) in cases {
let json = serde_json::to_string(&tier).unwrap();
assert_eq!(json, expected_json, "StalenessTier::{tier:?}");
}
}
#[test]
fn gotcha_record_layer0_stub_is_unconfirmed() {
let stub = GotchaRecord {
rule: "Do not call .await inside rayon::spawn.".to_string(),
reason: "rayon threads have no tokio runtime.".to_string(),
severity: Priority::Critical,
affected_files: vec!["src/analysis/walker.rs".to_string()],
ref_url: None,
discovered_session: 0,
confirmed: false,
};
assert!(
!stub.confirmed,
"Layer 0 stubs must be unconfirmed on construction"
);
let json = serde_json::to_string(&stub).unwrap();
let restored: GotchaRecord = serde_json::from_str(&json).unwrap();
assert!(
!restored.confirmed,
"confirmed flag must survive serde roundtrip"
);
assert!(json.contains("\"confirmed\":false"), "wire format: {json}");
}
#[test]
fn gotcha_record_confirmed_true_roundtrips() {
let confirmed = GotchaRecord {
rule: "Use SurrealKV::with_versioning(true, 0) for indefinite retention.".to_string(),
reason: "0 means retain all versions forever, not disabled.".to_string(),
severity: Priority::High,
affected_files: vec!["src/store/db.rs".to_string()],
ref_url: Some("https://github.com/example/issue/5".to_string()),
discovered_session: 1_710_520_800,
confirmed: true,
};
assert_serde_roundtrip(&confirmed);
let json = serde_json::to_string(&confirmed).unwrap();
assert!(json.contains("\"confirmed\":true"));
}
#[test]
fn staleness_score_fully_populated_serde() {
let s = StalenessScore {
value: 0.87,
tier: StalenessTier::Liability,
signals: vec![
StalenessSignal::NotAccessedDays(90),
StalenessSignal::LinesChangedPct(0.6),
StalenessSignal::EntryPointsChanged(3),
StalenessSignal::FileRenamed {
new_path: "src/store/backend.rs".to_string(),
},
],
computed_at: 1_710_520_800,
last_record_sha: "deadbeefcafe0123".to_string(),
};
assert_serde_roundtrip(&s);
let json = serde_json::to_string(&s).unwrap();
let restored: StalenessScore = serde_json::from_str(&json).unwrap();
assert_eq!(restored.tier, StalenessTier::Liability);
assert_eq!(restored.signals.len(), 4);
assert_eq!(restored.last_record_sha, "deadbeefcafe0123");
}
#[test]
fn quality_score_with_all_positive_signals_serde() {
let q = QualityScore {
value: 0.92,
tier: QualityTier::Excellent,
signals: vec![
QualitySignal::HasImperativeVerb,
QualitySignal::HasCausality,
QualitySignal::HasSeveritySet,
QualitySignal::HasReference,
QualitySignal::RuleLengthAdequate,
QualitySignal::ReasonLengthAdequate,
QualitySignal::AffectedFilesSpecified,
QualitySignal::HasSpecificIdentifier,
],
computed_at: 1_710_520_800,
};
assert_serde_roundtrip(&q);
let json = serde_json::to_string(&q).unwrap();
let restored: QualityScore = serde_json::from_str(&json).unwrap();
assert_eq!(restored.tier, QualityTier::Excellent);
assert_eq!(restored.signals.len(), 8);
}
#[test]
fn confidence_score_with_challenge_history_serde() {
let c = ConfidenceScore {
value: 0.45,
confirmation_count: 1,
contributor_count: 3,
last_challenged: Some(1_710_500_000),
challenge_count: 2,
};
let json = serde_json::to_string(&c).unwrap();
let restored: ConfidenceScore = serde_json::from_str(&json).unwrap();
assert_eq!(restored.last_challenged, Some(1_710_500_000));
assert_eq!(restored.challenge_count, 2);
assert_eq!(restored.contributor_count, 3);
let json2 = serde_json::to_string(&restored).unwrap();
assert_eq!(json, json2);
}
#[test]
fn record_ref_url_none_does_not_become_some() {
let mut r = sample_record();
r.ref_url = None;
let json = serde_json::to_string(&r).unwrap();
let restored: Record = serde_json::from_str(&json).unwrap();
assert!(
restored.ref_url.is_none(),
"ref_url: None must not become Some after roundtrip"
);
assert!(
json.contains("\"ref_url\":null"),
"wire format must encode None as null"
);
}
#[test]
fn context_packet_zero_knowledge_case_serde() {
let empty = ContextPacket {
stage: None,
critical_gotchas: vec![],
file_records: vec![],
related_decisions: vec![],
recent_session: None,
token_estimate: 0,
stale_warnings: vec![],
unconfirmed_candidates: vec![],
knowledge_gaps: vec![],
compliance_rate: None,
injection_string: String::new(),
};
assert_serde_roundtrip(&empty);
let json = serde_json::to_string(&empty).unwrap();
let restored: ContextPacket = serde_json::from_str(&json).unwrap();
assert!(restored.critical_gotchas.is_empty());
assert!(restored.file_records.is_empty());
assert!(restored.stage.is_none());
assert_eq!(restored.token_estimate, 0);
}
#[test]
fn record_tags_empty_and_many_both_survive_serde() {
let mut r = sample_record();
r.tags = vec![];
let json_empty = serde_json::to_string(&r).unwrap();
let restored_empty: Record = serde_json::from_str(&json_empty).unwrap();
assert!(
restored_empty.tags.is_empty(),
"empty tags must remain empty"
);
r.tags = (0..50).map(|i| format!("tag-{i:03}")).collect();
let json_many = serde_json::to_string(&r).unwrap();
let restored_many: Record = serde_json::from_str(&json_many).unwrap();
assert_eq!(restored_many.tags.len(), 50);
assert_eq!(restored_many.tags[0], "tag-000");
assert_eq!(restored_many.tags[49], "tag-049");
}
#[test]
fn file_record_layer0_stub_serde() {
let stub = FileRecord::layer0_stub(
"src/analysis/walker.rs",
vec![],
vec!["ignore".to_string(), "rayon".to_string()],
vec![],
0,
3,
17,
None,
true,
0,
0,
);
assert_serde_roundtrip(&stub);
let json = serde_json::to_string(&stub).unwrap();
let restored: FileRecord = serde_json::from_str(&json).unwrap();
assert!(
restored.purpose.is_empty(),
"empty purpose must remain empty"
);
assert!(restored.entry_points.is_empty());
assert!(restored.last_author.is_none());
assert!(restored.is_hotspot);
assert_eq!(restored.unwrap_count, 3);
}
#[test]
fn layer0_file_record_builder_sets_suppressed_quality() {
let record =
Record::layer0_file_stub("file:src/analysis/walker.rs", device_id(), 7, 1_710_520_800);
assert_eq!(record.key, "file:src/analysis/walker.rs");
assert_eq!(record.category, Category::File);
assert!(record.value.is_empty());
assert_eq!(record.quality.value, 0.10);
assert_eq!(record.quality.tier, QualityTier::Suppressed);
assert_eq!(record.source, RecordSource::StaticAnalysis);
assert_eq!(record.confidence.value, 0.10);
assert_eq!(record.confidence.contributor_count, 1);
}
#[test]
fn stale_review_entry_serde_roundtrip() {
let entry = StaleReviewEntry {
key: "file:src/store/db.rs".to_string(),
staleness_value: 0.72,
tier: StalenessTier::Stale,
last_updated: 1_710_520_800,
signals: vec![
"not accessed for 45 days".to_string(),
"3 entry points changed".to_string(),
],
};
assert_serde_roundtrip(&entry);
}
#[test]
fn stale_review_payload_serde_roundtrip() {
let payload = StaleReviewPayload {
session_timestamp: 1_710_520_800,
entries: vec![
StaleReviewEntry {
key: "file:src/store/db.rs".to_string(),
staleness_value: 0.72,
tier: StalenessTier::Stale,
last_updated: 1_710_500_000,
signals: vec!["not accessed for 45 days".to_string()],
},
StaleReviewEntry {
key: "gotcha:inference-async".to_string(),
staleness_value: 0.85,
tier: StalenessTier::Liability,
last_updated: 1_710_400_000,
signals: vec![
"90 commits since last confirmation".to_string(),
"75% of lines changed".to_string(),
],
},
],
};
assert_serde_roundtrip(&payload);
let json = serde_json::to_string(&payload).unwrap();
let restored: StaleReviewPayload = serde_json::from_str(&json).unwrap();
assert_eq!(restored.entries.len(), 2);
assert_eq!(restored.session_timestamp, 1_710_520_800);
}
#[test]
fn stale_review_payload_empty_entries_serde() {
let payload = StaleReviewPayload {
session_timestamp: 1_710_520_800,
entries: vec![],
};
assert_serde_roundtrip(&payload);
let json = serde_json::to_string(&payload).unwrap();
let restored: StaleReviewPayload = serde_json::from_str(&json).unwrap();
assert!(restored.entries.is_empty());
}
#[test]
fn staleness_signal_git_commits_since_serde() {
let signal = StalenessSignal::GitCommitsSince(42);
assert_serde_roundtrip(&signal);
let json = serde_json::to_string(&signal).unwrap();
assert!(json.contains("git_commits_since"), "wire format: {json}");
}
#[test]
fn staleness_signal_git_commits_since_display() {
let signal = StalenessSignal::GitCommitsSince(7);
assert_eq!(signal.to_string(), "7 commits since last confirmation");
}
#[test]
fn staleness_signal_display_all_variants() {
let signals: Vec<StalenessSignal> = vec![
StalenessSignal::NotAccessedDays(30),
StalenessSignal::LinesChangedPct(0.75),
StalenessSignal::EntryPointsChanged(2),
StalenessSignal::ImportsChanged(5),
StalenessSignal::FileDeleted,
StalenessSignal::FileRenamed {
new_path: "src/foo.rs".to_string(),
},
StalenessSignal::DependencyBumped {
dep: "tokio".to_string(),
old_ver: "1.40".to_string(),
new_ver: "1.50".to_string(),
},
StalenessSignal::LinkedFileChanged {
path: "src/bar.rs".to_string(),
},
StalenessSignal::CascadeFromDecision("decision:arch".to_string()),
StalenessSignal::TodosChanged,
StalenessSignal::UnsafeCountChanged(3),
StalenessSignal::UnwrapCountChanged(-2),
StalenessSignal::GitCommitsSince(7),
];
for signal in &signals {
let display = signal.to_string();
assert!(
!display.is_empty(),
"Display for {signal:?} should not be empty"
);
}
}
}