///|
pub(all) struct TextDifference {
start : Int
end : Int
original : String
replacement : String
} derive(Debug, Eq)
///|
pub(all) struct RedactionContract {
valid_findings : Bool
monotonic_offsets : Bool
newline_preserved : Bool
output_changed : Bool
audit_consistent : Bool
passes : Bool
} derive(Debug, Eq)
///|
pub fn result_contract(
input : String,
result : DeidResult,
) -> RedactionContract {
let valid_findings = validate_findings(input, result.findings).is_empty()
let monotonic_offsets = offsets_are_monotonic(result.offsets)
let newline_preserved = redaction_preserves_newlines(input, result)
let output_changed = input != result.text || result.findings.is_empty()
let audit_consistent = result.audit.finding_count == result.findings.length() &&
result.audit.applied_count == result.offsets.length()
{
valid_findings,
monotonic_offsets,
newline_preserved,
output_changed,
audit_consistent,
passes: valid_findings &&
monotonic_offsets &&
newline_preserved &&
audit_consistent,
}
}
///|
pub fn contract_to_text(contract : RedactionContract) -> String {
[
"valid_findings=\{contract.valid_findings}",
"monotonic_offsets=\{contract.monotonic_offsets}",
"newline_preserved=\{contract.newline_preserved}",
"output_changed=\{contract.output_changed}",
"audit_consistent=\{contract.audit_consistent}",
"passes=\{contract.passes}",
].join("\n")
}
///|
pub fn contract_to_json(contract : RedactionContract) -> String {
"{" +
"\"valid_findings\":\{contract.valid_findings}," +
"\"monotonic_offsets\":\{contract.monotonic_offsets}," +
"\"newline_preserved\":\{contract.newline_preserved}," +
"\"output_changed\":\{contract.output_changed}," +
"\"audit_consistent\":\{contract.audit_consistent}," +
"\"passes\":\{contract.passes}" +
"}"
}
///|
pub fn result_differences(
input : String,
result : DeidResult,
) -> Array[TextDifference] {
result.offsets.map(fn(offset) {
{
start: offset.original_start,
end: offset.original_end,
original: input[offset.original_start:offset.original_end].to_owned(),
replacement: result.text[offset.replacement_start:offset.replacement_end].to_owned(),
}
})
}
///|
pub fn differences_to_markdown(differences : Array[TextDifference]) -> String {
let lines = ["| Span | Original | Replacement |", "| ---: | --- | --- |"]
for difference in differences {
lines.push(
"| \{difference.start}..\{difference.end} | \{quote_for_log(difference.original)} | \{quote_for_log(difference.replacement)} |",
)
}
lines.join("\n")
}
///|
pub fn differences_to_json(differences : Array[TextDifference]) -> String {
"[" +
differences
.map(fn(item) {
"{" +
"\"start\":\{item.start}," +
"\"end\":\{item.end}," +
"\"original\":\{json_string(item.original)}," +
"\"replacement\":\{json_string(item.replacement)}" +
"}"
})
.join(",") +
"]"
}
///|
pub fn result_change_count(result : DeidResult) -> Int {
result.offsets.length()
}
///|
pub fn result_original_coverage(result : DeidResult) -> Int {
audit_offset_coverage(result.audit)
}
///|
pub fn result_output_coverage(result : DeidResult) -> Int {
result.offsets.fold(init=0, (sum, item) => {
sum + item.replacement_end - item.replacement_start
})
}
///|
pub fn result_delta(result : DeidResult) -> Int {
result_output_coverage(result) - result_original_coverage(result)
}
///|
pub fn result_replacement_kinds(result : DeidResult) -> Array[String] {
let result_names = []
for item in result.findings {
let name = phi_kind_name(item.kind)
if !result_names.contains(name) {
result_names.push(name)
}
}
result_names
}
///|
pub fn result_contract_report(input : String, result : DeidResult) -> String {
let contract = result_contract(input, result)
let kinds = result_replacement_kinds(result).join(",")
[
contract_to_text(contract),
"changes=\{result_change_count(result)}",
"original_coverage=\{result_original_coverage(result)}",
"output_coverage=\{result_output_coverage(result)}",
"delta=\{result_delta(result)}",
"kinds=\{kinds}",
].join("\n")
}
///|
pub fn result_matches_text(result : DeidResult, expected : String) -> Bool {
result.text == expected
}
///|
pub fn result_has_replacement(
result : DeidResult,
replacement : String,
) -> Bool {
result.findings.any(fn(item) { item.replacement == replacement })
}
///|
pub fn result_has_kind(result : DeidResult, kind : PhiKind) -> Bool {
result.findings.any(fn(item) { item.kind == kind })
}
///|
pub fn result_findings_between(
result : DeidResult,
start : Int,
end : Int,
) -> Array[Finding] {
findings_overlapping_span(result.findings, { start, end })
}
///|
pub fn result_text_window(
result : DeidResult,
center : Int,
radius : Int,
) -> TextWindow {
text_window(result.text, center, radius)
}
///|
pub fn result_mapping_description(result : DeidResult) -> String {
mapping_from_result(result).describe()
}
///|
pub fn result_has_stable_mapping(result : DeidResult) -> Bool {
mapping_from_result(result).is_monotonic()
}
///|
pub fn result_replay_checksum(result : DeidResult) -> String {
stable_hash(result.text + audit_to_json(result.audit))
}
///|
pub fn empty_result(input : String) -> DeidResult {
{
text: input,
findings: [],
offsets: [],
audit: {
input_length: input.length(),
output_length: input.length(),
finding_count: 0,
applied_count: 0,
counts: Map([]),
findings: [],
offsets: [],
},
}
}