#![forbid(unsafe_code)]
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub const PROTOCOL_VERSION: &str = "0.3.0";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Request {
pub protocol_version: String,
pub license: License,
pub project_root: String,
pub coverage_sources: Vec<CoverageSource>,
pub static_findings: StaticFindings,
#[serde(default)]
pub options: Options,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct License {
pub jwt: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "kebab-case")]
pub enum CoverageSource {
V8 {
path: String,
},
Istanbul {
path: String,
},
V8Dir {
path: String,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticFindings {
pub files: Vec<StaticFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticFile {
pub path: String,
pub functions: Vec<StaticFunction>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StaticFunction {
pub name: String,
pub start_line: u32,
pub end_line: u32,
pub cyclomatic: u32,
pub static_used: bool,
pub test_covered: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Options {
#[serde(default)]
pub include_hot_paths: bool,
#[serde(default)]
pub min_invocations_for_hot: Option<u64>,
#[serde(default)]
pub min_observation_volume: Option<u32>,
#[serde(default)]
pub low_traffic_threshold: Option<f64>,
#[serde(default)]
pub trace_count: Option<u64>,
#[serde(default)]
pub period_days: Option<u32>,
#[serde(default)]
pub deployments_seen: Option<u32>,
#[serde(default)]
pub window_seconds: Option<u64>,
#[serde(default)]
pub instances_observed: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub protocol_version: String,
pub verdict: ReportVerdict,
pub summary: Summary,
pub findings: Vec<Finding>,
#[serde(default)]
pub hot_paths: Vec<HotPath>,
#[serde(default)]
pub watermark: Option<Watermark>,
#[serde(default)]
pub errors: Vec<DiagnosticMessage>,
#[serde(default)]
pub warnings: Vec<DiagnosticMessage>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum ReportVerdict {
Clean,
HotPathChangesNeeded,
ColdCodeDetected,
LicenseExpiredGrace,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Summary {
pub functions_tracked: u64,
pub functions_hit: u64,
pub functions_unhit: u64,
pub functions_untracked: u64,
pub coverage_percent: f64,
pub trace_count: u64,
pub period_days: u32,
pub deployments_seen: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capture_quality: Option<CaptureQuality>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct CaptureQuality {
pub window_seconds: u64,
pub instances_observed: u32,
pub lazy_parse_warning: bool,
pub untracked_ratio_percent: f64,
}
impl CaptureQuality {
pub const LAZY_PARSE_THRESHOLD_PERCENT: f64 = 30.0;
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Finding {
pub id: String,
pub file: String,
pub function: String,
pub line: u32,
pub verdict: Verdict,
pub invocations: Option<u64>,
pub confidence: Confidence,
pub evidence: Evidence,
#[serde(default)]
pub actions: Vec<Action>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Verdict {
SafeToDelete,
ReviewRequired,
CoverageUnavailable,
LowTraffic,
Active,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Confidence {
VeryHigh,
High,
Medium,
Low,
None,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Evidence {
pub static_status: String,
pub test_coverage: String,
pub v8_tracking: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub untracked_reason: Option<String>,
pub observation_days: u32,
pub deployments_observed: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HotPath {
pub id: String,
pub file: String,
pub function: String,
pub line: u32,
pub invocations: u64,
pub percentile: u8,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Action {
pub kind: String,
pub description: String,
#[serde(default)]
pub auto_fixable: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Watermark {
TrialExpired,
LicenseExpiredGrace,
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticMessage {
pub code: String,
pub message: String,
}
#[must_use]
pub fn finding_id(file: &str, function: &str, line: u32) -> String {
format!("fallow:prod:{}", content_hash(file, function, line, "prod"))
}
#[must_use]
pub fn hot_path_id(file: &str, function: &str, line: u32) -> String {
format!("fallow:hot:{}", content_hash(file, function, line, "hot"))
}
fn content_hash(file: &str, function: &str, line: u32, kind: &str) -> String {
let mut hasher = Sha256::new();
hasher.update(file.as_bytes());
hasher.update(function.as_bytes());
hasher.update(line.to_string().as_bytes());
hasher.update(kind.as_bytes());
let digest = hasher.finalize();
hex_prefix(&digest)
}
fn hex_prefix(digest: &[u8]) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(8);
for &byte in digest.iter().take(4) {
out.push(char::from(HEX[usize::from(byte >> 4)]));
out.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
out
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Feature {
ProductionCoverage,
PortfolioDashboard,
McpCloudTools,
CrossRepoAggregation,
#[serde(other)]
Unknown,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn version_constant_is_v0_3() {
assert!(PROTOCOL_VERSION.starts_with("0.3."));
}
#[test]
fn unknown_report_verdict_round_trips() {
let json = r#""something-new""#;
let verdict: ReportVerdict = serde_json::from_str(json).unwrap();
assert!(matches!(verdict, ReportVerdict::Unknown));
}
#[test]
fn unknown_verdict_round_trips() {
let json = r#""future_state""#;
let verdict: Verdict = serde_json::from_str(json).unwrap();
assert!(matches!(verdict, Verdict::Unknown));
}
#[test]
fn unknown_confidence_round_trips() {
let json = r#""ultra_high""#;
let confidence: Confidence = serde_json::from_str(json).unwrap();
assert!(matches!(confidence, Confidence::Unknown));
}
#[test]
fn unknown_feature_round_trips() {
let json = r#""future_feature""#;
let feature: Feature = serde_json::from_str(json).unwrap();
assert!(matches!(feature, Feature::Unknown));
}
#[test]
fn unknown_watermark_round_trips() {
let json = r#""something-else""#;
let watermark: Watermark = serde_json::from_str(json).unwrap();
assert!(matches!(watermark, Watermark::Unknown));
}
#[test]
fn coverage_source_kebab_case() {
let json = r#"{"kind":"v8-dir","path":"/tmp/dumps"}"#;
let src: CoverageSource = serde_json::from_str(json).unwrap();
assert!(matches!(src, CoverageSource::V8Dir { .. }));
}
#[test]
fn response_allows_unknown_fields() {
let json = r#"{
"protocol_version": "0.2.0",
"verdict": "clean",
"summary": {
"functions_tracked": 0,
"functions_hit": 0,
"functions_unhit": 0,
"functions_untracked": 0,
"coverage_percent": 0.0,
"trace_count": 0,
"period_days": 0,
"deployments_seen": 0
},
"findings": [],
"future_top_level_field": 42
}"#;
let response: Response = serde_json::from_str(json).unwrap();
assert_eq!(response.protocol_version, "0.2.0");
}
#[test]
fn finding_id_is_deterministic() {
let first = finding_id("src/a.ts", "foo", 42);
let second = finding_id("src/a.ts", "foo", 42);
assert_eq!(first, second);
assert!(first.starts_with("fallow:prod:"));
assert_eq!(first.len(), "fallow:prod:".len() + 8);
}
#[test]
fn capture_quality_round_trips() {
let q = CaptureQuality {
window_seconds: 720,
instances_observed: 1,
lazy_parse_warning: true,
untracked_ratio_percent: 42.5,
};
let json = serde_json::to_string(&q).unwrap();
let parsed: CaptureQuality = serde_json::from_str(&json).unwrap();
assert_eq!(q, parsed);
}
#[test]
fn summary_without_capture_quality_deserializes() {
let json = r#"{
"functions_tracked": 10,
"functions_hit": 5,
"functions_unhit": 5,
"functions_untracked": 0,
"coverage_percent": 50.0,
"trace_count": 100,
"period_days": 1,
"deployments_seen": 1
}"#;
let summary: Summary = serde_json::from_str(json).unwrap();
assert!(summary.capture_quality.is_none());
}
#[test]
fn summary_with_capture_quality_round_trips() {
let summary = Summary {
functions_tracked: 10,
functions_hit: 5,
functions_unhit: 5,
functions_untracked: 3,
coverage_percent: 50.0,
trace_count: 100,
period_days: 1,
deployments_seen: 1,
capture_quality: Some(CaptureQuality {
window_seconds: 720,
instances_observed: 1,
lazy_parse_warning: true,
untracked_ratio_percent: 30.0,
}),
};
let json = serde_json::to_string(&summary).unwrap();
let parsed: Summary = serde_json::from_str(&json).unwrap();
assert_eq!(summary.capture_quality, parsed.capture_quality);
}
#[test]
fn lazy_parse_threshold_is_30_percent() {
assert!((CaptureQuality::LAZY_PARSE_THRESHOLD_PERCENT - 30.0).abs() < f64::EPSILON);
}
#[test]
fn hot_path_id_differs_from_finding_id() {
let f = finding_id("src/a.ts", "foo", 42);
let h = hot_path_id("src/a.ts", "foo", 42);
assert_ne!(f[f.len() - 8..], h[h.len() - 8..]);
}
#[test]
fn finding_id_changes_with_line() {
assert_ne!(
finding_id("src/a.ts", "foo", 10),
finding_id("src/a.ts", "foo", 11),
);
}
#[test]
fn finding_id_changes_with_file() {
assert_ne!(
finding_id("src/a.ts", "foo", 42),
finding_id("src/b.ts", "foo", 42),
);
}
#[test]
fn finding_id_changes_with_function() {
assert_ne!(
finding_id("src/a.ts", "foo", 42),
finding_id("src/a.ts", "bar", 42),
);
}
#[test]
fn finding_id_is_lowercase_hex_ascii() {
let id = finding_id("src/a.ts", "foo", 42);
let hash = &id["fallow:prod:".len()..];
assert!(
hash.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f')),
"expected lowercase hex, got {hash}"
);
}
#[test]
fn evidence_round_trips_with_untracked_reason() {
let evidence = Evidence {
static_status: "used".to_owned(),
test_coverage: "not_covered".to_owned(),
v8_tracking: "untracked".to_owned(),
untracked_reason: Some("lazy_parsed".to_owned()),
observation_days: 30,
deployments_observed: 14,
};
let json = serde_json::to_string(&evidence).unwrap();
assert!(json.contains("\"untracked_reason\":\"lazy_parsed\""));
let back: Evidence = serde_json::from_str(&json).unwrap();
assert_eq!(back.untracked_reason.as_deref(), Some("lazy_parsed"));
}
#[test]
fn static_function_requires_static_used_and_test_covered() {
let json = r#"{"name":"foo","start_line":1,"end_line":2,"cyclomatic":1}"#;
let result: Result<StaticFunction, _> = serde_json::from_str(json);
let err = result
.expect_err("missing static_used / test_covered must fail")
.to_string();
assert!(
err.contains("static_used") || err.contains("test_covered"),
"unexpected error text: {err}"
);
}
#[test]
fn options_defaults_when_fields_omitted() {
let json = "{}";
let options: Options = serde_json::from_str(json).unwrap();
assert!(!options.include_hot_paths);
assert!(options.min_invocations_for_hot.is_none());
assert!(options.min_observation_volume.is_none());
assert!(options.low_traffic_threshold.is_none());
assert!(options.trace_count.is_none());
assert!(options.period_days.is_none());
assert!(options.deployments_seen.is_none());
}
#[test]
fn evidence_omits_untracked_reason_when_none() {
let evidence = Evidence {
static_status: "unused".to_owned(),
test_coverage: "covered".to_owned(),
v8_tracking: "tracked".to_owned(),
untracked_reason: None,
observation_days: 30,
deployments_observed: 14,
};
let json = serde_json::to_string(&evidence).unwrap();
assert!(
!json.contains("untracked_reason"),
"expected untracked_reason omitted, got {json}"
);
}
}