///|
pub(all) struct WaitAssessment {
status : String
threshold_samples : Int32
sample_count : Int32
observed_streak : Int32
evidence_gaps : Int32
reason : String
} derive(ToJson)
///|
pub extend WaitAssessment with ToJson::{to_json}
///|
fn assess_wait(
samples : Int32,
streak : Int32,
gaps : Int32,
threshold : Int32,
) -> WaitAssessment raise LockError {
if threshold < 1 || threshold > 60 {
invalid(0, "wait sample threshold must be 1..60")
}
let (status, reason) = if streak >= threshold {
(
"fail", "Confirmed lock waits reached the consecutive-sample threshold; gaps elsewhere do not erase observed evidence.",
)
} else if gaps > 0 || samples < threshold {
(
"inconclusive", "Visibility gaps or insufficient samples prevent a passing assessment.",
)
} else {
(
"pass", "No consecutive lock-wait streak reached the threshold in this observation window; this does not prove business completion.",
)
}
{
status,
reason,
threshold_samples: threshold,
sample_count: samples,
observed_streak: streak,
evidence_gaps: gaps,
}
}
///|
/// Task streaks require the same PID/starttime in consecutive observations.
pub fn TaskCapture::assess(
self : TaskCapture,
wait_samples~ : Int32 = 2,
) -> WaitAssessment raise LockError {
let r = self.report()
let gaps = r.frames
.filter(f => f.root_state != "present" || !f.issues.is_empty())
.length()
assess_wait(
r.sample_count,
r.max_consecutive_wait_samples,
gaps,
wait_samples,
)
}
///|
/// File streaks mean at least one waiter for the original file in each sample.
pub fn FileCapture::assess(
self : FileCapture,
wait_samples~ : Int32 = 2,
) -> WaitAssessment raise LockError {
let r = self.report()
let gaps = r.frames
.filter(f => f.path_state != "present" || !f.issues.is_empty())
.length()
assess_wait(
r.sample_count,
r.max_consecutive_wait_samples,
gaps,
wait_samples,
)
}
///|
pub fn assess_capture(
text : String,
wait_samples~ : Int32 = 2,
) -> WaitAssessment raise LockError {
let schema = match task_json(text) {
Json::Object(fields) => fields.get("schema")
_ => None
}
match schema {
Some(Json::String("moonlocklens-task/1")) =>
parse_task_capture(text).assess(wait_samples~)
Some(Json::String("moonlocklens-file/1")) =>
parse_file_capture(text).assess(wait_samples~)
_ => {
invalid(
0, "gate requires a task or file capture, not a rendered report or raw locks",
)
abort("unreachable")
}
}
}