///|
/// A release manifest records deterministic package-level evidence for a
/// de-identification export without retaining the source document.
pub(all) struct ManifestCheck {
id : String
passed : Bool
value : String
severity : DiagnosticSeverity
} derive(Debug, Eq)
///|
pub(all) struct ReleaseManifest {
package_name : String
package_version : String
mut document_count : Int
mut source_checksum : String
mut output_checksum : String
policy_checksum : String
checks : Array[ManifestCheck]
mut generated_at : String
} derive(Debug, Eq)
///|
pub(all) struct ReleaseDecision {
approved : Bool
reasons : Array[String]
manifest_checksum : String
} derive(Debug, Eq)
///|
pub fn manifest_check(
id : String,
passed : Bool,
value : String,
severity : DiagnosticSeverity,
) -> ManifestCheck {
{ id, passed, value, severity }
}
///|
pub fn ReleaseManifest::new(
package_name : String,
package_version : String,
policy_checksum : String,
) -> ReleaseManifest {
{
package_name,
package_version,
document_count: 0,
source_checksum: "",
output_checksum: "",
policy_checksum,
checks: [],
generated_at: "",
}
}
///|
pub fn ReleaseManifest::add_check(
self : ReleaseManifest,
check : ManifestCheck,
) -> Unit {
self.checks.push(check)
}
///|
pub fn ReleaseManifest::set_counts(
self : ReleaseManifest,
document_count : Int,
source_checksum : String,
output_checksum : String,
) -> Unit {
self.document_count = document_count
self.source_checksum = source_checksum
self.output_checksum = output_checksum
}
///|
pub fn ReleaseManifest::set_generated_at(
self : ReleaseManifest,
value : String,
) -> Unit {
self.generated_at = value
}
///|
pub fn ReleaseManifest::passed(self : ReleaseManifest) -> Bool {
!self.checks.any(fn(check) { !check.passed && check.severity == Error })
}
///|
pub fn ReleaseManifest::failed_checks(
self : ReleaseManifest,
) -> Array[ManifestCheck] {
self.checks.filter(fn(check) { !check.passed })
}
///|
pub fn ReleaseManifest::error_count(self : ReleaseManifest) -> Int {
self.failed_checks().filter(fn(check) { check.severity == Error }).length()
}
///|
pub fn ReleaseManifest::warning_count(self : ReleaseManifest) -> Int {
self.failed_checks().filter(fn(check) { check.severity == Warning }).length()
}
///|
pub fn ReleaseManifest::checksum(self : ReleaseManifest) -> String {
stable_hash(
self.package_name +
":" +
self.package_version +
":" +
self.source_checksum +
":" +
self.output_checksum +
":" +
self.policy_checksum +
":" +
self.checks
.map(fn(check) { check.id + ":\{check.passed}:" + check.value })
.join("\n"),
)
}
///|
pub fn ReleaseManifest::summary(self : ReleaseManifest) -> String {
[
"package=\{self.package_name}",
"version=\{self.package_version}",
"documents=\{self.document_count}",
"checks=\{self.checks.length()}",
"errors=\{self.error_count()}",
"warnings=\{self.warning_count()}",
"passed=\{self.passed()}",
"checksum=\{self.checksum()}",
].join("\n")
}
///|
pub fn manifest_check_json(check : ManifestCheck) -> String {
"{" +
"\"id\":\{json_escape(check.id)}," +
"\"passed\":\{check.passed}," +
"\"value\":\{json_escape(check.value)}," +
"\"severity\":\{json_escape(severity_name(check.severity))}" +
"}"
}
///|
pub fn ReleaseManifest::to_json(self : ReleaseManifest) -> String {
"{" +
"\"package_name\":\{json_escape(self.package_name)}," +
"\"package_version\":\{json_escape(self.package_version)}," +
"\"document_count\":\{self.document_count}," +
"\"source_checksum\":\{json_escape(self.source_checksum)}," +
"\"output_checksum\":\{json_escape(self.output_checksum)}," +
"\"policy_checksum\":\{json_escape(self.policy_checksum)}," +
"\"generated_at\":\{json_escape(self.generated_at)}," +
"\"manifest_checksum\":\{json_escape(self.checksum())}," +
"\"checks\":[" +
self.checks.map(manifest_check_json).join(",") +
"]}"
}
///|
pub fn ReleaseManifest::to_markdown(self : ReleaseManifest) -> String {
let lines = [
"# Release manifest", "", "| Check | Status | Severity | Value |", "| --- | --- | --- | --- |",
]
for check in self.checks {
lines.push(
"| \{check.id} | \{check.passed} | \{severity_name(check.severity)} | \{check.value} |",
)
}
lines.push("")
lines.push("passed=\{self.passed()}")
lines.push("checksum=\{self.checksum()}")
lines.join("\n")
}
///|
pub fn release_decision(manifest : ReleaseManifest) -> ReleaseDecision {
let reasons = manifest
.failed_checks()
.map(fn(check) { check.id + ": " + check.value })
{
approved: manifest.passed() && manifest.document_count >= 0,
reasons,
manifest_checksum: manifest.checksum(),
}
}
///|
pub fn release_decision_summary(decision : ReleaseDecision) -> String {
let reasons = decision.reasons.join(",")
[
"approved=\{decision.approved}",
"reasons=\{reasons}",
"manifest_checksum=\{decision.manifest_checksum}",
].join("\n")
}
///|
pub fn release_manifest_for_result(
package_name : String,
package_version : String,
input : String,
result : DeidResult,
policy : RedactionPolicy,
) -> ReleaseManifest {
let manifest = ReleaseManifest::new(
package_name,
package_version,
stable_hash(policy_summary(policy)),
)
manifest.set_counts(1, stable_hash(input), stable_hash(result.text))
manifest.add_check(
manifest_check(
"output_contract",
output_contract_passes(input, result),
"output contract",
Error,
),
)
manifest.add_check(
manifest_check(
"finding_spans",
quality_finding_spans_valid(input, result.findings),
"finding spans",
Error,
),
)
manifest.add_check(
manifest_check(
"audit_counts",
result.audit.finding_count == result.findings.length(),
"audit count matches",
Error,
),
)
manifest.add_check(
manifest_check(
"no_original_phi",
quality_no_original_phi(result.text, result.findings),
"no original selected values",
Error,
),
)
manifest
}
///|
pub fn release_manifest_for_batch(
package_name : String,
package_version : String,
items : Array[BatchItem],
outcome : BatchOutcome,
policy : RedactionPolicy,
) -> ReleaseManifest {
let manifest = ReleaseManifest::new(
package_name,
package_version,
stable_hash(policy_summary(policy)),
)
let input_checksum = stable_hash(
items.map(fn(item) { item.id + ":" + item.text }).join("\n"),
)
let output_checksum = stable_hash(
outcome.results
.map(fn(item) { item.id + ":" + item.result.text })
.join("\n"),
)
manifest.set_counts(items.length(), input_checksum, output_checksum)
manifest.add_check(
manifest_check(
"batch_consistency",
batch_consistency_issues(outcome).is_empty(),
"batch totals",
Error,
),
)
manifest.add_check(
manifest_check(
"batch_success_ratio",
batch_success_ratio(outcome) >= 1.0,
"all documents processed",
Error,
),
)
manifest
}
///|
pub fn release_manifest_check_ids(manifest : ReleaseManifest) -> Array[String] {
manifest.checks.map(fn(check) { check.id })
}
///|
pub fn release_manifest_duplicate_checks(
manifest : ReleaseManifest,
) -> Array[String] {
let counts : Map[String, Int] = Map([])
for check in manifest.checks {
counts[check.id] = counts.get_or_default(check.id, 0) + 1
}
let result = []
for id, count in counts {
if count > 1 {
result.push(id)
}
}
result
}
///|
pub fn release_manifest_is_well_formed(manifest : ReleaseManifest) -> Bool {
manifest.package_name.trim().length() > 0 &&
manifest.package_version.trim().length() > 0 &&
manifest.document_count >= 0 &&
release_manifest_duplicate_checks(manifest).is_empty()
}
///|
pub fn release_manifest_ready(manifest : ReleaseManifest) -> Bool {
release_manifest_is_well_formed(manifest) &&
manifest.passed() &&
manifest.error_count() == 0
}
///|
pub fn release_manifest_checksum_stable(manifest : ReleaseManifest) -> Bool {
manifest.checksum() == manifest.checksum()
}
///|
pub fn release_manifest_merge(
left : ReleaseManifest,
right : ReleaseManifest,
) -> ReleaseManifest {
let result = ReleaseManifest::new(
left.package_name,
left.package_version,
left.policy_checksum,
)
result.set_counts(
left.document_count + right.document_count,
stable_hash(left.source_checksum + right.source_checksum),
stable_hash(left.output_checksum + right.output_checksum),
)
for check in left.checks {
result.add_check(check)
}
for check in right.checks {
result.add_check(check)
}
result
}