///|
pub(all) struct BatchConfig {
config : RedactionConfig
stop_on_error : Bool
retain_empty : Bool
max_documents : Int
} derive(Debug, Eq)
///|
pub fn BatchConfig::default() -> BatchConfig {
{
config: RedactionConfig::default(),
stop_on_error: true,
retain_empty: true,
max_documents: 0,
}
}
///|
pub(all) struct BatchDiagnostic {
id : String
ok : Bool
message : String
finding_count : Int
} derive(Debug, Eq)
///|
pub(all) struct BatchOutcome {
results : Array[BatchResult]
diagnostics : Array[BatchDiagnostic]
totals : AuditTotals
} derive(Debug)
///|
pub fn batch_total_findings(results : Array[BatchResult]) -> Int {
results.fold(init=0, (total, item) => total + item.result.findings.length())
}
///|
pub fn batch_total_applied(results : Array[BatchResult]) -> Int {
results.fold(init=0, (total, item) => total + item.result.offsets.length())
}
///|
pub fn batch_kind_counts(results : Array[BatchResult]) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for item in results {
merge_counts(counts, item.result.audit.counts)
}
counts
}
///|
pub fn batch_risk_counts(results : Array[BatchResult]) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for item in results {
for finding in item.result.findings {
let key = "\{risk_level(finding)}"
counts[key] = counts.get_or_default(key, 0) + 1
}
}
counts
}
///|
pub fn batch_totals(results : Array[BatchResult]) -> AuditTotals {
let by_kind = batch_kind_counts(results)
let by_risk = batch_risk_counts(results)
let mut protected_count = 0
for item in results {
protected_count += item.result.audit.finding_count -
item.result.audit.applied_count
}
{
documents: results.length(),
findings: batch_total_findings(results),
applied: batch_total_applied(results),
protected_count,
by_kind,
by_risk,
}
}
///|
pub fn redact_batch_with_config(
items : Array[BatchItem],
batch_config : BatchConfig,
rules? : Array[Rule] = comprehensive_rules(),
) -> BatchOutcome raise DeidError {
let results = []
let diagnostics = []
let limit = if batch_config.max_documents <= 0 ||
batch_config.max_documents > items.length() {
items.length()
} else {
batch_config.max_documents
}
for i in 0.. {
diagnostics.push({
id: item.id,
ok: false,
message: "\{err}",
finding_count: 0,
})
if batch_config.stop_on_error {
raise err
}
}
}
}
}
{ results, diagnostics, totals: batch_totals(results) }
}
///|
pub fn batch_failed_diagnostics(
outcome : BatchOutcome,
) -> Array[BatchDiagnostic] {
outcome.diagnostics.filter(fn(item) { !item.ok })
}
///|
pub fn batch_success_ratio(outcome : BatchOutcome) -> Float {
if outcome.diagnostics.is_empty() {
1.0
} else {
let passed = outcome.diagnostics.filter(fn(item) { item.ok }).length()
Float::from_int(passed) / Float::from_int(outcome.diagnostics.length())
}
}
///|
pub fn batch_contains_critical(outcome : BatchOutcome) -> Bool {
outcome.results.any(fn(item) { audit_has_critical(item.result.audit) })
}
///|
pub fn batch_ids(results : Array[BatchResult]) -> Array[String] {
results.map(fn(item) { item.id })
}
///|
pub fn batch_findings(results : Array[BatchResult]) -> Array[Finding] {
let findings = []
for item in results {
findings.append(item.result.findings)
}
findings
}
///|
pub fn batch_audit_json(outcome : BatchOutcome) -> String {
let items = outcome.results.map(fn(item) {
"{\"id\":\{json_string(item.id)},\"result\":\{result_to_json(item.result)}}"
})
"{" +
"\"documents\":\{outcome.totals.documents}," +
"\"findings\":\{outcome.totals.findings}," +
"\"applied\":\{outcome.totals.applied}," +
"\"results\":[" +
items.join(",") +
"]," +
"\"diagnostics\":[" +
outcome.diagnostics
.map(fn(item) {
"{\"id\":\{json_string(item.id)},\"ok\":\{item.ok},\"message\":\{json_string(item.message)}}"
})
.join(",") +
"]" +
"}"
}
///|
pub fn batch_diagnostics_csv(outcome : BatchOutcome) -> String {
let lines = ["id,ok,message,finding_count"]
for item in outcome.diagnostics {
lines.push(
[
csv_cell(item.id),
"\{item.ok}",
csv_cell(item.message),
"\{item.finding_count}",
].join(","),
)
}
lines.join("\n")
}
///|
pub fn deduplicate_batch(items : Array[BatchItem]) -> Array[BatchItem] {
let result = []
let seen : Map[String, Unit] = Map([])
for item in items {
let key = stable_hash(item.text)
if !seen.contains(key) {
seen[key] = ()
result.push(item)
}
}
result
}
///|
pub fn batch_partition(
items : Array[BatchItem],
size : Int,
) -> Array[Array[BatchItem]] {
if size <= 0 {
[items.copy()]
} else {
let groups = []
let mut current = []
for item in items {
current.push(item)
if current.length() == size {
groups.push(current)
current = []
}
}
if !current.is_empty() {
groups.push(current)
}
groups
}
}
///|
pub fn merge_audit_totals(
left : AuditTotals,
right : AuditTotals,
) -> AuditTotals {
let by_kind = left.by_kind.copy()
merge_counts(by_kind, right.by_kind)
let by_risk = left.by_risk.copy()
merge_counts(by_risk, right.by_risk)
{
documents: left.documents + right.documents,
findings: left.findings + right.findings,
applied: left.applied + right.applied,
protected_count: left.protected_count + right.protected_count,
by_kind,
by_risk,
}
}
///|
pub fn merge_batch_outcomes(
left : BatchOutcome,
right : BatchOutcome,
) -> BatchOutcome {
let results = left.results.copy()
results.append(right.results)
let diagnostics = left.diagnostics.copy()
diagnostics.append(right.diagnostics)
{
results,
diagnostics,
totals: merge_audit_totals(left.totals, right.totals),
}
}
///|
pub fn batch_consistency_issues(outcome : BatchOutcome) -> Array[String] {
let issues = []
if outcome.totals.documents != outcome.results.length() {
issues.push("document total does not match result count")
}
if outcome.totals.findings != batch_total_findings(outcome.results) {
issues.push("finding total does not match result count")
}
if outcome.totals.applied != batch_total_applied(outcome.results) {
issues.push("applied total does not match result count")
}
issues
}