use std::collections::HashSet;
use std::path::Path;
use std::time::{Duration, Instant};
use anstyle::{Ansi256Color, AnsiColor, Color, Style};
use textwrap::core::display_width;
use aristo_core::canon::CanonMatchesFile;
use aristo_core::canon_verify::{
AnnotationOutcomeStatus, CallFrame, DifferentialReport, FaultSpec, FieldDivergence, Finding,
FrameRole, GetVerifySessionResponse, HttpVerifyClient, PostVerifySessionResponse,
SessionStatus, Snapshot, TestOutcome, TestOutcomeStatus, TestStep, VerifyClient, VerifyError,
VerifySessionRequest, VerifySessionTag,
};
use aristo_core::expectations::{Expectation, ExpectationsFile};
use aristo_core::index::{AnnotationId, IndexEntry, IndexFile, IntentEntry};
use super::card::Card;
use super::waiver;
use crate::workspace::Workspace;
use crate::{CliError, CliResult};
const LONGPOLL_WAIT_SECS: u32 = 30;
const POLL_INTERVAL: Duration = Duration::from_secs(3);
fn poll_interval() -> Duration {
if let Ok(ms) = std::env::var("ARISTO_VERIFY_POLL_MS") {
if let Ok(n) = ms.parse::<u64>() {
return Duration::from_millis(n);
}
}
POLL_INTERVAL
}
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct CanonDispatchEntry<'a> {
pub id: &'a AnnotationId,
pub entry: &'a IntentEntry,
}
pub(crate) fn partition_full<'a>(
index: &'a IndexFile,
pending_full_ids: &[&'a AnnotationId],
) -> (
Vec<CanonDispatchEntry<'a>>,
Vec<&'a AnnotationId>, // non-canon-bound; keep the NotImplemented hint
) {
let mut canon: Vec<CanonDispatchEntry<'a>> = Vec::new();
let mut other: Vec<&'a AnnotationId> = Vec::new();
for id in pending_full_ids {
let Some(entry) = index.entries.get(*id) else {
continue;
};
let IndexEntry::Intent(intent) = entry else {
continue;
};
if id.is_canon_bound() {
canon.push(CanonDispatchEntry { id, entry: intent });
} else {
other.push(id);
}
}
(canon, other)
}
pub(crate) fn build_tags(
entries: &[CanonDispatchEntry<'_>],
matches: &CanonMatchesFile,
) -> Vec<VerifySessionTag> {
entries
.iter()
.filter_map(|d| build_one_tag(d, matches))
.collect()
}
fn build_one_tag(
dispatch: &CanonDispatchEntry<'_>,
matches: &CanonMatchesFile,
) -> Option<VerifySessionTag> {
let annotation_id = match &dispatch.entry.binding {
aristo_core::index::BindingState::Local => return None,
aristo_core::index::BindingState::Bound { linked } => linked.as_str().to_string(),
aristo_core::index::BindingState::Certified { linked, .. } => linked.as_str().to_string(),
};
let canon_id = strip_canon_prefix(dispatch.id.as_str()).to_string();
let cache_entry = matches.entries.get(dispatch.id)?;
let accepted = cache_entry.accepted_matches.first()?;
let version = accepted.version.clone();
let source_path = format_source_path(&dispatch.entry.file, dispatch.entry.site.as_str());
Some(VerifySessionTag {
annotation_id,
canon_id,
version,
source_path,
})
}
fn strip_canon_prefix(id: &str) -> &str {
id.strip_prefix("aristos:")
.or_else(|| id.strip_prefix("kanon:"))
.unwrap_or(id)
}
fn format_source_path(file: &str, site: &str) -> String {
match extract_line(site) {
Some(line) => format!("{file}:{line}"),
None => file.to_string(),
}
}
fn extract_line(site: &str) -> Option<u32> {
let after = site.split(" (line ").nth(1)?;
let trimmed = after.trim_end_matches(')');
trimmed.parse().ok()
}
#[cfg(test)]
pub(crate) fn dispatch_session<C: VerifyClient + ?Sized>(
client: &C,
req: &VerifySessionRequest,
) -> Result<PostVerifySessionResponse, VerifyError> {
client.post_session(req)
}
pub(crate) fn run_canon_dispatch(
workspace_root: &Path,
matches_path: &Path,
expectations_path: &Path,
canon_entries: &[CanonDispatchEntry<'_>],
tags_filter: Option<&[String]>,
wait: bool,
) -> CliResult<usize> {
if canon_entries.is_empty() {
return Ok(0);
}
let creds = aristo_core::auth::resolve_full().map_err(no_auth_to_cli_error)?;
let matches = CanonMatchesFile::read(matches_path).map_err(|e| CliError::Other {
message: format!(
"failed to read canon-matches cache at {}: {e}",
matches_path.display()
),
exit_code: 1,
})?;
let mut tags = build_tags(canon_entries, &matches);
if let Some(requested) = tags_filter {
let allowed = build_tag_filter_set(canon_entries, requested)?;
tags.retain(|t| allowed.contains(&t.annotation_id));
if tags.is_empty() {
return Err(CliError::Other {
message: format!(
"--tags filter matched zero eligible canon-bound entries. \
Requested ids: {}",
requested.join(", ")
),
exit_code: 1,
});
}
}
if tags.is_empty() {
println!(
"\n→ {} canon-bound entries are pending verification, but no `accepted_matches` were found in",
canon_entries.len()
);
println!(
" .aristo/canon-matches.toml. Run `aristo canon refresh` to repopulate the cache."
);
return Ok(0);
}
let repo_full_name = match aristo_core::auth::derive_repo_full_name(workspace_root) {
Ok(r) => r,
Err(e) => {
std::env::var("ARISTO_REPO").map_err(|_| CliError::Other {
message: format!(
"could not determine repo for verify: {e}\n \
Set ARISTO_REPO=<owner/repo> to override (CI use)."
),
exit_code: 1,
})?
}
};
let commit_sha =
aristo_core::git::rev_parse_head(workspace_root).map_err(|e| CliError::Other {
message: format!("git rev-parse HEAD failed: {e}"),
exit_code: 1,
})?;
let pushed =
aristo_core::git::commit_present_on_remote(workspace_root, &commit_sha).map_err(|e| {
CliError::Other {
message: format!("git push-first precheck failed: {e}"),
exit_code: 1,
}
})?;
if !pushed {
return Err(CliError::Other {
message: format!(
"HEAD ({}) is not pushed to origin. Push your branch first; \
`aristo verify --watch` for local-edit sync is planned but \
not yet shipped.",
short_sha(&commit_sha)
),
exit_code: 1,
});
}
let base_url =
std::env::var("ARETTA_API_URL").unwrap_or_else(|_| creds.server.as_str().to_string());
let client: Box<dyn VerifyClient> = if let Some(mock) = test_mock_client_from_env() {
mock
} else {
Box::new(HttpVerifyClient::new(base_url, &creds.token))
};
let req = VerifySessionRequest {
repo_full_name,
commit_sha,
tags,
};
let resp = client.post_session(&req).map_err(verify_error_to_cli)?;
let dispatched = req.tags.len();
print_session_dispatched(&req, &resp);
if wait {
let expectations =
ExpectationsFile::read(expectations_path).map_err(|e| CliError::Other {
message: format!("failed to read {}: {e}", expectations_path.display()),
exit_code: 1,
})?;
let final_snapshot = poll_until_terminal(&*client, &resp.session_id)?;
render_final_snapshot(&final_snapshot, &expectations);
let verdict = waiver::evaluate(&final_snapshot, &expectations);
if verdict.is_red() {
return Err(exit_error_for(&verdict));
}
}
Ok(dispatched)
}
fn exit_error_for(verdict: &waiver::WaiverVerdict) -> CliError {
let ratchet = if verdict.ratchet_breaches > 0 {
format!(", {} accepted-gap-now-passes", verdict.ratchet_breaches)
} else {
String::new()
};
CliError::Other {
message: format!(
"verify reported {} failed, {} build_failed, {} inconclusive{ratchet}",
verdict.unwaived_failed, verdict.build_failed, verdict.inconclusive
),
exit_code: 1,
}
}
pub(crate) fn run_view_session(session_id: &str, wait: bool) -> CliResult<()> {
let creds = aristo_core::auth::resolve_full().map_err(no_auth_to_cli_error)?;
let base_url =
std::env::var("ARETTA_API_URL").unwrap_or_else(|_| creds.server.as_str().to_string());
let client: Box<dyn VerifyClient> = if let Some(mock) = test_mock_client_from_env() {
mock
} else {
Box::new(HttpVerifyClient::new(base_url, &creds.token))
};
let snapshot = if wait {
poll_until_terminal(&*client, session_id)?
} else {
client
.get_session(session_id, None)
.map_err(verify_error_to_cli)?
};
let expectations = match Workspace::find(None) {
Ok(ws) => ExpectationsFile::read(&ws.expectations_path()).map_err(|e| CliError::Other {
message: format!("failed to read {}: {e}", ws.expectations_path().display()),
exit_code: 1,
})?,
Err(_) => ExpectationsFile::default(),
};
render_final_snapshot(&snapshot, &expectations);
if wait {
let verdict = waiver::evaluate(&snapshot, &expectations);
if verdict.is_red() {
return Err(exit_error_for(&verdict));
}
}
Ok(())
}
fn poll_until_terminal<C: VerifyClient + ?Sized>(
client: &C,
session_id: &str,
) -> CliResult<GetVerifySessionResponse> {
let started = Instant::now();
let mut last_heartbeat = started;
let mut intermediate_rendered = false;
loop {
let snapshot = client
.get_session(session_id, Some(LONGPOLL_WAIT_SECS))
.map_err(verify_error_to_cli)?;
if snapshot.status.is_terminal() {
return Ok(snapshot);
}
if !intermediate_rendered {
println!();
println!(
" status: {} ({} of {} annotations verified so far)",
status_label(snapshot.status),
snapshot.summary.verified,
snapshot.summary.total_annotations
);
intermediate_rendered = true;
}
if last_heartbeat.elapsed() >= HEARTBEAT_INTERVAL {
let elapsed = started.elapsed().as_secs();
println!(" still running… ({elapsed}s elapsed)");
last_heartbeat = Instant::now();
}
std::thread::sleep(poll_interval());
}
}
fn build_tag_filter_set(
canon_entries: &[CanonDispatchEntry<'_>],
requested: &[String],
) -> CliResult<HashSet<String>> {
let mut allowed: HashSet<String> = HashSet::new();
let mut by_source: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
let mut by_canon: std::collections::HashMap<&str, String> = std::collections::HashMap::new();
for d in canon_entries {
let linked = match &d.entry.binding {
aristo_core::index::BindingState::Bound { linked } => linked.as_str().to_string(),
aristo_core::index::BindingState::Certified { linked, .. } => {
linked.as_str().to_string()
}
aristo_core::index::BindingState::Local => continue,
};
by_source.insert(d.id.as_str(), linked.clone());
let canon = strip_canon_prefix(d.id.as_str());
by_canon.insert(canon, linked);
}
for raw in requested {
let id = raw.trim();
if id.is_empty() {
continue;
}
if id.starts_with("arta_") {
return Err(CliError::Other {
message: format!(
"--tags rejects opaque server ids (got `{id}`). Pass the source-form id \
instead, e.g. `--tags aristos:foo,kanon:bar`."
),
exit_code: 2,
});
}
if let Some(linked) = by_source.get(id) {
allowed.insert(linked.clone());
} else if let Some(linked) = by_canon.get(id) {
allowed.insert(linked.clone());
} else {
return Err(CliError::Other {
message: format!(
"--tags id `{id}` is not a canon-bound entry in this workspace's index"
),
exit_code: 1,
});
}
}
Ok(allowed)
}
fn render_final_snapshot(snapshot: &GetVerifySessionResponse, expectations: &ExpectationsFile) {
println!();
println!(
"session {} — verifying {} against canon {}",
snapshot.session_id,
short_sha(&snapshot.user_commit_sha),
snapshot.canon_version
);
println!(
"status: {} ({}/{} verified)",
status_label(snapshot.status),
snapshot.summary.verified,
snapshot.summary.total_annotations
);
println!();
println!("{:<55} {:<14} TESTS", "ANNOTATION", "STATUS");
for ann in &snapshot.annotations {
let waiver_match: Option<(AnnotationId, &Expectation)> =
waiver::waiver_key(&ann.tier, &ann.canon_id)
.and_then(|id| expectations.get(&id).map(|e| (id, e)));
let waived_failing = matches!(ann.status, AnnotationOutcomeStatus::Failed)
&& waiver_match.is_some()
&& !waiver::tests_operationally_broken(&ann.tests);
let ratchet_breach =
matches!(ann.status, AnnotationOutcomeStatus::Verified) && waiver_match.is_some();
let (icon, word) = if waived_failing {
("[gap]", "accepted")
} else {
(
annotation_icon(ann.status),
annotation_status_word(ann.status),
)
};
let passed = ann
.tests
.iter()
.filter(|t| matches!(t.status, TestOutcomeStatus::Pass))
.count();
let header = format!(
"{}{}@{} ({})",
ann.tier, ann.canon_id, ann.version, ann.scope
);
let tests_summary = if ann.tests.is_empty() {
"(no coverage)".to_string()
} else {
format!("{}/{} passed", passed, ann.tests.len())
};
println!("{:<55} {} {:<11} {}", header, icon, word, tests_summary);
println!(" {}", ann.source_path);
if let Some((id, exp)) = &waiver_match {
if waived_failing {
let repro = format!("{}{}", ann.tier, ann.canon_id);
match first_failing_report(&ann.tests) {
Some(report) => render_violation_card(report, &repro, CardKind::Accepted(exp)),
None => render_accepted_gap(exp),
}
continue;
}
if ratchet_breach {
render_ratchet_breach(id);
continue;
}
}
for t in &ann.tests {
if !matches!(
t.status,
TestOutcomeStatus::Fail
| TestOutcomeStatus::BuildFailed
| TestOutcomeStatus::CloneFailed
| TestOutcomeStatus::Timeout
| TestOutcomeStatus::Error
) {
continue;
}
match &t.report {
Some(report) => render_violation_card(
report,
&format!("{}{}", ann.tier, ann.canon_id),
CardKind::Violated,
),
None => render_test_bullet(t),
}
}
if matches!(ann.status, AnnotationOutcomeStatus::Failed) && waiver_match.is_none() {
anstream::println!(
" {C_BOLD}Known limitation?{C_BOLD:#} acknowledge it so future runs report a known gap, not a failure:"
);
anstream::println!(
" {C_DIM}aristo verify --accept {}{} --because \"<why this is OK for now>\"{C_DIM:#}",
ann.tier, ann.canon_id
);
anstream::println!();
}
}
let matched: std::collections::BTreeSet<AnnotationId> = snapshot
.annotations
.iter()
.filter_map(|ann| {
let id = waiver::waiver_key(&ann.tier, &ann.canon_id)?;
expectations.is_waived(&id).then_some(id)
})
.collect();
for id in expectations.entries.keys() {
if !matched.contains(id) {
eprintln!(
" note: waiver for `{}` in .aristo/expectations.toml matched no annotation this \
run — renamed, removed, or stale. `aristo list` to check; remove if no longer needed.",
id.as_str()
);
}
}
println!();
}
fn render_test_bullet(t: &TestOutcome) {
let bin = t.test_binary.as_deref().unwrap_or("(session)");
let word = test_status_word(t.status);
match &t.stderr_url {
Some(url) => println!(" • {bin} {word} — see {url}"),
None => println!(" • {bin} {word}"),
}
}
fn render_accepted_gap(exp: &Expectation) {
let mut card = Card::new();
card.raw(
format!("{C_WARN}⚠ KNOWN PROPERTY FAILURE{C_WARN:#}"),
"⚠ KNOWN PROPERTY FAILURE",
);
card.blank();
card.wrap_hang("accepted ", &exp.reason, C_WARN);
if let Some(tracking) = &exp.tracking {
card.line(&format!("tracking {tracking}"), C_DIM, 0);
}
anstream::println!();
anstream::print!("{}", card.render());
anstream::println!();
}
fn render_ratchet_breach(id: &AnnotationId) {
let mut card = Card::new();
card.raw(
format!("{C_HEAD}✗ accepted gap now passes{C_HEAD:#}"),
"✗ accepted gap now passes",
);
card.blank();
card.wrap(
"This property now holds, so the waiver is stale — remove it from \
.aristo/expectations.toml:",
Style::new(),
0,
);
card.blank();
card.line(id.as_str(), C_DIM, 2);
anstream::println!();
anstream::print!("{}", card.render());
anstream::println!();
}
const C_HEAD: Style = Style::new()
.bold()
.fg_color(Some(Color::Ansi(AnsiColor::Red)));
const C_BOLD: Style = Style::new().bold();
const C_DIM: Style = Style::new().dimmed();
const C_DEL: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Red)));
const C_ADD: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green)));
const C_WARN: Style = Style::new()
.bold()
.fg_color(Some(Color::Ansi256(Ansi256Color(208))));
enum CardKind<'a> {
Violated,
Accepted(&'a Expectation),
}
fn render_violation_card(report: &DifferentialReport, repro_id: &str, kind: CardKind<'_>) {
let card = build_violation_card(report, &kind);
anstream::println!();
anstream::print!("{}", card.render());
if matches!(kind, CardKind::Violated) {
anstream::println!();
anstream::println!(" {C_BOLD}Reproduce{C_BOLD:#}");
anstream::println!(" {C_DIM}aristo verify --filter id={repro_id} --rerun{C_DIM:#}");
}
anstream::println!();
}
fn build_violation_card(report: &DifferentialReport, kind: &CardKind<'_>) -> Card {
let Finding::StateEq {
expected,
actual,
divergence,
} = &report.finding;
let mut card = Card::new();
match kind {
CardKind::Violated => card.raw(
format!(
"{C_HEAD}✗ PROPERTY VIOLATED{C_HEAD:#} {C_BOLD}{}{C_BOLD:#}",
report.property.canon_id
),
&format!("✗ PROPERTY VIOLATED {}", report.property.canon_id),
),
CardKind::Accepted(_) => card.raw(
format!(
"{C_WARN}⚠ KNOWN PROPERTY FAILURE{C_WARN:#} {C_BOLD}{}{C_BOLD:#}",
report.property.canon_id
),
&format!("⚠ KNOWN PROPERTY FAILURE {}", report.property.canon_id),
),
}
card.blank();
card.wrap(&report.property.statement, Style::new(), 0);
if let Some(s) = &report.property.impl_source {
let loc = match &s.snippet {
Some(snip) => format!("impl {}:{} · {snip}", s.path, s.line),
None => format!("impl {}:{}", s.path, s.line),
};
card.blank();
card.line(&loc, C_DIM, 0);
}
if let Some(fault) = &report.scenario.fault {
render_fault_banner(&mut card, fault);
}
if !report.scenario.test_shape.is_empty() {
card.blank();
render_test_shape(&mut card, &report.scenario.test_shape);
}
if !report.scenario.call_path.is_empty() {
card.blank();
render_call_path(&mut card, report);
}
card.blank();
render_divergence(&mut card, report, expected, actual, divergence);
if let CardKind::Accepted(exp) = kind {
card.blank();
card.wrap_hang("accepted ", &exp.reason, C_WARN);
if let Some(tracking) = &exp.tracking {
card.line(&format!("tracking {tracking}"), C_DIM, 0);
}
}
card
}
fn render_fault_banner(card: &mut Card, fault: &FaultSpec) {
let op = fault.op.to_lowercase();
let plain = format!(
"fault {} · {op} #{} → {} · fired {}×",
fault.kind, fault.nth, fault.error, fault.fired_count
);
let styled = format!(
"{C_WARN}fault{C_WARN:#} {} · {op} #{} → {C_WARN}{}{C_WARN:#} {C_DIM}· fired {}×{C_DIM:#}",
fault.kind, fault.nth, fault.error, fault.fired_count
);
card.raw(styled, &plain);
}
fn render_test_shape(card: &mut Card, steps: &[TestStep]) {
card.raw(
format!("{C_BOLD}what turso ran{C_BOLD:#}"),
"what turso ran",
);
for step in steps {
card.line(&step.label, C_DIM, 2);
}
}
fn render_call_path(card: &mut Card, report: &DifferentialReport) {
let frames = &report.scenario.call_path;
let Some(entry) = frames.first() else {
return;
};
let entry_label = shorten_frame_label(&entry.label);
card.raw(
format!(
"{C_BOLD}where your code broke it{C_BOLD:#} {C_DIM}· inside {entry_label}{C_DIM:#}"
),
&format!("where your code broke it · inside {entry_label}"),
);
card.blank();
let body = &frames[1..];
let fork_depth = fork_depth(frames);
const BASE: usize = 2;
let fault_marker = report.scenario.fault.as_ref().map(|f| {
format!(
"✗ {} → {} fault injected here",
f.op.to_lowercase(),
f.error
)
});
struct RenderedFrame {
prefix: String,
label: String,
role: FrameRole,
}
let mut rendered: Vec<RenderedFrame> = Vec::with_capacity(body.len());
for (i, frame) in body.iter().enumerate() {
let rel = frame.depth.saturating_sub(fork_depth) as usize;
let prefix = if rel == 0 {
" ".repeat(BASE)
} else {
format!("{}{}", " ".repeat(BASE), tree_prefix(body, i, rel))
};
rendered.push(RenderedFrame {
prefix,
label: shorten_frame_label(&frame.label),
role: frame.role,
});
}
let marker_col = rendered
.iter()
.filter(|r| matches!(r.role, FrameRole::Fault))
.map(|r| display_width(&r.prefix) + display_width(&r.label))
.max()
.map(|w| w + 2);
for r in &rendered {
match r.role {
FrameRole::Normal | FrameRole::Effect => {
let plain = format!("{}{}", r.prefix, r.label);
let styled = format!("{}{C_DIM}{}{C_DIM:#}", r.prefix, r.label);
card.raw(styled, &plain);
}
FrameRole::Fault => {
let marker = fault_marker.as_deref().unwrap_or("✗ fault injected here");
let here = display_width(&r.prefix) + display_width(&r.label);
let gap = " ".repeat(marker_col.unwrap_or(here + 2).saturating_sub(here));
let plain = format!("{}{}{gap}{marker}", r.prefix, r.label);
let styled = format!(
"{}{C_DIM}{}{C_DIM:#}{gap}{C_WARN}{marker}{C_WARN:#}",
r.prefix, r.label
);
card.raw(styled, &plain);
}
}
}
}
fn tree_prefix(body: &[CallFrame], i: usize, rel: usize) -> String {
let fork = body[i].depth.saturating_sub(rel as u32);
let rel_at = |idx: usize| (body[idx].depth.saturating_sub(fork)) as usize;
let has_later_sibling = |level: usize| {
for j in (i + 1)..body.len() {
let rj = rel_at(j);
if rj < level {
return false;
}
if rj == level {
return true;
}
}
false
};
let mut out = String::new();
for level in 1..rel {
out.push_str(if has_later_sibling(level) {
"│ "
} else {
" "
});
}
out.push_str(if has_later_sibling(rel) {
"├─ "
} else {
"└─ "
});
out
}
fn fork_depth(frames: &[CallFrame]) -> u32 {
let mut fork = u32::MAX;
let mut prev: Option<u32> = None;
for f in frames {
if let Some(p) = prev {
if f.depth <= p && f.depth >= 1 {
fork = fork.min(f.depth - 1);
}
}
prev = Some(f.depth);
}
fork
}
fn shorten_frame_label(label: &str) -> String {
let segs: Vec<&str> = label.split("::").collect();
let n = segs.len();
if n == 0 {
return label.to_string();
}
let last = segs[n - 1];
if n >= 2 {
let penult = segs[n - 2];
let is_type = penult
.chars()
.next()
.is_some_and(|c| c.is_ascii_uppercase());
if is_type {
return format!("{penult}::{last}");
}
}
last.to_string()
}
fn render_divergence(
card: &mut Card,
report: &DifferentialReport,
expected: &Snapshot,
actual: &Snapshot,
divergence: &[FieldDivergence],
) {
let compared = report.relation.compared.join(", ");
let n_ignored = report.relation.ignored.len();
let field_word = if n_ignored == 1 { "field" } else { "fields" };
card.raw(
format!(
"{C_BOLD}divergence observed{C_BOLD:#} {C_DIM}· compared [{compared}] \
({n_ignored} {field_word} ignored){C_DIM:#}"
),
&format!(
"divergence observed · compared [{compared}] ({n_ignored} {field_word} ignored)"
),
);
card.blank();
const LABEL_W: usize = 11;
const VALUE_W: usize = 21;
for d in divergence {
diff_row(
card,
"expected",
&d.field,
&d.expected,
&expected.label,
C_DEL,
LABEL_W,
VALUE_W,
);
diff_row(
card,
"actual",
&d.field,
&d.actual,
&actual.label,
C_ADD,
LABEL_W,
VALUE_W,
);
}
if let Some(why) = divergence.first().and_then(|d| d.provenance.as_deref()) {
card.blank();
card.wrap_hang(" why ", why, C_DIM);
}
}
#[allow(clippy::too_many_arguments)]
fn diff_row(
card: &mut Card,
tag: &str,
field: &str,
value: &str,
side: &str,
vstyle: Style,
label_w: usize,
value_w: usize,
) {
let clause = format!("{field} = {value}");
let label_pad = " ".repeat(label_w.saturating_sub(display_width(tag)).max(1));
let value_pad = " ".repeat(value_w.saturating_sub(display_width(&clause)).max(1));
let plain = format!("{tag}{label_pad}{clause}{value_pad}{side}");
let styled = format!(
"{C_DIM}{tag}{C_DIM:#}{label_pad}{vstyle}{clause}{vstyle:#}{value_pad}{C_DIM}{side}{C_DIM:#}"
);
card.raw_indented(styled, &plain, 2);
}
fn first_failing_report(tests: &[TestOutcome]) -> Option<&DifferentialReport> {
tests.iter().find_map(|t| {
matches!(
t.status,
TestOutcomeStatus::Fail
| TestOutcomeStatus::BuildFailed
| TestOutcomeStatus::CloneFailed
| TestOutcomeStatus::Timeout
| TestOutcomeStatus::Error
)
.then_some(t.report.as_ref())
.flatten()
})
}
fn status_label(s: SessionStatus) -> &'static str {
match s {
SessionStatus::Queued => "queued",
SessionStatus::Running => "running",
SessionStatus::Done => "done",
SessionStatus::Failed => "failed",
SessionStatus::TimedOut => "timed out",
SessionStatus::Cancelled => "cancelled",
}
}
fn annotation_icon(s: AnnotationOutcomeStatus) -> &'static str {
match s {
AnnotationOutcomeStatus::Verified => "[ok]",
AnnotationOutcomeStatus::Failed => "[fail]",
AnnotationOutcomeStatus::BuildFailed => "[warn]",
AnnotationOutcomeStatus::Inconclusive => "[?]",
AnnotationOutcomeStatus::NoCoverage => "[--]",
}
}
fn annotation_status_word(s: AnnotationOutcomeStatus) -> &'static str {
match s {
AnnotationOutcomeStatus::Verified => "verified",
AnnotationOutcomeStatus::Failed => "failed",
AnnotationOutcomeStatus::BuildFailed => "build_failed",
AnnotationOutcomeStatus::Inconclusive => "inconclusive",
AnnotationOutcomeStatus::NoCoverage => "no_coverage",
}
}
fn test_status_word(s: TestOutcomeStatus) -> &'static str {
match s {
TestOutcomeStatus::Pass => "passed",
TestOutcomeStatus::Fail => "failed",
TestOutcomeStatus::BuildFailed => "build_failed",
TestOutcomeStatus::CloneFailed => "clone_failed",
TestOutcomeStatus::Timeout => "timeout",
TestOutcomeStatus::Error => "error",
}
}
fn print_session_dispatched(req: &VerifySessionRequest, resp: &PostVerifySessionResponse) {
let annotation_word = if resp.plan_size == 1 {
"annotation"
} else {
"annotations"
};
println!();
println!(
"→ verify session dispatched — verifying {} {annotation_word} against {}",
resp.plan_size,
short_sha(&req.commit_sha)
);
println!(" session: {}", resp.session_id);
println!(" view: {}", resp.view_url);
println!();
println!(
" Re-attach with: aristo verify --view {} --wait",
resp.session_id
);
}
fn short_sha(sha: &str) -> String {
sha.chars().take(7).collect()
}
fn no_auth_to_cli_error(e: aristo_core::auth::AuthError) -> CliError {
CliError::Other {
message: format!(
"verify requires authentication: {e}\n \
Run `aristo auth login` to sign in.\n \
(Without a token, the SDK skips canon-bound `verify=\"full\"` \
entries — they'd be rejected server-side anyway.)"
),
exit_code: 1,
}
}
fn verify_error_to_cli(e: VerifyError) -> CliError {
match e {
VerifyError::Auth(inner) => CliError::Other {
message: format!(
"verify auth error: {inner}\n \
Your token may be expired — re-run `aristo auth login`."
),
exit_code: 1,
},
VerifyError::BadRequest {
status: 402,
message,
} => CliError::Other {
message: format!(
"no canon coverage applies for your scopes — \
contact Aretta for DP onboarding to enable verification.\n \
(server message: {message})"
),
exit_code: 1,
},
VerifyError::BadRequest { status, message } => CliError::Other {
message: format!("verify server rejected request (HTTP {status}): {message}"),
exit_code: 1,
},
other => CliError::Other {
message: format!("verify failed: {other}"),
exit_code: 1,
},
}
}
fn test_mock_client_from_env() -> Option<Box<dyn VerifyClient>> {
let path = std::env::var("ARISTO_CANON_VERIFY_FIXTURE").ok()?;
let raw = std::fs::read_to_string(&path).ok()?;
let parsed: FixtureFile = serde_json::from_str(&raw).ok()?;
let post_resp = parsed.post.map(|p| PostVerifySessionResponse {
session_id: p.session_id,
view_url: p.view_url,
plan_size: p.plan_size,
});
let get_responses: Vec<GetVerifySessionResponse> = parsed.gets.unwrap_or_default();
let record_path = format!("{path}.posted.json");
let mock = match (post_resp, get_responses.is_empty()) {
(Some(p), true) => aristo_core::canon_verify::MockVerifyClient::with_post_response(p),
(Some(p), false) => {
aristo_core::canon_verify::MockVerifyClient::with_post_and_gets(p, get_responses)
}
(None, false) => {
aristo_core::canon_verify::MockVerifyClient::with_get_responses(get_responses)
}
(None, true) => return None,
};
Some(Box::new(RecordingMock {
inner: mock,
record_path,
}))
}
#[derive(serde::Deserialize)]
struct FixtureFile {
post: Option<FixturePost>,
gets: Option<Vec<GetVerifySessionResponse>>,
}
#[derive(serde::Deserialize)]
struct FixturePost {
session_id: String,
view_url: String,
plan_size: u32,
}
struct RecordingMock {
inner: aristo_core::canon_verify::MockVerifyClient,
record_path: String,
}
impl VerifyClient for RecordingMock {
fn post_session(
&self,
req: &VerifySessionRequest,
) -> Result<PostVerifySessionResponse, VerifyError> {
if let Ok(serialized) = serde_json::to_string_pretty(req) {
let _ = std::fs::write(&self.record_path, serialized);
}
self.inner.post_session(req)
}
fn get_session(
&self,
session_id: &str,
wait_seconds: Option<u32>,
) -> Result<aristo_core::canon_verify::GetVerifySessionResponse, VerifyError> {
self.inner.get_session(session_id, wait_seconds)
}
}
#[cfg(test)]
mod tests {
use super::*;
use aristo_core::canon::{AcceptedMatch, CacheEntry, CacheMeta, PrefixTier};
use aristo_core::index::{
ArtaId, BindingState, CoveredRegion, IndexEntry, IntentEntry, Meta, Sha256, Status,
VerifyLevel, VerifyMethod,
};
use std::collections::BTreeMap;
fn empty_index() -> IndexFile {
IndexFile {
meta: Meta {
schema_version: 1,
generated_by: None,
generated_at: None,
source_root: None,
},
entries: BTreeMap::new(),
}
}
fn zero_hash() -> Sha256 {
Sha256::parse(&format!("sha256:{}", "0".repeat(64))).unwrap()
}
fn arta(s: &str) -> ArtaId {
ArtaId::parse(s).unwrap()
}
const CR03_FULL: &str = include_str!("fixtures/cr03.full.json");
fn cr03_report() -> DifferentialReport {
serde_json::from_str(CR03_FULL).expect("cr03.full.json must deserialize")
}
fn render_full_card(report: &DifferentialReport) -> String {
build_violation_card(report, &CardKind::Violated).render()
}
fn strip_sgr(s: &str) -> String {
let mut out = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
for n in chars.by_ref() {
if n == 'm' {
break;
}
}
} else {
out.push(c);
}
}
out
}
#[test]
fn violation_card_fits_88_cols() {
let report = cr03_report();
let rendered = render_full_card(&report);
for line in rendered.lines() {
assert!(
display_width(line) <= 88,
"every card line must fit 88 cols; got {} → {line:?}",
display_width(line)
);
}
eprintln!("\n{rendered}");
}
#[test]
fn violation_card_is_turso_only_no_ip_leak() {
let report = cr03_report();
let rendered = render_full_card(&report);
for forbidden in [
"io_bridge",
"harness",
"lean",
"Lean",
"aretta",
"InjectedTurso",
"/checkouts/",
] {
assert!(
!rendered.contains(forbidden),
"card must not leak {forbidden:?}; got:\n{rendered}"
);
}
}
#[test]
fn card_shows_what_turso_ran() {
let report = cr03_report();
let rendered = render_full_card(&report);
assert!(
rendered.contains("what turso ran"),
"missing 'what turso ran' header; got:\n{rendered}"
);
assert!(rendered.contains("PRAGMA journal_mode=WAL"));
assert!(
rendered.contains("CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, v INTEGER)")
);
assert!(
!rendered.contains("faulted on commit"),
"the cr_03-specific 'faulted on commit' note must be gone; got:\n{rendered}"
);
}
#[test]
fn card_call_path_fork_and_markers() {
let report = cr03_report();
let rendered = render_full_card(&report);
assert!(
rendered.contains("where your code broke it"),
"missing call-path header; got:\n{rendered}"
);
assert!(
rendered.contains("inside Connection::execute"),
"header must name the entry frame; got:\n{rendered}"
);
for label in [
"prepare_wal_start",
"begin_write_wal_header",
"WalFile::prepare_wal_finish",
] {
assert!(
rendered.contains(label),
"call path must show {label:?}; got:\n{rendered}"
);
}
assert!(
rendered.contains("→ EIO fault injected here"),
"fault marker missing/changed; got:\n{rendered}"
);
assert!(
!rendered.contains("writes the WAL header to disk"),
"the cr_03-specific effect marker must be gone; got:\n{rendered}"
);
assert!(
!rendered.contains("sets initialized"),
"the effect frame must NOT carry divergence values; got:\n{rendered}"
);
let line_with = |needle: &str| -> &str {
rendered
.lines()
.find(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no line for {needle:?}; got:\n{rendered}"))
};
assert!(
line_with("WalFile::prepare_wal_start").contains("├─ "),
"first branch must use the ├─ connector; got:\n{rendered}"
);
assert!(
line_with("WalFile::prepare_wal_finish").contains("└─ "),
"fault branch must use the └─ connector; got:\n{rendered}"
);
let label_col = |needle: &str| -> usize {
let line = rendered
.lines()
.find(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no line for {needle:?}"));
let plain = strip_sgr(line);
display_width(&plain[..plain.find(needle).unwrap()])
};
assert!(
label_col("begin_write_wal_header") > label_col("WalFile::prepare_wal_finish"),
"effect branch must be deeper (further right) than the fault branch:\n{rendered}"
);
}
#[test]
fn card_divergence_block_after_call_path() {
let report = cr03_report();
let rendered = render_full_card(&report);
let lines: Vec<&str> = rendered.lines().collect();
let pos = |needle: &str| {
lines
.iter()
.position(|l| l.contains(needle))
.unwrap_or_else(|| panic!("no line for {needle:?} in:\n{rendered}"))
};
assert!(
rendered.contains("divergence observed"),
"missing 'divergence observed' header; got:\n{rendered}"
);
assert!(
rendered.contains("initialized = true"),
"divergence must show the diverging value; got:\n{rendered}"
);
assert!(
pos("divergence observed") > pos("WalFile::prepare_wal_finish"),
"divergence block must come AFTER the call path; got:\n{rendered}"
);
assert!(
pos("initialized = true") > pos("WalFile::prepare_wal_finish"),
"the diverging value must render below the spine, not inline; got:\n{rendered}"
);
assert!(rendered.contains("compared [initialized]"));
assert!(rendered.contains("9 fields ignored"));
assert!(rendered.contains("initialized = false"));
}
#[test]
fn card_has_no_op_trace_rendering() {
let report = cr03_report();
assert!(
!report.scenario.op_trace.is_empty(),
"fixture should still carry op_trace data"
);
let rendered = render_full_card(&report);
assert!(
!rendered.contains("what we tested"),
"the old op-trace header must be gone; got:\n{rendered}"
);
assert!(
!rendered.contains("← injected"),
"the old op-trace injected marker must be gone; got:\n{rendered}"
);
}
fn intent(
id: &str,
binding: BindingState,
file: &str,
site_with_line: &str,
) -> (AnnotationId, IntentEntry) {
(
AnnotationId::parse(id).unwrap(),
IntentEntry {
text: "the property".into(),
verify: VerifyLevel::Method(VerifyMethod::Full),
status: Status::Unknown,
text_hash: zero_hash(),
body_hash: zero_hash(),
file: file.into(),
site: site_with_line.into(),
covered_region: CoveredRegion::Function,
binding,
parent: None,
last_critiqued_at_text_hash: None,
last_critique_finding_count: None,
},
)
}
fn matches_with(id: &AnnotationId, canon_id: &str, version: &str) -> CanonMatchesFile {
let mut f = CanonMatchesFile {
meta: CacheMeta::default(),
..CanonMatchesFile::default()
};
f.entries.insert(
id.clone(),
CacheEntry {
last_match_text_hash: "x".into(),
canon_fetched_at: "2026-05-24T00:00:00Z".into(),
pending_matches: vec![],
accepted_matches: vec![AcceptedMatch {
canon_id: canon_id.into(),
version: version.into(),
canonical_text: "the property".into(),
canon_version: "v0.2.0".into(),
confidence: 0.95,
prefix_tier: PrefixTier::Aristos,
backed_by: Some("test backing".into()),
linked: None,
accepted_at: "2026-05-24T00:00:00Z".into(),
bound_at: "2026-05-24T00:00:00Z".into(),
}],
rejected_matches: vec![],
},
);
f
}
#[test]
fn partition_separates_canon_bound_from_local() {
let (aristos_id, aristos_entry) = intent(
"aristos:foo",
BindingState::Bound {
linked: arta("arta_op4q3z9NbV"),
},
"src/x.rs",
"fn x (line 1)",
);
let (kanon_id, kanon_entry) = intent(
"kanon:bar",
BindingState::Bound {
linked: arta("arta_xyz1234567"),
},
"src/y.rs",
"fn y (line 10)",
);
let (local_id, local_entry) =
intent("baz", BindingState::Local, "src/z.rs", "fn z (line 5)");
let mut index = empty_index();
index
.entries
.insert(aristos_id.clone(), IndexEntry::Intent(aristos_entry));
index
.entries
.insert(kanon_id.clone(), IndexEntry::Intent(kanon_entry));
index
.entries
.insert(local_id.clone(), IndexEntry::Intent(local_entry));
let pending = vec![&aristos_id, &kanon_id, &local_id];
let (canon, other) = partition_full(&index, &pending);
assert_eq!(canon.len(), 2);
assert_eq!(other.len(), 1);
assert_eq!(other[0].as_str(), "baz");
let canon_ids: Vec<_> = canon.iter().map(|c| c.id.as_str()).collect();
assert!(canon_ids.contains(&"aristos:foo"));
assert!(canon_ids.contains(&"kanon:bar"));
}
#[test]
fn partition_returns_empty_for_no_input() {
let index = empty_index();
let (canon, other) = partition_full(&index, &[]);
assert!(canon.is_empty());
assert!(other.is_empty());
}
#[test]
fn build_tags_emits_arta_id_canon_id_version_and_source_path() {
let (id, entry) = intent(
"aristos:vacuum_preserves_logical_content",
BindingState::Bound {
linked: arta("arta_op4q3z9NbV"),
},
"crates/vacuum/src/lib.rs",
"fn vacuum (line 42)",
);
let matches = matches_with(&id, "vacuum_preserves_logical_content", "v0.1.0");
let dispatch = vec![CanonDispatchEntry {
id: &id,
entry: &entry,
}];
let tags = build_tags(&dispatch, &matches);
assert_eq!(tags.len(), 1);
let t = &tags[0];
assert_eq!(t.annotation_id, "arta_op4q3z9NbV");
assert_eq!(t.canon_id, "vacuum_preserves_logical_content");
assert_eq!(t.version, "v0.1.0");
assert_eq!(t.source_path, "crates/vacuum/src/lib.rs:42");
}
#[test]
fn build_tags_works_for_certified_binding_state() {
use aristo_core::index::{CommitHash, VerifiedOutcome};
let (id, entry) = intent(
"kanon:foo",
BindingState::Certified {
linked: arta("arta_aaaa1234"),
verified_outcome: VerifiedOutcome::parse(&format!("v1:{}", "A".repeat(86)))
.unwrap(),
last_verified_at_commit: CommitHash::parse(&"a".repeat(40)).unwrap(),
},
"src/foo.rs",
"fn foo (line 7)",
);
let matches = matches_with(&id, "foo", "v0.2.0");
let dispatch = vec![CanonDispatchEntry {
id: &id,
entry: &entry,
}];
let tags = build_tags(&dispatch, &matches);
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].annotation_id, "arta_aaaa1234");
assert_eq!(tags[0].canon_id, "foo");
assert_eq!(tags[0].version, "v0.2.0");
}
#[test]
fn build_tags_drops_entries_missing_from_cache() {
let (id, entry) = intent(
"aristos:foo",
BindingState::Bound {
linked: arta("arta_aaaa1234"),
},
"src/foo.rs",
"fn foo (line 7)",
);
let matches = CanonMatchesFile::default();
let dispatch = vec![CanonDispatchEntry {
id: &id,
entry: &entry,
}];
let tags = build_tags(&dispatch, &matches);
assert!(tags.is_empty(), "missing cache entry must drop the tag");
}
#[test]
fn build_tags_drops_local_binding() {
let (id, entry) = intent(
"aristos:foo",
BindingState::Local,
"src/foo.rs",
"fn foo (line 7)",
);
let matches = matches_with(&id, "foo", "v0.1.0");
let dispatch = vec![CanonDispatchEntry {
id: &id,
entry: &entry,
}];
let tags = build_tags(&dispatch, &matches);
assert!(tags.is_empty());
}
#[test]
fn build_tags_falls_back_to_file_only_when_site_lacks_line() {
let (id, entry) = intent(
"aristos:foo",
BindingState::Bound {
linked: arta("arta_aaaa1234"),
},
"src/foo.rs",
"fn foo", );
let matches = matches_with(&id, "foo", "v0.1.0");
let dispatch = vec![CanonDispatchEntry {
id: &id,
entry: &entry,
}];
let tags = build_tags(&dispatch, &matches);
assert_eq!(tags.len(), 1);
assert_eq!(tags[0].source_path, "src/foo.rs");
}
#[test]
fn strip_canon_prefix_handles_both_tiers_and_no_prefix() {
assert_eq!(strip_canon_prefix("aristos:foo_bar"), "foo_bar");
assert_eq!(
strip_canon_prefix("kanon:checkout_total_non_negative"),
"checkout_total_non_negative"
);
assert_eq!(strip_canon_prefix("local_id"), "local_id");
}
#[test]
fn extract_line_parses_walker_emitted_site_format() {
assert_eq!(extract_line("fn foo (line 42)"), Some(42));
assert_eq!(extract_line("fn really::long::path (line 1)"), Some(1));
assert_eq!(extract_line("fn x"), None);
assert_eq!(extract_line(""), None);
}
#[test]
fn dispatch_session_round_trips_through_mock() {
let mock = aristo_core::canon_verify::MockVerifyClient::with_post_response(
PostVerifySessionResponse {
session_id: "01HMTEST".into(),
view_url: "https://dev.aretta.ai/dashboard/jobs/01HMTEST".into(),
plan_size: 1,
},
);
let req = VerifySessionRequest {
repo_full_name: "owner/repo".into(),
commit_sha: "deadbeef".into(),
tags: vec![VerifySessionTag {
annotation_id: "arta_x".into(),
canon_id: "foo".into(),
version: "v0.1.0".into(),
source_path: "src/x.rs:1".into(),
}],
};
let resp = dispatch_session(&mock, &req).expect("dispatch ok");
assert_eq!(resp.session_id, "01HMTEST");
let posted = mock.posted_requests();
assert_eq!(posted.len(), 1);
assert_eq!(posted[0].tags[0].annotation_id, "arta_x");
}
#[test]
fn dispatch_session_propagates_server_error() {
let mock =
aristo_core::canon_verify::MockVerifyClient::with_post_error(VerifyError::BadRequest {
status: 402,
message: "no_canon_coverage".into(),
});
let req = VerifySessionRequest {
repo_full_name: "o/r".into(),
commit_sha: "x".into(),
tags: vec![],
};
let err = dispatch_session(&mock, &req).unwrap_err();
match err {
VerifyError::BadRequest { status: 402, .. } => {}
other => panic!("expected BadRequest 402, got {other:?}"),
}
}
}