use serde::Serialize;
use crate::compliance::checks::{LocalFinding, Severity};
const SCHEMA_VERSION: u32 = 2;
const SCORED_RAILS: &[(&str, f64)] = &[
("compliance", 0.25),
("security", 0.25),
("governance", 0.15),
];
const PENDING_RAILS: &[(&str, f64, &str)] = &[
(
"scalability",
0.15,
"Load characteristics and data growth are not visible in source.",
),
(
"observability",
0.2,
"Instrumentation wiring is visible, but whether it covers what matters is a judgement.",
),
];
struct Criterion {
id: &'static str,
rail: &'static str,
label: &'static str,
}
const CRITERIA: &[Criterion] = &[
Criterion {
id: "cmp-encryption-at-rest",
rail: "compliance",
label: "Sensitive fields are encrypted at rest",
},
Criterion {
id: "cmp-data-classification",
rail: "compliance",
label: "Sensitive data is identified and handled according to its sensitivity",
},
Criterion {
id: "sec-tenant-isolation",
rail: "security",
label: "One customer cannot see or change another customer\u{2019}s data",
},
Criterion {
id: "gov-data-retention",
rail: "governance",
label: "How long data is kept, and how it is deleted, is defined",
},
Criterion {
id: "gdpr-erasure",
rail: "governance",
label: "Users can request data export and erasure",
},
];
#[derive(PartialEq)]
enum OnFinding {
Fail,
Review,
}
fn criterion_for_check(check: &str) -> Option<(&'static str, OnFinding)> {
Some(match check {
"tenant-isolation-wiring" | "tenant-context-half-wired" => {
("sec-tenant-isolation", OnFinding::Fail)
}
"encryptor-registration" | "tenant-em-wiring" | "better-auth-encryption-context" => {
("cmp-encryption-at-rest", OnFinding::Fail)
}
"retention-wiring" => ("gov-data-retention", OnFinding::Fail),
"erasure-wiring" => ("gdpr-erasure", OnFinding::Fail),
"possible-misclassification" => ("cmp-data-classification", OnFinding::Review),
_ => return None,
})
}
fn criterion(id: &str) -> Option<&'static Criterion> {
CRITERIA.iter().find(|c| c.id == id)
}
fn rail_for_check(check: &str) -> &'static str {
criterion_for_check(check)
.and_then(|(id, _)| criterion(id))
.map(|c| c.rail)
.unwrap_or("compliance")
}
fn severity_for(finding: &LocalFinding) -> &'static str {
match (&finding.severity, finding.check.as_str()) {
(Severity::Warning, "tenant-context-half-wired") => "critical",
(Severity::Warning, _) => "high",
(Severity::Info, _) => "info",
}
}
fn penalty_for(severity: &str) -> f64 {
match severity {
"critical" => 30.0,
"high" => 15.0,
"medium" => 8.0,
"low" => 4.0,
_ => 0.0,
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CardFinding {
pub(crate) severity: String,
pub(crate) title: String,
pub(crate) detail: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) fix: Option<String>,
pub(crate) source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) criterion: Option<String>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CardItemCheck {
pub(crate) id: String,
pub(crate) status: String,
pub(crate) detail: String,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CardItem {
pub(crate) label: String,
pub(crate) status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) criterion: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub(crate) checks: Vec<CardItemCheck>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct CardDimension {
pub(crate) score: u32,
pub(crate) summary: String,
pub(crate) items: Vec<CardItem>,
pub(crate) questions: Vec<serde_json::Value>,
pub(crate) findings: Vec<CardFinding>,
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) pending: Option<bool>,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct ReportCard {
pub(crate) schema_version: u32,
pub(crate) overall: u32,
pub(crate) headline: String,
pub(crate) caveat: String,
pub(crate) phase: String,
pub(crate) step: String,
pub(crate) frameworks: Vec<String>,
pub(crate) dimensions: std::collections::BTreeMap<String, CardDimension>,
pub(crate) generated_at: String,
}
fn item_label(check: &str) -> &'static str {
match check {
"encryptor-registration" => "Field encryptor is registered",
"retention-wiring" => "Retention policies are wired",
"erasure-wiring" => "GDPR erasure is wired",
"possible-misclassification" => "Sensitive fields are classified",
"better-auth-encryption-context" => "Better Auth reads bind an encryption context",
"tenant-isolation-wiring" => "Tenant isolation filter is installed",
"tenant-em-wiring" => "An encryption tenant is bound",
"tenant-context-half-wired" => "Tenant filter and encryption context agree",
_ => "Deterministic check",
}
}
fn remedy(check: &str) -> Option<String> {
let text = match check {
"tenant-context-half-wired" => {
"Replace the hand-rolled fork with \
wrapEmWithTenantContext(Orm.em.fork(opts), context?.tenantId). setFilterParams \
scopes rows only; it does not bind the encryption key."
}
"tenant-em-wiring" | "tenant-isolation-wiring" => {
"Bind the tenant on the EntityManager before reading encrypted columns."
}
"encryptor-registration" => "Register a FieldEncryptor during bootstrap.",
"retention-wiring" => "Declare retention on classified entities.",
"erasure-wiring" => "Wire the erasure handler so subject-deletion requests are honoured.",
"possible-misclassification" => {
"Classify the field with .compliance('pii'|'phi'|'pci') or confirm 'none' is correct."
}
"better-auth-encryption-context" => {
"Wrap Better Auth's EntityManager so its reads carry the tenant."
}
_ => return None,
};
Some(text.to_string())
}
pub(crate) fn build_local_report_card(
app_name: &str,
module_count: usize,
findings: &[LocalFinding],
generated_at: String,
) -> ReportCard {
let mut dimensions = std::collections::BTreeMap::new();
let mut weighted_total = 0.0;
let mut weight_used = 0.0;
for (rail, weight) in SCORED_RAILS {
let rail_findings: Vec<&LocalFinding> = findings
.iter()
.filter(|f| rail_for_check(&f.check) == *rail)
.collect();
let mut score = 100.0;
let mut card_findings = Vec::new();
for finding in &rail_findings {
let severity = severity_for(finding);
score -= penalty_for(severity);
card_findings.push(CardFinding {
severity: severity.to_string(),
title: format!("{} ({})", item_label(&finding.check), finding.project),
detail: finding.message.clone(),
fix: remedy(&finding.check),
source: "cli".to_string(),
criterion: criterion_for_check(&finding.check).map(|(id, _)| id.to_string()),
});
}
let score = score.max(0.0);
let mut items: Vec<CardItem> = Vec::new();
for crit in CRITERIA.iter().filter(|c| c.rail == *rail) {
let mut checks = Vec::new();
let mut failed = false;
let mut review = false;
let mut detail = None;
for check in checks_for_criterion(crit.id) {
let hits: Vec<&&LocalFinding> =
rail_findings.iter().filter(|f| f.check == *check).collect();
let status = match hits.first() {
None => "pass",
Some(first) => {
detail.get_or_insert_with(|| first.message.clone());
match criterion_for_check(check).map(|(_, on)| on) {
Some(OnFinding::Review) => {
review = true;
"review"
}
_ => {
failed = true;
"fail"
}
}
}
};
checks.push(CardItemCheck {
id: (*check).to_string(),
status: status.to_string(),
detail: match hits.first() {
None => "No problems found by this check.".to_string(),
Some(first) => format!(
"{} finding{}: {}{}",
hits.len(),
if hits.len() > 1 { "s" } else { "" },
first.message,
if hits.len() > 1 {
format!(" (and {} more)", hits.len() - 1)
} else {
String::new()
}
),
},
});
}
items.push(CardItem {
label: crit.label.to_string(),
status: if failed {
"unmet"
} else if review {
"pending"
} else {
"met"
}
.to_string(),
detail,
criterion: Some(crit.id.to_string()),
checks,
});
}
let summary = if card_findings.is_empty() {
format!("No deterministic {rail} findings across {module_count} module(s).")
} else {
format!(
"{} deterministic finding(s) across {} module(s).",
card_findings.len(),
module_count
)
};
dimensions.insert(
(*rail).to_string(),
CardDimension {
score: score.round() as u32,
summary,
items,
questions: Vec::new(),
findings: card_findings,
pending: None,
},
);
weighted_total += score * weight;
weight_used += weight;
}
for (rail, _weight, why) in PENDING_RAILS {
dimensions.insert(
(*rail).to_string(),
CardDimension {
score: 0,
summary: format!("Not assessed. {why}"),
items: Vec::new(),
questions: Vec::new(),
findings: Vec::new(),
pending: Some(true),
},
);
}
let overall = if weight_used > 0.0 {
(weighted_total / weight_used).round() as u32
} else {
0
};
let total = findings.len();
let headline = if total == 0 {
format!(
"{app_name}: no deterministic compliance or security findings across {module_count} module(s)."
)
} else {
format!(
"{app_name}: {total} deterministic finding(s) across {module_count} module(s), scored on compliance, security and governance."
)
};
ReportCard {
schema_version: SCHEMA_VERSION,
overall,
headline,
caveat: "Deterministic checks only. Compliance, security and governance are scored \
from static analysis of the checklist items those checks cover; scalability \
and observability need judgement a source read cannot supply and are left \
unassessed rather than scored zero. The overall score is weighted across the \
scored rails only. All five rails need an agent's judgement, which the studio \
surface provides."
.to_string(),
phase: "audit".to_string(),
step: "audit".to_string(),
frameworks: Vec::new(),
dimensions,
generated_at,
}
}
const ALL_CHECKS: &[&str] = &[
"encryptor-registration",
"tenant-em-wiring",
"better-auth-encryption-context",
"possible-misclassification",
"tenant-isolation-wiring",
"tenant-context-half-wired",
"retention-wiring",
"erasure-wiring",
];
fn checks_for_criterion(id: &str) -> Vec<&'static str> {
ALL_CHECKS
.iter()
.copied()
.filter(|check| criterion_for_check(check).is_some_and(|(c, _)| c == id))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn finding(check: &str, severity: Severity) -> LocalFinding {
LocalFinding {
severity,
project: "iam".to_string(),
check: check.to_string(),
subject: "registrations.ts".to_string(),
message: "example".to_string(),
}
}
fn at() -> String {
"2026-01-01T00:00:00Z".to_string()
}
#[test]
fn a_clean_workspace_scores_full_marks_on_the_rails_it_can_judge() {
let card = build_local_report_card("demo", 3, &[], at());
assert_eq!(card.overall, 100);
assert_eq!(card.dimensions["compliance"].score, 100);
assert_eq!(card.dimensions["security"].score, 100);
assert_eq!(card.dimensions["governance"].score, 100);
assert_eq!(card.dimensions["governance"].pending, None);
}
#[test]
fn items_use_platform_criterion_ids_and_exact_labels() {
let card = build_local_report_card("demo", 1, &[], at());
let expected = [
(
"compliance",
"cmp-encryption-at-rest",
"Sensitive fields are encrypted at rest",
),
(
"compliance",
"cmp-data-classification",
"Sensitive data is identified and handled according to its sensitivity",
),
(
"security",
"sec-tenant-isolation",
"One customer cannot see or change another customer\u{2019}s data",
),
(
"governance",
"gov-data-retention",
"How long data is kept, and how it is deleted, is defined",
),
(
"governance",
"gdpr-erasure",
"Users can request data export and erasure",
),
];
let total: usize = card.dimensions.values().map(|d| d.items.len()).sum();
assert_eq!(total, expected.len());
for (rail, id, label) in expected {
let item = card.dimensions[rail]
.items
.iter()
.find(|i| i.criterion.as_deref() == Some(id))
.unwrap_or_else(|| panic!("{rail} has no {id} item"));
assert_eq!(item.label, label);
}
let json = serde_json::to_value(&card).unwrap();
assert_eq!(
json["dimensions"]["security"]["items"][0]["criterion"],
"sec-tenant-isolation"
);
assert!(
json["dimensions"]["security"]["items"][0]["label"]
.as_str()
.unwrap()
.contains('\u{2019}'),
"the label must keep the typographic apostrophe"
);
}
#[test]
fn every_local_check_maps_to_its_platform_criterion() {
for (check, id) in [
("tenant-isolation-wiring", "sec-tenant-isolation"),
("tenant-context-half-wired", "sec-tenant-isolation"),
("encryptor-registration", "cmp-encryption-at-rest"),
("tenant-em-wiring", "cmp-encryption-at-rest"),
("better-auth-encryption-context", "cmp-encryption-at-rest"),
("retention-wiring", "gov-data-retention"),
("erasure-wiring", "gdpr-erasure"),
("possible-misclassification", "cmp-data-classification"),
] {
assert_eq!(
criterion_for_check(check).map(|(c, _)| c),
Some(id),
"{check}"
);
assert!(
ALL_CHECKS.contains(&check),
"{check} missing from ALL_CHECKS"
);
}
}
#[test]
fn a_failed_check_marks_its_criterion_unmet_and_names_itself() {
let card = build_local_report_card(
"demo",
1,
&[finding("tenant-em-wiring", Severity::Warning)],
at(),
);
let item = card.dimensions["compliance"]
.items
.iter()
.find(|i| i.criterion.as_deref() == Some("cmp-encryption-at-rest"))
.unwrap();
assert_eq!(item.status, "unmet");
let em = item
.checks
.iter()
.find(|c| c.id == "tenant-em-wiring")
.unwrap();
assert_eq!(em.status, "fail");
assert!(
item.checks
.iter()
.filter(|c| c.id != "tenant-em-wiring")
.all(|c| c.status == "pass")
);
assert_eq!(
card.dimensions["compliance"].findings[0]
.criterion
.as_deref(),
Some("cmp-encryption-at-rest")
);
}
#[test]
fn possible_misclassification_is_a_review_not_a_failure() {
let card = build_local_report_card(
"demo",
1,
&[finding("possible-misclassification", Severity::Info)],
at(),
);
let item = card.dimensions["compliance"]
.items
.iter()
.find(|i| i.criterion.as_deref() == Some("cmp-data-classification"))
.unwrap();
assert_eq!(item.status, "pending");
assert_eq!(item.checks[0].status, "review");
}
#[test]
fn unassessed_rails_are_pending_not_zero_scores() {
let card = build_local_report_card("demo", 1, &[], at());
for rail in ["scalability", "observability"] {
assert_eq!(card.dimensions[rail].pending, Some(true), "{rail}");
}
assert_eq!(
card.overall, 100,
"unassessed rails must not drag the average down"
);
}
#[test]
fn the_silent_failure_check_outweighs_the_others() {
let half_wired = build_local_report_card(
"demo",
1,
&[finding("tenant-context-half-wired", Severity::Warning)],
at(),
);
let ordinary = build_local_report_card(
"demo",
1,
&[finding("tenant-isolation-wiring", Severity::Warning)],
at(),
);
assert!(
half_wired.dimensions["security"].score < ordinary.dimensions["security"].score,
"the check whose failure mode is silent must cost more"
);
assert_eq!(
half_wired.dimensions["security"].findings[0].severity,
"critical"
);
}
#[test]
fn findings_route_to_the_right_rail() {
let card = build_local_report_card(
"demo",
1,
&[
finding("tenant-isolation-wiring", Severity::Warning),
finding("tenant-em-wiring", Severity::Warning),
finding("retention-wiring", Severity::Warning),
],
at(),
);
assert_eq!(card.dimensions["security"].findings.len(), 1);
assert_eq!(card.dimensions["compliance"].findings.len(), 1);
assert_eq!(card.dimensions["governance"].findings.len(), 1);
}
#[test]
fn a_rail_never_scores_below_zero() {
let many: Vec<LocalFinding> = (0..20)
.map(|_| finding("tenant-context-half-wired", Severity::Warning))
.collect();
let card = build_local_report_card("demo", 1, &many, at());
assert_eq!(card.dimensions["security"].score, 0);
}
#[test]
fn info_findings_are_reported_without_costing_points() {
let card = build_local_report_card(
"demo",
1,
&[finding("possible-misclassification", Severity::Info)],
at(),
);
assert_eq!(card.dimensions["compliance"].score, 100);
assert_eq!(card.dimensions["compliance"].findings.len(), 1);
assert_eq!(card.dimensions["compliance"].findings[0].severity, "info");
}
#[test]
fn passing_checks_are_listed_as_met_so_the_checklist_shows_work_done() {
let card = build_local_report_card("demo", 1, &[], at());
let security = &card.dimensions["security"];
assert!(!security.items.is_empty());
assert!(security.items.iter().all(|i| i.status == "met"));
}
#[test]
fn every_finding_carries_a_remedy_and_is_marked_deterministic() {
let card = build_local_report_card(
"demo",
1,
&[finding("tenant-context-half-wired", Severity::Warning)],
at(),
);
let f = &card.dimensions["security"].findings[0];
assert_eq!(f.source, "cli");
assert!(
f.fix
.as_ref()
.is_some_and(|s| s.contains("wrapEmWithTenantContext"))
);
}
#[test]
fn the_card_declares_the_contract_version_and_audit_phase() {
let card = build_local_report_card("demo", 1, &[], at());
assert_eq!(card.schema_version, SCHEMA_VERSION);
assert_eq!(card.phase, "audit");
assert!(
card.caveat.contains("unassessed"),
"the caveat must say some rails were not scored: {}",
card.caveat
);
assert!(
card.caveat.contains("studio"),
"the caveat must say where the remaining rails come from: {}",
card.caveat
);
assert!(
!card.caveat.contains("--upload"),
"the caveat must not name a flag `score` does not accept"
);
}
}
pub(crate) fn iso8601_now() -> String {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let days = (secs / 86_400) as i64;
let rem = secs % 86_400;
let z = days + 719_468;
let era = z.div_euclid(146_097);
let doe = z.rem_euclid(146_097);
let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
y,
m,
d,
rem / 3_600,
(rem % 3_600) / 60,
rem % 60
)
}