///|
pub(all) enum DiagnosticSeverity {
Info
Warning
Error
} derive(Debug, Eq)
///|
pub(all) struct Diagnostic {
code : String
severity : DiagnosticSeverity
message : String
start : Int
end : Int
} derive(Debug, Eq)
///|
pub fn severity_name(severity : DiagnosticSeverity) -> String {
match severity {
Info => "info"
Warning => "warning"
Error => "error"
}
}
///|
pub fn diagnose_rule(rule : Rule) -> Array[Diagnostic] {
let result = []
for issue in validate_rule(rule) {
result.push({
code: "RULE",
severity: Error,
message: issue.message,
start: 0,
end: 0,
})
}
if rule.contextual &&
!rule.pattern.contains(":") &&
!rule.pattern.contains(":") {
result.push({
code: "RULE_CONTEXT",
severity: Warning,
message: "contextual rule has no explicit field separator",
start: 0,
end: 0,
})
}
if rule.confidence < 60 {
result.push({
code: "RULE_CONFIDENCE",
severity: Info,
message: "rule is intentionally below the default confidence floor",
start: 0,
end: 0,
})
}
result
}
///|
pub fn diagnose_rules(rules : Array[Rule]) -> Array[Diagnostic] {
let result = []
for rule in rules {
result.append(diagnose_rule(rule))
}
result
}
///|
pub fn diagnose_policy(
policy : RedactionPolicy,
input_length : Int,
) -> Array[Diagnostic] {
let result = []
if policy.min_confidence < 0 || policy.min_confidence > 100 {
result.push({
code: "POLICY_CONFIDENCE",
severity: Error,
message: "confidence must be 0..100",
start: 0,
end: 0,
})
}
if policy.context_window < 0 {
result.push({
code: "POLICY_CONTEXT",
severity: Error,
message: "context window cannot be negative",
start: 0,
end: 0,
})
}
for item in policy.protected_ranges {
if !item.as_span().is_valid(input_length) {
result.push({
code: "POLICY_PROTECTED",
severity: Error,
message: item.reason,
start: item.start,
end: item.end,
})
}
}
result
}
///|
pub fn diagnose_findings(
input : String,
findings : Array[Finding],
) -> Array[Diagnostic] {
let result = []
for issue in validate_findings(input, findings) {
result.push({
code: "FINDING",
severity: Error,
message: issue,
start: 0,
end: 0,
})
}
for pair in overlap_pairs(findings) {
result.push({
code: "OVERLAP",
severity: Warning,
message: pair.0 + " overlaps " + pair.1,
start: 0,
end: 0,
})
}
result
}
///|
pub fn diagnostic_count_by_severity(
items : Array[Diagnostic],
) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in items {
let key = severity_name(item.severity)
result[key] = result.get_or_default(key, 0) + 1
}
result
}
///|
pub fn diagnostics_have_errors(items : Array[Diagnostic]) -> Bool {
items.any(fn(item) { item.severity == Error })
}
///|
pub fn diagnostics_to_text(items : Array[Diagnostic]) -> String {
if items.is_empty() {
"No diagnostics."
} else {
items
.map(fn(item) {
"[\{severity_name(item.severity)}] \{item.code}: \{item.message} (\{item.start}..\{item.end})"
})
.join("\n")
}
}
///|
pub fn diagnostics_to_json(items : Array[Diagnostic]) -> String {
let body = items
.map(fn(item) {
"{" +
"\"code\":\{json_string(item.code)}," +
"\"severity\":\{json_string(severity_name(item.severity))}," +
"\"message\":\{json_string(item.message)}," +
"\"start\":\{item.start},\"end\":\{item.end}" +
"}"
})
.join(",")
"[" + body + "]"
}
///|
pub fn explain_risk(kind : PhiKind) -> String {
match kind {
IdNumber => "Direct government identifier; use a strict policy."
MedicalRecord => "Patient-linked medical identifier; keep audit evidence."
Insurance => "Coverage or account identifier; review downstream exports."
PersonName => "Direct personal name; contextual rules may be conservative."
Phone => "Contact number; preserve separators only when required."
Email => "Electronic contact address; verify domain-like patterns."
Address => "Location data; institution-specific rules may be needed."
Date => "Date may be quasi-identifying; choose policy by use case."
Organization => "Organization field; review whether it is identifying."
Custom(name) => "Custom category: \{name}."
}
}
///|
pub fn rule_recommendation(rule : Rule) -> String {
if rule.confidence >= 95 {
"safe-default"
} else if rule.confidence >= 80 {
"review-context"
} else {
"opt-in"
}
}
///|
pub fn rule_recommendations(rules : Array[Rule]) -> Map[String, String] {
let result : Map[String, String] = Map([])
for rule in rules {
result[rule.id] = rule_recommendation(rule)
}
result
}
///|
pub fn document_diagnostics(
input : String,
policy : RedactionPolicy,
rules : Array[Rule],
) -> Array[Diagnostic] raise DeidError {
let result = diagnose_policy(policy, input.length())
result.append(diagnose_rules(rules))
let findings = scan(input, rules~)
result.append(diagnose_findings(input, findings))
result
}
///|
pub fn diagnostics_summary(items : Array[Diagnostic]) -> String {
let counts = diagnostic_count_by_severity(items)
[
"total=\{items.length()}",
"errors=\{counts.get_or_default("error", 0)}",
"warnings=\{counts.get_or_default("warning", 0)}",
"info=\{counts.get_or_default("info", 0)}",
].join(" ")
}