///|
pub fn findings_with_mode(
findings : Array[Finding],
mode : ReplacementMode,
) -> Array[Finding] {
findings.map(fn(item) {
{ ..item, replacement: replacement_preview(item, mode) }
})
}
///|
pub fn apply_findings_with_mode(
input : String,
findings : Array[Finding],
mode : ReplacementMode,
) -> (String, Array[OffsetMap]) {
apply_findings(input, findings_with_mode(findings, mode))
}
///|
pub fn redact_only_kinds(
input : String,
kinds : Array[PhiKind],
mode : ReplacementMode,
) -> DeidResult raise DeidError {
let policy = with_policy_mode(
allow_only_kinds(RedactionPolicy::default(), kinds),
mode,
)
redact_with_policy(input, policy)
}
///|
pub fn redact_except_kinds(
input : String,
excluded : Array[PhiKind],
mode : ReplacementMode,
) -> DeidResult raise DeidError {
let findings = scan(input)
let selected = findings.filter(fn(item) { !excluded.contains(item.kind) })
let changed = findings_with_mode(selected, mode)
let (text, offsets) = apply_findings(input, changed)
{
text,
findings: changed,
offsets,
audit: build_audit(input, text, changed, offsets),
}
}
///|
pub fn redact_range(
input : String,
range : Span,
config : RedactionConfig,
) -> DeidResult raise DeidError {
let bounded = range.clamp(input.length())
let fragment = input[bounded.start:bounded.end].to_owned()
let fragment_result = redact_with_config(fragment, config)
let findings = shift_findings(fragment_result.findings, bounded.start)
let changed = findings.map(fn(item) {
{ ..item, replacement: item.replacement }
})
let (text, offsets) = apply_findings(input, changed)
{
text,
findings: changed,
offsets,
audit: build_audit(input, text, changed, offsets),
}
}
///|
pub fn redact_prefix(
input : String,
length : Int,
config : RedactionConfig,
) -> DeidResult raise DeidError {
redact_range(input, { start: 0, end: length }, config)
}
///|
pub fn redact_suffix(
input : String,
length : Int,
config : RedactionConfig,
) -> DeidResult raise DeidError {
let start = input.length() - length
redact_range(input, { start, end: input.length() }, config)
}
///|
pub fn redact_windowed(
input : String,
radius : Int,
config : RedactionConfig,
) -> DeidResult raise DeidError {
let initial = scan(input)
let windows = initial.map(fn(item) {
finding_span(item).shift(-radius).clamp(input.length())
})
let union = merge_spans(windows)
let candidates = []
for window in union {
let result = redact_range(input, window, config)
candidates.append(result.findings)
}
let final_findings = resolve_overlaps(candidates)
let (text, offsets) = apply_findings(input, final_findings)
{
text,
findings: final_findings,
offsets,
audit: build_audit(input, text, final_findings, offsets),
}
}
///|
pub fn preview_redaction(
input : String,
config : RedactionConfig,
) -> Array[Finding] raise DeidError {
let result = redact_with_config(input, {
..config,
policy: { ..config.policy, action: ReviewOnly },
})
result.findings
}
///|
pub fn redact_until_stable(
input : String,
config : RedactionConfig,
maximum_rounds : Int,
) -> DeidResult raise DeidError {
let mut current = redact_with_config(input, config)
let rounds = if maximum_rounds <= 0 { 1 } else { maximum_rounds }
for _ in 1.. DeidResult raise DeidError {
let policy = protected_spans.fold(init=config.policy, (current, item) => {
add_protected_range(current, item.start, item.end, "allowlist")
})
redact_with_config(input, { ..config, policy, })
}
///|
pub fn redact_with_deny_rules(
input : String,
denied_rule_ids : Array[String],
config : RedactionConfig,
) -> DeidResult raise DeidError {
let policy = deny_rules(config.policy, denied_rule_ids)
redact_with_config(input, { ..config, policy, })
}
///|
pub fn replacement_for_finding(
finding : Finding,
mode : ReplacementMode,
) -> String {
replacement_preview(finding, mode)
}
///|
pub fn redaction_changed_text(input : String, result : DeidResult) -> String {
let changed = StringBuilder()
let mut cursor = 0
for offset in result.offsets {
if offset.original_start > cursor {
changed.write_string(input[cursor:offset.original_start].to_owned())
}
cursor = offset.original_end
}
changed.to_string()
}
///|
pub fn redaction_is_idempotent(
input : String,
config : RedactionConfig,
) -> Bool raise DeidError {
let first = redact_with_config(input, config)
let second = redact_with_config(first.text, config)
first.text == second.text
}
///|
pub fn redaction_preserves_newlines(
input : String,
result : DeidResult,
) -> Bool {
count_char(input, '\n') == count_char(result.text, '\n')
}
///|
pub fn redaction_preserves_separators(
input : String,
result : DeidResult,
separators : String,
) -> Bool {
for c in separators {
if count_char(input, c) != count_char(result.text, c) {
return false
}
}
true
}
///|
pub fn redaction_summary(input : String, result : DeidResult) -> String {
[
audit_summary(result.audit),
"newlines_preserved=\{redaction_preserves_newlines(input, result)}",
"integrity=\{result_integrity_score(input, result)}",
"checksum=\{stable_result_checksum(result)}",
].join("\n")
}