///|
pub(all) struct TaskChange {
pid : Int32
start_ticks : String
change : String
before : String
after : String
} derive(ToJson)
///|
pub(all) struct TaskComparison {
root_pid : Int32
root_start_ticks : String
evidence_complete : Bool
before_waiters : Int32
after_waiters : Int32
resolved_lock_waits : Int32
new_lock_waits : Int32
changes : Array[TaskChange]
issues : Array[String]
} derive(ToJson)
///|
pub extend TaskChange with ToJson::{to_json}
///|
pub extend TaskComparison with ToJson::{to_json}
///|
/// Compare the final observations of the same task in two saved sessions.
/// Missing identities are observations, never proof of successful completion.
pub fn TaskCapture::compare(
self : TaskCapture,
after : TaskCapture,
) -> TaskComparison raise LockError {
if self.frames.is_empty() || after.frames.is_empty() {
invalid(0, "comparison requires two nonempty task captures")
}
if self.boot_id != after.boot_id ||
self.pid_namespace != after.pid_namespace ||
self.root_pid != after.root_pid ||
self.start_ticks != after.start_ticks {
invalid(
0, "comparison requires the same boot, procfs scope and root PID/starttime",
)
}
let before = self.report().frames.last().unwrap()
let latest = after.report().frames.last().unwrap()
let complete = before.root_state == "present" &&
latest.root_state == "present" &&
before.issues.is_empty() &&
latest.issues.is_empty()
let issues = before.issues.map(s => "Before: " + s)
for issue in latest.issues {
issues.push("After: " + issue)
}
if before.root_state != "present" || latest.root_state != "present" {
issues.push(
"Root observations are incomplete; disappearance does not prove completion.",
)
}
let prior : Map[String, TaskProcess] = Map([])
let current : Map[String, Bool] = Map([])
for p in before.processes {
prior[process_identity(p.info)] = p
}
let changes : Array[TaskChange] = []
let mut resolved = 0
let mut new_waits = 0
for p in latest.processes {
let id = process_identity(p.info)
current[id] = true
let old = prior.get(id)
let change = match old {
None => "newly_observed"
Some(old) =>
if old.finding != p.finding {
"finding_changed"
} else if blocker_signature(old) != blocker_signature(p) {
"blocker_evidence_changed"
} else if old.info.state != p.info.state ||
old.info.wchan != p.info.wchan ||
old.info.wchan_status != p.info.wchan_status {
"wait_hint_changed"
} else {
""
}
}
if complete && old is Some(old) {
if old.finding == "confirmed_file_lock_wait" &&
p.finding != "confirmed_file_lock_wait" {
resolved += 1
}
if old.finding != "confirmed_file_lock_wait" &&
p.finding == "confirmed_file_lock_wait" {
new_waits += 1
}
}
if change != "" {
changes.push({
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change,
before: old.map(v => v.finding).unwrap_or("not_observed"),
after: p.finding,
})
}
}
for p in before.processes {
if !current.contains(process_identity(p.info)) {
changes.push({
pid: p.info.pid,
start_ticks: p.info.start_ticks,
change: "not_observed",
before: p.finding,
after: "not_observed",
})
}
}
{
root_pid: self.root_pid,
root_start_ticks: self.start_ticks,
evidence_complete: complete,
before_waiters: before.processes
.filter(p => p.finding == "confirmed_file_lock_wait")
.length(),
after_waiters: latest.processes
.filter(p => p.finding == "confirmed_file_lock_wait")
.length(),
resolved_lock_waits: resolved,
new_lock_waits: new_waits,
changes,
issues,
}
}
///|
pub fn TaskComparison::to_markdown(self : TaskComparison) -> String {
let mut out = "# Task observation comparison\n\nRoot PID \{self.root_pid}/\{self.root_start_ticks}. Final observed waiters: \{self.before_waiters} -> \{self.after_waiters}.\n\nSame-identity lock waits no longer observed: \{self.resolved_lock_waits}; newly observed waits in previously seen processes: \{self.new_lock_waits}. Evidence complete: \{self.evidence_complete}.\n\n| PID / start | Change | Before | After |\n|---|---|---|---|\n"
for c in self.changes {
out += "| \{c.pid}/\{c.start_ticks} | \{c.change} | \{c.before} | \{c.after} |\n"
}
for issue in self.issues {
out += "\n- \{md(issue)}\n"
}
out +
"\nComparison uses each session's final sample, not whole-window maxima. Labels before/after follow argument order, not a claim about wall-clock ordering. Missing processes do not prove completion; intervals between samples remain unobserved.\n"
}