///|
pub enum PipelineStage {
Normalize
Detect
Resolve
Apply
Audit
Complete
} derive(Debug, Eq)
///|
pub(all) struct PipelineConfig {
policy : RedactionPolicy
rules : Array[Rule]
normalize_input : Bool
include_keyword_diagnostics : Bool
keep_review_findings : Bool
} derive(Debug, Eq)
///|
pub(all) struct PipelineTrace {
stage : PipelineStage
message : String
} derive(Debug, Eq)
///|
pub(all) struct PipelineResult {
original : String
normalized : String
findings : Array[Finding]
result : DeidResult
statistics : ScanStatistics
diagnostics : Array[Diagnostic]
trace : Array[PipelineTrace]
} derive(Debug)
///|
pub fn PipelineConfig::default() -> PipelineConfig {
{
policy: RedactionPolicy::default(),
rules: comprehensive_rules(),
normalize_input: true,
include_keyword_diagnostics: true,
keep_review_findings: true,
}
}
///|
pub fn PipelineResult::text(self : PipelineResult) -> String {
self.result.text
}
///|
pub fn PipelineResult::audit(self : PipelineResult) -> AuditReport {
self.result.audit
}
///|
pub fn pipeline_stage_name(stage : PipelineStage) -> String {
match stage {
Normalize => "normalize"
Detect => "detect"
Resolve => "resolve"
Apply => "apply"
Audit => "audit"
Complete => "complete"
}
}
///|
pub fn pipeline_trace_text(trace : Array[PipelineTrace]) -> String {
trace
.map(fn(item) { "[\{pipeline_stage_name(item.stage)}] \{item.message}" })
.join("\n")
}
///|
fn pipeline_input(input : String, config : PipelineConfig) -> String {
if config.normalize_input {
sanitize_input(input)
} else {
input
}
}
///|
fn pipeline_review_diagnostics(
input : String,
findings : Array[Finding],
config : PipelineConfig,
) -> Array[Diagnostic] {
let diagnostics = diagnose_findings(input, findings)
if config.include_keyword_diagnostics && has_sensitive_keyword(input) {
diagnostics.push({
code: "KEYWORD_CONTEXT",
severity: Info,
message: "sensitive field labels were found; verify contextual recall",
start: 0,
end: input.length(),
})
}
diagnostics
}
///|
pub fn run_pipeline(
input : String,
config : PipelineConfig,
) -> PipelineResult raise DeidError {
let trace = [{ stage: Normalize, message: "input normalization selected" }]
let normalized = pipeline_input(input, config)
trace.push({
stage: Detect,
message: "scanning \{config.rules.length()} rules",
})
let candidates = scan(normalized, rules=config.rules)
trace.push({
stage: Resolve,
message: "resolved \{candidates.length()} findings",
})
let selected = policy_filter_findings(candidates, config.policy)
let result = redact_with_config(
normalized,
config_to_pipeline(config),
rules=config.rules,
)
trace.push({
stage: Apply,
message: "applied \{result.offsets.length()} replacements",
})
let diagnostics = pipeline_review_diagnostics(normalized, selected, config)
trace.push({ stage: Audit, message: "audit generated" })
trace.push({ stage: Complete, message: "pipeline complete" })
{
original: input,
normalized,
findings: if config.keep_review_findings {
selected
} else {
result.findings
},
result,
statistics: scan_statistics(normalized, config.rules, selected),
diagnostics,
trace,
}
}
///|
fn config_to_pipeline(config : PipelineConfig) -> RedactionConfig {
{ ..RedactionConfig::default(), policy: config.policy }
}
///|
pub fn pipeline_with_policy(
input : String,
policy : RedactionPolicy,
) -> PipelineResult raise DeidError {
run_pipeline(input, { ..PipelineConfig::default(), policy, })
}
///|
pub fn pipeline_findings(result : PipelineResult) -> Array[Finding] {
result.findings
}
///|
pub fn pipeline_has_errors(result : PipelineResult) -> Bool {
diagnostics_have_errors(result.diagnostics)
}
///|
pub fn pipeline_is_safe(result : PipelineResult) -> Bool {
!pipeline_has_errors(result) &&
output_contract_passes(result.normalized, result.result)
}
///|
pub fn pipeline_summary(result : PipelineResult) -> String {
[
"normalized_length=\{result.normalized.length()}",
"findings=\{result.findings.length()}",
"applied=\{result.result.offsets.length()}",
"diagnostics=\{result.diagnostics.length()}",
"safe=\{pipeline_is_safe(result)}",
].join("\n")
}
///|
pub fn pipeline_to_json(result : PipelineResult) -> String {
"{" +
"\"original_length\":\{result.original.length()}," +
"\"normalized_length\":\{result.normalized.length()}," +
"\"result\":\{result_to_json(result.result)}," +
"\"statistics\":{" +
"\"rule_count\":\{result.statistics.rule_count}," +
"\"candidate_count\":\{result.statistics.candidate_count}," +
"\"critical_count\":\{result.statistics.critical_count}" +
"}," +
"\"diagnostics\":\{diagnostics_to_json(result.diagnostics)}" +
"}"
}
///|
pub fn pipeline_for_profile(
input : String,
profile : Profile,
mode : ReplacementMode,
) -> PipelineResult raise DeidError {
let pack = select_pack(profile)
let policy = with_policy_mode(RedactionPolicy::default(), mode)
run_pipeline(input, { ..PipelineConfig::default(), policy, rules: pack.rules })
}
///|
pub fn pipeline_replay(
input : String,
result : PipelineResult,
) -> Bool raise DeidError {
let replay = run_pipeline(input, {
..PipelineConfig::default(),
rules: comprehensive_rules(),
})
replay.result.text == result.result.text
}
///|
pub fn pipeline_trace_checksum(result : PipelineResult) -> String {
stable_hash(pipeline_trace_text(result.trace))
}