///|
/// Human review lifecycle for findings that should not be silently discarded.
pub(all) enum ReviewStatus {
Queued
Assigned
InReview
Decided
Cancelled
} derive(Debug, Eq)
///|
pub(all) enum ReviewDecision {
ApproveRedaction
KeepValue
Escalate
Defer
NoDecision
} derive(Debug, Eq)
///|
pub(all) struct ReviewTask {
id : String
document_id : String
finding : Finding
risk : RiskLevel
priority : Int
status : ReviewStatus
assignee : String
decision : ReviewDecision
notes : Array[String]
created_sequence : Int
} derive(Debug, Eq)
///|
pub(all) struct ReviewPolicy {
min_confidence : Int
max_open_tasks : Int
escalate_risk : RiskLevel
assign_by_kind : Map[String, String]
auto_keep_below : Int
} derive(Debug, Eq)
///|
pub(all) struct ReviewQueue {
mut tasks : Array[ReviewTask]
mut sequence : Int
mut closed : Bool
policy : ReviewPolicy
} derive(Debug)
///|
pub fn review_status_name(status : ReviewStatus) -> String {
match status {
Queued => "queued"
Assigned => "assigned"
InReview => "in_review"
Decided => "decided"
Cancelled => "cancelled"
}
}
///|
pub fn review_decision_name(decision : ReviewDecision) -> String {
match decision {
ApproveRedaction => "approve_redaction"
KeepValue => "keep_value"
Escalate => "escalate"
Defer => "defer"
NoDecision => "no_decision"
}
}
///|
pub fn ReviewPolicy::default() -> ReviewPolicy {
{
min_confidence: 45,
max_open_tasks: 10000,
escalate_risk: Critical,
assign_by_kind: Map([]),
auto_keep_below: 20,
}
}
///|
pub fn ReviewPolicy::strict() -> ReviewPolicy {
{
..ReviewPolicy::default(),
min_confidence: 70,
max_open_tasks: 1000,
escalate_risk: High,
auto_keep_below: 0,
}
}
///|
pub fn review_policy_assign(
policy : ReviewPolicy,
kind : PhiKind,
reviewer : String,
) -> ReviewPolicy {
let assignments = policy.assign_by_kind.copy()
assignments[phi_kind_name(kind)] = reviewer
{ ..policy, assign_by_kind: assignments }
}
///|
pub fn review_policy_for_kind(policy : ReviewPolicy, kind : PhiKind) -> String? {
policy.assign_by_kind.get(phi_kind_name(kind))
}
///|
pub fn ReviewQueue::new(policy : ReviewPolicy) -> ReviewQueue {
{ tasks: [], sequence: 0, closed: false, policy }
}
///|
pub fn ReviewQueue::default() -> ReviewQueue {
ReviewQueue::new(ReviewPolicy::default())
}
///|
pub fn ReviewQueue::is_closed(self : ReviewQueue) -> Bool {
self.closed
}
///|
pub fn ReviewQueue::close(self : ReviewQueue) -> Unit {
self.closed = true
}
///|
pub fn ReviewQueue::open(self : ReviewQueue) -> Unit {
self.closed = false
}
///|
pub fn ReviewQueue::size(self : ReviewQueue) -> Int {
self.tasks.length()
}
///|
pub fn ReviewQueue::open_count(self : ReviewQueue) -> Int {
self.tasks
.filter(fn(task) { task.status != Decided && task.status != Cancelled })
.length()
}
///|
pub fn ReviewQueue::decided_count(self : ReviewQueue) -> Int {
self.tasks.filter(fn(task) { task.status == Decided }).length()
}
///|
fn review_priority(finding : Finding, policy : ReviewPolicy) -> Int {
let risk_score = match risk_level(finding) {
Low => 10
Medium => 30
High => 60
Critical => 90
}
let low_bonus = if finding.confidence < policy.min_confidence {
20
} else {
0
}
risk_score + (100 - finding.confidence) + low_bonus
}
///|
pub fn review_task(
document_id : String,
finding : Finding,
sequence : Int,
policy : ReviewPolicy,
) -> ReviewTask {
{
id: "review-\{document_id}-\{sequence}",
document_id,
finding,
risk: risk_level(finding),
priority: review_priority(finding, policy),
status: Queued,
assignee: "",
decision: NoDecision,
notes: [],
created_sequence: sequence,
}
}
///|
pub fn ReviewQueue::enqueue(
self : ReviewQueue,
document_id : String,
finding : Finding,
) -> ReviewTask? {
if self.closed || self.open_count() >= self.policy.max_open_tasks {
None
} else {
self.sequence += 1
let task = review_task(document_id, finding, self.sequence, self.policy)
self.tasks.push(task)
Some(task)
}
}
///|
pub fn ReviewQueue::enqueue_findings(
self : ReviewQueue,
document_id : String,
findings : Array[Finding],
) -> Array[ReviewTask] {
let tasks = []
for finding in findings {
match self.enqueue(document_id, finding) {
Some(task) => tasks.push(task)
None => ()
}
}
tasks
}
///|
pub fn ReviewQueue::enqueue_result(
self : ReviewQueue,
document_id : String,
result : DeidResult,
) -> Array[ReviewTask] {
self.enqueue_findings(document_id, result.findings)
}
///|
pub fn ReviewQueue::get(self : ReviewQueue, id : String) -> ReviewTask? {
let mut result : ReviewTask? = None
for task in self.tasks {
if task.id == id {
result = Some(task)
}
}
result
}
///|
pub fn ReviewQueue::update_task(
self : ReviewQueue,
id : String,
update : ReviewTask,
) -> Bool {
let mut updated = false
for i in 0.. Bool {
let mut updated = false
for i in 0.. Bool {
let mut updated = false
for i in 0.. Bool {
let mut updated = false
for i in 0.. Bool {
let mut updated = false
for i in 0.. Bool {
let mut updated = false
for i in 0.. Array[ReviewTask] {
self.tasks.filter(fn(task) {
task.status == Queued && task.assignee.is_empty()
})
}
///|
pub fn ReviewQueue::assigned_to(
self : ReviewQueue,
reviewer : String,
) -> Array[ReviewTask] {
self.tasks.filter(fn(task) { task.assignee == reviewer })
}
///|
pub fn ReviewQueue::by_status(
self : ReviewQueue,
status : ReviewStatus,
) -> Array[ReviewTask] {
self.tasks.filter(fn(task) { task.status == status })
}
///|
pub fn ReviewQueue::by_risk(
self : ReviewQueue,
risk : RiskLevel,
) -> Array[ReviewTask] {
self.tasks.filter(fn(task) { task.risk == risk })
}
///|
pub fn ReviewQueue::by_document(
self : ReviewQueue,
document_id : String,
) -> Array[ReviewTask] {
self.tasks.filter(fn(task) { task.document_id == document_id })
}
///|
pub fn ReviewQueue::top_priority(
self : ReviewQueue,
limit : Int,
) -> Array[ReviewTask] {
let tasks = self.tasks.filter(fn(task) {
task.status != Decided && task.status != Cancelled
})
tasks.sort_by(fn(left, right) { right.priority - left.priority })
if limit <= 0 || limit >= tasks.length() {
tasks
} else {
tasks[:limit].to_owned()
}
}
///|
pub fn ReviewQueue::auto_assign(self : ReviewQueue, reviewer : String) -> Int {
let mut assigned = 0
for i in 0.. Int {
let mut assigned = 0
for i in 0.. {
self.tasks[i] = { ..task, status: Assigned, assignee: reviewer }
assigned += 1
}
_ => ()
}
}
assigned
}
///|
pub fn ReviewQueue::auto_decide_low_confidence(self : ReviewQueue) -> Int {
let mut count = 0
for i in 0.. String {
[
"id=\{task.id}",
"document=\{task.document_id}",
"kind=\{phi_kind_name(task.finding.kind)}",
"risk=\{task.risk}",
"confidence=\{task.finding.confidence}",
"priority=\{task.priority}",
"status=\{review_status_name(task.status)}",
"assignee=\{task.assignee}",
"decision=\{review_decision_name(task.decision)}",
].join("\n")
}
///|
pub fn review_task_json(task : ReviewTask) -> String {
let notes = task.notes.map(json_escape).join(",")
"{" +
"\"id\":\{json_escape(task.id)}," +
"\"document_id\":\{json_escape(task.document_id)}," +
"\"kind\":\{json_escape(phi_kind_name(task.finding.kind))}," +
"\"risk\":\{json_escape("\{task.risk}")}," +
"\"priority\":\{task.priority}," +
"\"status\":\{json_escape(review_status_name(task.status))}," +
"\"assignee\":\{json_escape(task.assignee)}," +
"\"decision\":\{json_escape(review_decision_name(task.decision))}," +
"\"notes\":[" +
notes +
"]}"
}
///|
pub fn ReviewQueue::to_json(self : ReviewQueue) -> String {
"[" + self.tasks.map(review_task_json).join(",") + "]"
}
///|
pub fn ReviewQueue::to_csv(self : ReviewQueue) -> String {
let lines = [
"id,document_id,kind,risk,confidence,priority,status,assignee,decision",
]
for task in self.tasks {
lines.push(
[
csv_cell(task.id),
csv_cell(task.document_id),
csv_cell(phi_kind_name(task.finding.kind)),
csv_cell("\{task.risk}"),
"\{task.finding.confidence}",
"\{task.priority}",
review_status_name(task.status),
csv_cell(task.assignee),
review_decision_name(task.decision),
].join(","),
)
}
lines.join("\n")
}
///|
pub fn ReviewQueue::risk_counts(self : ReviewQueue) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for task in self.tasks {
let key = "\{task.risk}"
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn ReviewQueue::status_counts(self : ReviewQueue) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for task in self.tasks {
let key = review_status_name(task.status)
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn ReviewQueue::decision_counts(self : ReviewQueue) -> Map[String, Int] {
let counts : Map[String, Int] = Map([])
for task in self.tasks {
let key = review_decision_name(task.decision)
counts[key] = counts.get_or_default(key, 0) + 1
}
counts
}
///|
pub fn ReviewQueue::average_priority(self : ReviewQueue) -> Float {
if self.tasks.is_empty() {
0.0
} else {
Float::from_int(self.tasks.fold(init=0, (sum, task) => sum + task.priority)) /
Float::from_int(self.tasks.length())
}
}
///|
pub fn ReviewQueue::completion_ratio(self : ReviewQueue) -> Float {
if self.tasks.is_empty() {
1.0
} else {
Float::from_int(self.decided_count()) / Float::from_int(self.tasks.length())
}
}
///|
pub fn ReviewQueue::has_critical_open(self : ReviewQueue) -> Bool {
self.tasks.any(fn(task) {
task.risk == Critical && task.status != Decided && task.status != Cancelled
})
}
///|
pub fn ReviewQueue::is_ready(self : ReviewQueue) -> Bool {
!self.has_critical_open() && self.open_count() == 0
}
///|
pub fn ReviewQueue::checksum(self : ReviewQueue) -> String {
let values = self.tasks.map(fn(task) {
task.id +
":" +
review_status_name(task.status) +
":" +
review_decision_name(task.decision)
})
values.sort()
stable_hash(values.join("\n"))
}
///|
pub fn review_queue_summary(queue : ReviewQueue) -> String {
[
"tasks=\{queue.size()}",
"open=\{queue.open_count()}",
"decided=\{queue.decided_count()}",
"completion=\{queue.completion_ratio()}",
"critical_open=\{queue.has_critical_open()}",
"checksum=\{queue.checksum()}",
].join("\n")
}