use aristo_core::canon_verify::{
AnnotationOutcomeStatus, GetVerifySessionResponse, SessionStatus, TestOutcome,
TestOutcomeStatus,
};
use aristo_core::expectations::ExpectationsFile;
use aristo_core::index::{AnnotationId, IndexFile};
use crate::commands::index::now_rfc3339;
use crate::workspace::Workspace;
use crate::{CliError, CliResult};
pub(crate) fn waiver_key(tier: &str, canon_id: &str) -> Option<AnnotationId> {
if tier != "aristos:" && tier != "kanon:" {
return None;
}
AnnotationId::parse(&format!("{tier}{}", canon_id.trim())).ok()
}
pub(crate) fn tests_operationally_broken(tests: &[TestOutcome]) -> bool {
tests.iter().any(|t| {
matches!(
t.status,
TestOutcomeStatus::BuildFailed
| TestOutcomeStatus::CloneFailed
| TestOutcomeStatus::Timeout
| TestOutcomeStatus::Error
)
})
}
pub(crate) fn run_accept(
ws: &Workspace,
index: &IndexFile,
requested: &str,
reason: &str,
tracking: Option<&str>,
) -> CliResult<()> {
if reason.trim().is_empty() {
return Err(CliError::Other {
message: "--because requires a non-empty reason. A reasonless waiver is how \
baselines rot; explain why this gap is accepted."
.into(),
exit_code: 2,
});
}
let id = resolve_canon_id(index, requested)?;
let path = ws.expectations_path();
let mut file = ExpectationsFile::read(&path).map_err(|e| CliError::Other {
message: format!("failed to read {}: {e}", path.display()),
exit_code: 1,
})?;
file.accept(
id.clone(),
reason.to_string(),
tracking.map(str::to_string),
now_rfc3339(),
);
file.write_atomic(&path).map_err(CliError::Io)?;
println!("accepted known gap: {}", id.as_str());
println!(" reason: {reason}");
if let Some(t) = tracking {
println!(" tracking: {t}");
}
println!();
println!(
" Recorded in .aristo/expectations.toml — commit it. `aristo verify` will report this as a"
);
println!(
" known gap (not a failure) until the property holds; when it does, verify goes red so you"
);
println!(" remember to remove the stale waiver.");
Ok(())
}
fn resolve_canon_id(index: &IndexFile, requested: &str) -> CliResult<AnnotationId> {
let raw = requested.trim();
if raw.starts_with("arta_") {
return Err(CliError::Other {
message: format!(
"--accept rejects opaque server ids (got `{raw}`). Pass the source-form canon id, \
e.g. `aristos:foo` or just `foo`."
),
exit_code: 2,
});
}
let candidates: Vec<String> = if raw.contains(':') {
vec![raw.to_string()]
} else {
vec![format!("aristos:{raw}"), format!("kanon:{raw}")]
};
for cand in &candidates {
if let Ok(id) = AnnotationId::parse(cand) {
if id.is_canon_bound() && index.entries.contains_key(&id) {
return Ok(id);
}
}
}
Err(CliError::Other {
message: format!(
"`{raw}` is not a canon-bound (`aristos:` / `kanon:`) entry in this workspace's index. \
Only canon-bound properties can be waived — run `aristo list` to see eligible ids."
),
exit_code: 1,
})
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub(crate) struct WaiverVerdict {
pub unwaived_failed: u32,
pub accepted_gaps: u32,
pub ratchet_breaches: u32,
pub build_failed: u32,
pub inconclusive: u32,
pub incomplete_session: bool,
}
impl WaiverVerdict {
pub fn is_red(&self) -> bool {
self.unwaived_failed > 0
|| self.build_failed > 0
|| self.inconclusive > 0
|| self.ratchet_breaches > 0
|| self.incomplete_session
}
}
fn is_clean_accepted_gap(
ann: &aristo_core::canon_verify::AnnotationVerification,
expectations: &ExpectationsFile,
) -> bool {
matches!(ann.status, AnnotationOutcomeStatus::Failed)
&& !tests_operationally_broken(&ann.tests)
&& waiver_key(&ann.tier, &ann.canon_id)
.map(|id| expectations.is_waived(&id))
.unwrap_or(false)
}
pub(crate) fn evaluate(
snapshot: &GetVerifySessionResponse,
expectations: &ExpectationsFile,
) -> WaiverVerdict {
let mut accepted_gaps = 0u32;
let mut ratchet_breaches = 0u32;
for ann in &snapshot.annotations {
if is_clean_accepted_gap(ann, expectations) {
accepted_gaps += 1;
continue;
}
let waived = waiver_key(&ann.tier, &ann.canon_id)
.map(|id| expectations.is_waived(&id))
.unwrap_or(false);
if waived && matches!(ann.status, AnnotationOutcomeStatus::Verified) {
ratchet_breaches += 1;
}
}
let s = &snapshot.summary;
WaiverVerdict {
unwaived_failed: s.failed.saturating_sub(accepted_gaps),
accepted_gaps,
ratchet_breaches,
build_failed: s.build_failed,
inconclusive: s.inconclusive,
incomplete_session: !matches!(snapshot.status, SessionStatus::Done),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn summary_for(ann_status: &str) -> &'static str {
match ann_status {
"verified" => {
r#"{ "total_annotations": 1, "verified": 1, "failed": 0, "build_failed": 0, "inconclusive": 0, "no_coverage": 0 }"#
}
"failed" => {
r#"{ "total_annotations": 1, "verified": 0, "failed": 1, "build_failed": 0, "inconclusive": 0, "no_coverage": 0 }"#
}
"build_failed" => {
r#"{ "total_annotations": 1, "verified": 0, "failed": 0, "build_failed": 1, "inconclusive": 0, "no_coverage": 0 }"#
}
_ => {
r#"{ "total_annotations": 1, "verified": 0, "failed": 0, "build_failed": 0, "inconclusive": 0, "no_coverage": 0 }"#
}
}
}
fn snapshot_with(
ann_status: &str,
canon_id: &str,
tier: &str,
tests: &str,
) -> GetVerifySessionResponse {
let summary = summary_for(ann_status);
let json = format!(
r#"{{
"session_id": "s", "status": "done", "user_commit_sha": "x",
"canon_version": "v", "started_at": "t",
"annotations": [{{
"annotation_id": "arta_x", "canon_id": "{canon_id}", "version": "v",
"scope": "turso", "tier": "{tier}", "source_path": "p",
"status": "{ann_status}", "tests": {tests}
}}],
"summary": {summary}
}}"#
);
serde_json::from_str(&json).unwrap()
}
fn waived(id: &str) -> ExpectationsFile {
let mut f = ExpectationsFile::default();
f.accept(
AnnotationId::parse(id).unwrap(),
"reason".into(),
None,
"t".into(),
);
f
}
#[test]
fn unwaived_failure_is_red() {
let snap = snapshot_with("failed", "foo", "aristos:", "[]");
let v = evaluate(&snap, &ExpectationsFile::default());
assert_eq!(v.unwaived_failed, 1);
assert_eq!(v.accepted_gaps, 0);
assert!(v.is_red());
}
#[test]
fn waived_failure_is_an_accepted_gap_not_red() {
let snap = snapshot_with("failed", "foo", "aristos:", "[]");
let v = evaluate(&snap, &waived("aristos:foo"));
assert_eq!(v.accepted_gaps, 1);
assert_eq!(v.unwaived_failed, 0);
assert!(!v.is_red());
}
#[test]
fn waived_pass_trips_the_ratchet_and_is_red() {
let snap = snapshot_with("verified", "foo", "aristos:", "[]");
let v = evaluate(&snap, &waived("aristos:foo"));
assert_eq!(v.ratchet_breaches, 1);
assert!(v.is_red());
}
#[test]
fn unwaived_pass_is_green() {
let snap = snapshot_with("verified", "foo", "aristos:", "[]");
let v = evaluate(&snap, &ExpectationsFile::default());
assert_eq!(v.ratchet_breaches, 0);
assert!(!v.is_red());
}
#[test]
fn build_failure_is_red_even_when_waived() {
let snap = snapshot_with("build_failed", "foo", "aristos:", "[]");
let v = evaluate(&snap, &waived("aristos:foo"));
assert_eq!(v.build_failed, 1);
assert_eq!(v.accepted_gaps, 0);
assert!(v.is_red());
}
#[test]
fn empty_annotations_with_summary_failure_is_still_red() {
let json = r#"{
"session_id": "s", "status": "done", "user_commit_sha": "x",
"canon_version": "v", "started_at": "t", "annotations": [],
"summary": { "total_annotations": 1, "verified": 0, "failed": 1,
"build_failed": 0, "inconclusive": 0, "no_coverage": 0 }
}"#;
let snap: GetVerifySessionResponse = serde_json::from_str(json).unwrap();
let v = evaluate(&snap, &ExpectationsFile::default());
assert_eq!(v.unwaived_failed, 1);
assert!(v.is_red());
}
#[test]
fn non_done_terminal_session_is_red() {
for status in ["failed", "timed_out", "cancelled"] {
let json = format!(
r#"{{
"session_id": "s", "status": "{status}", "user_commit_sha": "x",
"canon_version": "v", "started_at": "t", "annotations": [],
"summary": {{ "total_annotations": 0, "verified": 0, "failed": 0,
"build_failed": 0, "inconclusive": 0, "no_coverage": 0 }}
}}"#
);
let snap: GetVerifySessionResponse = serde_json::from_str(&json).unwrap();
let v = evaluate(&snap, &ExpectationsFile::default());
assert!(v.incomplete_session, "{status} must be incomplete");
assert!(v.is_red(), "{status} session must be red");
}
}
#[test]
fn waived_failed_with_operational_test_is_not_forgiven() {
let tests = r#"[{ "test_binary": "t1", "status": "fail" },
{ "test_binary": "t2", "status": "build_failed" }]"#;
let snap = snapshot_with("failed", "foo", "aristos:", tests);
let v = evaluate(&snap, &waived("aristos:foo"));
assert_eq!(v.accepted_gaps, 0, "operational break is not a clean gap");
assert_eq!(v.unwaived_failed, 1);
assert!(v.is_red());
}
#[test]
fn waiver_key_rejects_colonless_tier() {
assert!(waiver_key("aristos", "foo").is_none());
assert_eq!(
waiver_key("aristos:", "foo").unwrap().as_str(),
"aristos:foo"
);
assert_eq!(waiver_key("kanon:", "bar").unwrap().as_str(), "kanon:bar");
}
}