use std::path::{Path, PathBuf};
use std::process::Command as ProcessCommand;
use std::time::Instant;
use anyhow::{Context, Result};
use chrono::Utc;
use clap::{Args, Subcommand};
use rusqlite::{Connection, params};
use serde_json::Value;
use agentstatedeveloper_core::{
FtsFilters, SearchFtsDb, calibration::compute_calibration, compute_trust_score, stale_warning,
};
use crate::config::Config;
#[derive(Debug, Args)]
pub struct ProbeCmd {
#[command(subcommand)]
pub sub: ProbeSub,
}
#[derive(Debug, Subcommand)]
pub enum ProbeSub {
Run(ProbeRunArgs),
Add(ProbeAddArgs),
Bootstrap(ProbeBootstrapArgs),
List(ProbeListArgs),
History(ProbeHistoryArgs),
Reindex(ProbeReindexArgs),
}
#[derive(Debug, Args)]
pub struct ProbeListArgs {
#[arg(long)]
pub json: bool,
#[arg(long)]
pub tag: Option<String>,
}
#[derive(Debug, Args)]
pub struct ProbeBootstrapArgs {
#[arg(long)]
pub force: bool,
#[arg(long, default_value = "5")]
pub top: usize,
}
#[derive(Debug, Args)]
pub struct ProbeReindexArgs {
#[arg(long)]
pub force: bool,
}
#[derive(Debug, Args)]
pub struct ProbeHistoryArgs {
#[arg(long, default_value = "20")]
pub last: usize,
#[arg(long)]
pub tag: Option<String>,
#[arg(long)]
pub json: bool,
}
#[derive(Debug, Args)]
pub struct ProbeRunArgs {
#[arg(long)]
pub json: bool,
#[arg(long)]
pub name: Option<String>,
#[arg(long)]
pub tag: Option<String>,
#[arg(long)]
pub filter: Option<String>,
#[arg(long)]
pub fail_slow: Option<u64>,
#[arg(long)]
pub fail_fast: bool,
#[arg(long, default_value = "0")]
pub jobs: usize,
}
#[derive(Debug, Args)]
pub struct ProbeAddArgs {
#[arg(long)]
pub name: String,
#[arg(long)]
pub command: String,
#[arg(long, num_args = 1..)]
pub args: Vec<String>,
#[arg(long)]
pub assert: Option<String>,
#[arg(long)]
pub description: Option<String>,
}
#[derive(Debug, serde::Deserialize)]
struct ProbeFile {
#[serde(default)]
probe: Vec<ProbeEntry>,
}
#[derive(Debug, serde::Deserialize, Clone)]
struct ProbeEntry {
name: String,
#[serde(default)]
#[allow(dead_code)]
description: String,
command: String,
#[serde(default)]
args: Vec<String>,
#[serde(default)]
tags: Vec<String>,
#[serde(default)]
cwd: Option<String>,
#[serde(default = "toml_empty_table")]
assert: toml::Value,
}
fn toml_empty_table() -> toml::Value {
toml::Value::Table(toml::map::Map::new())
}
pub fn run(cfg: &Config, cmd: ProbeCmd) -> Result<()> {
match cmd.sub {
ProbeSub::Run(args) => run_probes(cfg, args),
ProbeSub::Add(args) => add_probe(cfg, args),
ProbeSub::Bootstrap(args) => bootstrap_probes(cfg, args),
ProbeSub::List(args) => list_probes(cfg, args),
ProbeSub::History(args) => show_history(cfg, args),
ProbeSub::Reindex(args) => reindex_analytics(cfg, args),
}
}
fn list_probes(cfg: &Config, args: ProbeListArgs) -> Result<()> {
let path = probe_file_path(cfg);
if !path.exists() {
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"probe_file": path.display().to_string(),
"exists": false,
"probes": [],
"note": "no probes configured — run `asd probe bootstrap` to generate a starter set",
}))?
);
} else {
println!("no probes file at {}", path.display());
println!("run `asd probe bootstrap` to generate a starter set");
}
return Ok(());
}
let raw =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let pf: ProbeFile =
toml::from_str(&raw).with_context(|| format!("parsing {}", path.display()))?;
let probes: Vec<&ProbeEntry> = pf
.probe
.iter()
.filter(|p| match &args.tag {
Some(t) => p.tags.iter().any(|pt| pt == t),
None => true,
})
.collect();
if args.json {
let entries: Vec<serde_json::Value> = probes
.iter()
.map(|p| {
serde_json::json!({
"name": p.name,
"command": p.command,
"args": p.args,
"tags": p.tags,
})
})
.collect();
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"probe_file": path.display().to_string(),
"exists": true,
"total": pf.probe.len(),
"matched": probes.len(),
"probes": entries,
}))?
);
return Ok(());
}
println!("# probes from {}", path.display());
if probes.is_empty() {
let total = pf.probe.len();
if total == 0 {
println!("(file exists but has no [[probe]] entries)");
} else {
println!("(no probes match --tag filter; {total} total in file)");
}
return Ok(());
}
for p in &probes {
let tags = if p.tags.is_empty() {
String::new()
} else {
format!(" [{}]", p.tags.join(", "))
};
println!("{}{tags}", p.name);
println!(" asd {} {}", p.command, p.args.join(" "));
}
println!();
println!("{} probe(s); run with `asd probe run`", probes.len());
Ok(())
}
fn analytics_path(cfg: &Config) -> PathBuf {
let db_dir = Path::new(&cfg.db_path).parent().unwrap_or(Path::new("."));
db_dir.join(".asd").join("probe-analytics.db")
}
fn open_analytics_db(path: &Path) -> rusqlite::Result<Connection> {
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let conn = Connection::open(path)?;
conn.execute_batch("PRAGMA journal_mode=WAL; PRAGMA synchronous=NORMAL;")?;
conn.execute_batch(
"
CREATE TABLE IF NOT EXISTS probe_runs (
run_id TEXT PRIMARY KEY,
asd_version TEXT NOT NULL,
started_at TEXT NOT NULL,
finished_at TEXT,
probe_file TEXT,
db_state TEXT,
symbol_count INTEGER,
scope TEXT NOT NULL DEFAULT 'all',
total INTEGER NOT NULL,
passed INTEGER NOT NULL,
failed INTEGER NOT NULL,
budget_failed INTEGER NOT NULL DEFAULT 0,
wall_time_ms INTEGER NOT NULL,
worker_count INTEGER,
performance_budget_ms INTEGER,
filter_name TEXT,
filter_tag TEXT
);
CREATE TABLE IF NOT EXISTS probe_results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
run_id TEXT NOT NULL REFERENCES probe_runs(run_id),
name TEXT NOT NULL,
command TEXT,
assertion TEXT,
tags TEXT,
passed INTEGER NOT NULL DEFAULT 1,
slow INTEGER NOT NULL DEFAULT 0,
timed_out INTEGER NOT NULL DEFAULT 0,
duration_ms INTEGER NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_pr_run ON probe_results(run_id);
CREATE INDEX IF NOT EXISTS idx_pr_name ON probe_results(name);
CREATE INDEX IF NOT EXISTS idx_pr_trend ON probe_results(name, duration_ms);
CREATE INDEX IF NOT EXISTS idx_runs_ver ON probe_runs(asd_version);
CREATE INDEX IF NOT EXISTS idx_runs_at ON probe_runs(started_at);
",
)?;
Ok(conn)
}
fn scope_from_record(record: &Value) -> String {
match (
record.get("filter_name").and_then(Value::as_str),
record.get("filter_tag").and_then(Value::as_str),
) {
(Some(n), _) => format!("name:{}", n),
(_, Some(t)) => format!("tag:{}", t),
_ => "all".to_string(),
}
}
fn insert_run_to_analytics(conn: &Connection, record: &Value) {
let run_id = match record.get("started_at").and_then(Value::as_str) {
Some(s) => s,
None => return,
};
let exists: bool = conn
.query_row(
"SELECT 1 FROM probe_runs WHERE run_id=?1",
params![run_id],
|_| Ok(true),
)
.unwrap_or(false);
if exists {
return;
}
let scope = scope_from_record(record);
let res = conn.execute(
"INSERT OR IGNORE INTO probe_runs
(run_id, asd_version, started_at, finished_at, probe_file, db_state, symbol_count,
scope, total, passed, failed, budget_failed, wall_time_ms, worker_count,
performance_budget_ms, filter_name, filter_tag)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)",
params![
run_id,
record
.get("asd_version")
.and_then(Value::as_str)
.unwrap_or(""),
record
.get("started_at")
.and_then(Value::as_str)
.unwrap_or(""),
record.get("finished_at").and_then(Value::as_str),
record.get("probe_file").and_then(Value::as_str),
record.get("db_state").and_then(Value::as_str),
record.get("symbol_count").and_then(Value::as_i64),
scope,
record.get("total").and_then(Value::as_i64).unwrap_or(0),
record.get("passed").and_then(Value::as_i64).unwrap_or(0),
record.get("failed").and_then(Value::as_i64).unwrap_or(0),
record
.get("budget_failed")
.and_then(Value::as_bool)
.map(|b| b as i64)
.unwrap_or(0),
record
.get("wall_time_ms")
.and_then(Value::as_i64)
.unwrap_or(0),
record.get("worker_count").and_then(Value::as_i64),
record.get("performance_budget_ms").and_then(Value::as_i64),
record.get("filter_name").and_then(Value::as_str),
record.get("filter_tag").and_then(Value::as_str),
],
);
if res.is_err() {
return;
}
if let Some(probes) = record.get("probes").and_then(Value::as_array) {
for p in probes {
let tags_str = p
.get("tags")
.map(|v| v.to_string())
.unwrap_or_else(|| "[]".to_string());
let _ = conn.execute(
"INSERT INTO probe_results
(run_id, name, command, assertion, tags, passed, slow, timed_out, duration_ms)
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
params![
run_id,
p.get("name").and_then(Value::as_str).unwrap_or(""),
p.get("command").and_then(Value::as_str),
p.get("assertion").and_then(Value::as_str),
tags_str,
p.get("passed")
.and_then(Value::as_bool)
.map(|b| b as i64)
.unwrap_or(1),
p.get("slow")
.and_then(Value::as_bool)
.map(|b| b as i64)
.unwrap_or(0),
p.get("timed_out")
.and_then(Value::as_bool)
.map(|b| b as i64)
.unwrap_or(0),
p.get("duration_ms").and_then(Value::as_i64).unwrap_or(0),
],
);
}
}
}
fn probe_file_path(cfg: &Config) -> PathBuf {
let db_dir = Path::new(&cfg.db_path).parent().unwrap_or(Path::new("."));
db_dir.join(".asd").join("probes.toml")
}
fn history_path(cfg: &Config) -> PathBuf {
let db_dir = Path::new(&cfg.db_path).parent().unwrap_or(Path::new("."));
db_dir.join(".asd").join("probe-history.jsonl")
}
const MAX_HISTORY: usize = 500;
fn append_history(cfg: &Config, record: &Value) {
let path = history_path(cfg);
if let Some(parent) = path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let new_line = match serde_json::to_string(record) {
Ok(s) => s,
Err(_) => return,
};
let existing = std::fs::read_to_string(&path).unwrap_or_default();
let mut lines: Vec<String> = existing
.lines()
.filter(|l| !l.trim().is_empty())
.map(|l| l.to_string())
.collect();
lines.push(new_line);
if lines.len() > MAX_HISTORY {
lines.drain(0..lines.len() - MAX_HISTORY);
}
let content = lines.join("\n") + "\n";
let _ = std::fs::write(&path, content);
}
fn run_probes(cfg: &Config, args: ProbeRunArgs) -> Result<()> {
let path = probe_file_path(cfg);
if !path.exists() {
anyhow::bail!(
"no probes file found at {}\nRun `asd probe add` to create probes.",
path.display()
);
}
let raw =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let pf: ProbeFile =
toml::from_str(&raw).with_context(|| format!("parsing {}", path.display()))?;
let probes: Vec<&ProbeEntry> = pf
.probe
.iter()
.filter(|p| {
if let Some(ref n) = args.name {
if p.name != *n {
return false;
}
}
if let Some(ref t) = args.tag {
if !p.tags.iter().any(|tag| tag == t) {
return false;
}
}
if let Some(ref f) = args.filter {
if !p.name.contains(f.as_str()) {
return false;
}
}
true
})
.collect();
if probes.is_empty() {
if args.name.is_some() || args.tag.is_some() || args.filter.is_some() {
let mut reason = Vec::new();
if let Some(ref n) = args.name {
reason.push(format!("name={:?}", n));
}
if let Some(ref t) = args.tag {
reason.push(format!("tag={:?}", t));
}
if let Some(ref f) = args.filter {
reason.push(format!("filter={:?}", f));
}
println!("No probes matched filter(s): {}.", reason.join(", "));
} else {
println!("No probes to run.");
}
return Ok(());
}
let n = probes.len();
const AUTO_JOBS_CAP: usize = 6;
let jobs = if args.jobs == 0 {
std::thread::available_parallelism()
.map(|p| p.get())
.unwrap_or(4)
.min(AUTO_JOBS_CAP)
.min(n)
} else {
args.jobs.min(n).max(1)
};
if !args.json && jobs > 1 {
eprintln!("Running {} probe(s) [{} parallel]…", n, jobs);
}
let db_state = {
let warn = stale_warning(&cfg.db_path, 3600);
match warn {
None => "fresh",
Some(ref s) if s.contains("FTS search index failed") => "symbols-fresh/fts-stale",
Some(_) => "stale",
}
};
let symbol_count: Option<u64> = SearchFtsDb::open(&cfg.db_path)
.ok()
.map(|fts| fts.symbol_count() as u64);
let trust = compute_trust_score(&cfg.db_path);
let started_at = Utc::now().to_rfc3339();
let wall_start = Instant::now();
let cacheable = |p: &&ProbeEntry| {
p.command != "hydrate-roundtrip"
&& p.command != "hydrate_roundtrip"
&& p.command != "workflow"
};
let probe_keys: Vec<Option<String>> = probes
.iter()
.map(|p| {
if cacheable(&p) {
Some(command_cache_key(p, &cfg.db_path))
} else {
None
}
})
.collect();
let mut key_to_representative: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for (i, key_opt) in probe_keys.iter().enumerate() {
if let Some(key) = key_opt {
key_to_representative.entry(key.clone()).or_insert(i);
}
}
let n_unique = key_to_representative.len();
let n_cacheable = probe_keys.iter().filter(|k| k.is_some()).count();
let n_deduped = n_cacheable.saturating_sub(n_unique);
if !args.json && n_deduped > 0 {
eprintln!(
" {} duplicate command invocation(s) eliminated by cache ({} unique → {} probes)",
n_deduped, n_unique, n_cacheable
);
}
let command_cache: std::collections::HashMap<String, CachedOutput> = {
let unique_probes: Vec<(String, &ProbeEntry)> = key_to_representative
.iter()
.map(|(key, &idx)| (key.clone(), probes[idx]))
.collect();
let mut map: std::collections::HashMap<String, CachedOutput> =
std::collections::HashMap::with_capacity(unique_probes.len());
std::thread::scope(|scope| {
for chunk in unique_probes.chunks(jobs) {
let handles: Vec<_> = chunk
.iter()
.map(|(key, probe)| {
scope.spawn(move || (key.clone(), run_command_only(cfg, probe)))
})
.collect();
for h in handles {
let (k, v) = h.join().unwrap_or_else(|_| {
(
"__panic__".to_string(),
CachedOutput {
json: None,
stdout_raw: String::new(),
stderr: String::new(),
success: false,
duration_ms: 0,
timed_out: false,
exec_error: Some(
"thread panicked during command execution".to_string(),
),
},
)
});
map.insert(k, v);
}
}
});
map
};
let fail_fast_flag = std::sync::atomic::AtomicBool::new(false);
let fail_fast = args.fail_fast;
let fail_slow = args.fail_slow;
let show_json = args.json;
let mut results: Vec<ProbeResult> = Vec::with_capacity(n);
std::thread::scope(|scope| {
for chunk_start in (0..probes.len()).step_by(jobs) {
if fail_fast_flag.load(std::sync::atomic::Ordering::Relaxed) {
break;
}
let chunk_end = (chunk_start + jobs).min(probes.len());
let chunk = &probes[chunk_start..chunk_end];
let ff = &fail_fast_flag;
let handles: Vec<_> = chunk
.iter()
.enumerate()
.map(|(j, probe)| {
let global_idx = chunk_start + j;
let cache_key = probe_keys[global_idx].clone();
let cached = cache_key.as_ref().and_then(|k| command_cache.get(k));
scope.spawn(move || {
if ff.load(std::sync::atomic::Ordering::Relaxed) {
return None;
}
let result = if let Some(c) = cached {
run_assertion_against(probe, c)
} else {
execute_probe(cfg, probe)
};
if result.error.is_some() && fail_fast {
ff.store(true, std::sync::atomic::Ordering::Relaxed);
}
Some((global_idx, result))
})
})
.collect();
let mut chunk_results: Vec<(usize, ProbeResult)> = handles
.into_iter()
.filter_map(|h| h.join().unwrap_or(None))
.collect();
chunk_results.sort_by_key(|(i, _)| *i);
for (_, result) in &chunk_results {
let is_fail = result.error.is_some();
let is_slow = fail_slow.map_or(false, |ms| result.duration_ms > ms as u128);
if !show_json {
let status = if is_fail {
"FAIL"
} else if is_slow {
"SLOW"
} else {
"PASS"
};
let ms = result.duration_ms;
if is_fail {
println!("{:<5} {} ({}ms)", status, result.name, ms);
println!(" {}", result.error.as_deref().unwrap_or(""));
if let Some(ref payload) = result.debug_payload_summary {
println!(" debug: {}", payload);
}
} else {
println!("{:<5} {} ({}ms)", status, result.name, ms);
}
}
}
results.extend(chunk_results.into_iter().map(|(_, r)| r));
}
});
let passed = results.iter().filter(|r| r.error.is_none()).count();
let failed = results.iter().filter(|r| r.error.is_some()).count();
let slow_violations: Vec<&ProbeResult> = if let Some(threshold_ms) = args.fail_slow {
results
.iter()
.filter(|r| r.duration_ms > threshold_ms as u128)
.collect()
} else {
Vec::new()
};
let fail_slow = args.fail_slow;
let result_to_json = |r: &ProbeResult| {
let is_slow = fail_slow.map_or(false, |ms| r.duration_ms > ms as u128);
serde_json::json!({
"name": r.name,
"command": r.command,
"assertion": r.assertion,
"tags": r.tags,
"passed": r.error.is_none(),
"slow": is_slow,
"timed_out": r.timed_out,
"duration_ms": r.duration_ms,
"error": r.error,
"debug_payload": r.debug_payload,
})
};
let dedup_db_path = cfg.db_path.clone();
let mut by_cache: std::collections::HashMap<String, (u128, Vec<&ProbeResult>)> =
std::collections::HashMap::new();
for r in &results {
let key = format!(
"{}|{}",
r.command,
r.duration_ms,
);
let entry = by_cache.entry(key).or_insert((r.duration_ms, Vec::new()));
entry.1.push(r);
}
let _ = dedup_db_path;
let mut deduped: Vec<(u128, Vec<&ProbeResult>)> = by_cache.into_values().collect();
deduped.sort_by(|a, b| b.0.cmp(&a.0));
let slowest_top5: Vec<Value> = deduped
.iter()
.take(5)
.map(|(duration_ms, group)| {
if group.len() == 1 {
result_to_json(group[0])
} else {
let names: Vec<&str> = group.iter().map(|r| r.name.as_str()).collect();
let mut v = result_to_json(group[0]);
if let Some(obj) = v.as_object_mut() {
obj.insert("probe_names".to_string(), serde_json::json!(names));
obj.insert(
"cache_shared_count".to_string(),
serde_json::json!(group.len()),
);
obj.insert("duration_ms".to_string(), serde_json::json!(duration_ms));
}
v
}
})
.collect();
let wall_time_ms = wall_start.elapsed().as_millis();
let finished_at = Utc::now().to_rfc3339();
let budget_failed = !slow_violations.is_empty();
let slow_violation_names: Vec<&str> = slow_violations.iter().map(|r| r.name.as_str()).collect();
if args.json {
let json_results: Vec<Value> = results.iter().map(|r| result_to_json(r)).collect();
let calibration_obs: Vec<(String, bool)> = results
.iter()
.filter_map(|r| {
let level = r.calibration_signal.as_deref()?;
Some((level.to_string(), r.error.is_none()))
})
.collect();
let calibration = compute_calibration(calibration_obs);
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"asd_version": env!("CARGO_PKG_VERSION"),
"started_at": started_at,
"finished_at": finished_at,
"probe_file": probe_file_path(cfg),
"db_path": cfg.db_path,
"total": results.len(),
"passed": passed,
"failed": failed,
"budget_failed": budget_failed,
"wall_time_ms": wall_time_ms,
"worker_count": jobs,
"db_state": db_state,
"symbol_count": symbol_count,
"performance_budget_ms": args.fail_slow,
"slow_violations": slow_violation_names,
"slowest": slowest_top5,
"trust": trust.to_json(),
"calibration": calibration,
"results": json_results,
}))?
);
} else {
println!(
"\n{} probe(s): {} passed, {} failed [{} workers, {}ms wall]",
results.len(),
passed,
failed,
jobs,
wall_time_ms
);
if !slow_violations.is_empty() {
let threshold_ms = args.fail_slow.unwrap();
println!("SLOW violations (>{threshold_ms} ms):");
for r in &slow_violations {
println!(" {} ({}ms)", r.name, r.duration_ms);
}
}
if !slowest_top5.is_empty() && results.len() > 1 {
println!("Slowest probes:");
for entry in &slowest_top5 {
println!(
" {} ({}ms)",
entry["name"].as_str().unwrap_or("?"),
entry["duration_ms"].as_u64().unwrap_or(0)
);
}
}
}
let probes_compact: Vec<Value> = results
.iter()
.map(|r| {
let is_slow = fail_slow.map_or(false, |ms| r.duration_ms > ms as u128);
serde_json::json!({
"name": r.name,
"command": r.command,
"assertion": r.assertion,
"tags": r.tags,
"passed": r.error.is_none(),
"slow": is_slow,
"timed_out": r.timed_out,
"duration_ms": r.duration_ms,
})
})
.collect();
let history_record = serde_json::json!({
"kind": "probe_run",
"asd_version": env!("CARGO_PKG_VERSION"),
"started_at": started_at,
"finished_at": finished_at,
"probe_file": probe_file_path(cfg),
"db_state": db_state,
"symbol_count": symbol_count,
"total": results.len(),
"passed": passed,
"failed": failed,
"budget_failed": budget_failed,
"wall_time_ms": wall_time_ms,
"worker_count": jobs,
"performance_budget_ms": args.fail_slow,
"filter_name": args.name,
"filter_tag": args.tag,
"slowest": slowest_top5,
"probes": probes_compact,
});
append_history(cfg, &history_record);
if let Ok(conn) = open_analytics_db(&analytics_path(cfg)) {
insert_run_to_analytics(&conn, &history_record);
}
if failed > 0 || !slow_violations.is_empty() {
std::process::exit(1);
}
Ok(())
}
struct ProbeResult {
name: String,
command: String,
assertion: String, tags: Vec<String>,
duration_ms: u128,
timed_out: bool, error: Option<String>,
debug_payload: Option<Value>,
debug_payload_summary: Option<String>,
calibration_signal: Option<String>,
}
fn extract_calibration_signal(json: Option<&Value>) -> Option<String> {
json?
.get("uncertainty")?
.get("level")?
.as_str()
.map(|s| s.to_string())
}
#[allow(dead_code)] #[derive(Clone)]
struct CachedOutput {
json: Option<Value>,
stdout_raw: String,
stderr: String,
success: bool,
duration_ms: u128,
timed_out: bool,
exec_error: Option<String>,
}
fn command_cache_key(probe: &ProbeEntry, db_path: &std::path::Path) -> String {
let cwd_part = probe.cwd.as_deref().unwrap_or("");
format!(
"{}|{}|{}|{}",
probe.command,
probe.args.join("\x00"),
cwd_part,
db_path.display()
)
}
fn run_command_only(cfg: &Config, probe: &ProbeEntry) -> CachedOutput {
let start = Instant::now();
let asd_bin_path: PathBuf = {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("asd"));
if exe.exists() {
exe
} else {
PathBuf::from("asd")
}
};
let asd_bin = asd_bin_path.to_string_lossy().into_owned();
let subcmd = match probe.command.as_str() {
"prepare-change" | "prepare_change" => "prepare-change",
"annotate-commit" | "annotate_commit" => "annotate-commit",
"task-close" | "task_close" => "task-close",
other => other,
};
let probe_dir = std::path::Path::new(&cfg.db_path)
.parent()
.unwrap_or(std::path::Path::new("."))
.to_path_buf();
let work_dir = probe.cwd.as_ref().map(PathBuf::from).unwrap_or(probe_dir);
fn shell_quote_inner(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
let db_path_str = cfg.db_path.to_string_lossy().into_owned();
let mut shell_cmd = format!(
"cd {} && {} --db {} {}",
shell_quote_inner(work_dir.to_string_lossy().as_ref()),
shell_quote_inner(&asd_bin),
shell_quote_inner(&db_path_str),
shell_quote_inner(subcmd),
);
for arg in &probe.args {
shell_cmd.push(' ');
shell_cmd.push_str(&shell_quote_inner(arg));
}
let mut cmd = ProcessCommand::new("/bin/sh");
cmd.arg("-c").arg(&shell_cmd);
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
return CachedOutput {
json: None,
stdout_raw: String::new(),
stderr: String::new(),
success: false,
duration_ms: start.elapsed().as_millis(),
timed_out: false,
exec_error: Some(format!(
"failed to execute asd ({:?}) via sh: {}",
asd_bin, e
)),
};
}
};
let stdout_raw = String::from_utf8_lossy(&output.stdout).into_owned();
let stderr = String::from_utf8_lossy(&output.stderr).into_owned();
let json: Option<Value> = serde_json::from_str(&stdout_raw).ok();
let duration_ms = start.elapsed().as_millis();
CachedOutput {
json,
stdout_raw,
stderr,
success: output.status.success(),
duration_ms,
timed_out: false,
exec_error: None,
}
}
fn run_assertion_against(probe: &ProbeEntry, cached: &CachedOutput) -> ProbeResult {
let assertion_kind = probe
.assert
.as_table()
.and_then(|m| m.get("kind"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if let Some(ref exec_err) = cached.exec_error {
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: cached.duration_ms,
timed_out: false,
error: Some(exec_err.clone()),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: cached
.json
.as_ref()
.and_then(|j| extract_calibration_signal(Some(j))),
};
}
if !cached.success && cached.json.is_none() {
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: cached.duration_ms,
timed_out: false,
error: Some(format!("command failed: {}", cached.stderr.trim())),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: cached
.json
.as_ref()
.and_then(|j| extract_calibration_signal(Some(j))),
};
}
let json = match cached.json.as_ref() {
Some(v) => v,
None => {
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: cached.duration_ms,
timed_out: false,
error: Some("command output was not valid JSON".to_string()),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: cached
.json
.as_ref()
.and_then(|j| extract_calibration_signal(Some(j))),
};
}
};
match eval_assert(&probe.assert, json) {
Ok(()) => ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: cached.duration_ms,
timed_out: false,
error: None,
debug_payload: None,
debug_payload_summary: None,
calibration_signal: cached
.json
.as_ref()
.and_then(|j| extract_calibration_signal(Some(j))),
},
Err(msg) => {
let summary = summarize_debug_payload(json);
ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: cached.duration_ms,
timed_out: false,
error: Some(msg),
debug_payload: Some(json.clone()),
debug_payload_summary: summary,
calibration_signal: extract_calibration_signal(Some(json)),
}
}
}
}
fn execute_probe(cfg: &Config, probe: &ProbeEntry) -> ProbeResult {
let start = Instant::now();
let assertion_kind = probe
.assert
.as_table()
.and_then(|m| m.get("kind"))
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if probe.command.as_str() == "hydrate-roundtrip"
|| probe.command.as_str() == "hydrate_roundtrip"
{
let result = run_hydrate_roundtrip_probe(cfg);
let duration_ms = start.elapsed().as_millis();
return match result {
Ok(msg) => ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: "hydrate_roundtrip".to_string(),
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: None,
debug_payload: Some(serde_json::json!({ "message": msg })),
debug_payload_summary: Some(msg),
calibration_signal: None, },
Err(msg) => ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: "hydrate_roundtrip".to_string(),
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: Some(msg.clone()),
debug_payload: Some(serde_json::json!({ "error": msg })),
debug_payload_summary: Some(msg),
calibration_signal: None,
},
};
}
let asd_bin_path: PathBuf = {
let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("asd"));
if exe.exists() {
exe
} else {
PathBuf::from("asd")
}
};
let asd_bin = asd_bin_path.to_string_lossy().into_owned();
let subcmd = match probe.command.as_str() {
"prepare-change" | "prepare_change" => "prepare-change",
"annotate-commit" | "annotate_commit" => "annotate-commit",
"task-close" | "task_close" => "task-close",
other => other,
};
let probe_dir = Path::new(&cfg.db_path)
.parent()
.unwrap_or(Path::new("."))
.to_path_buf();
let work_dir = probe.cwd.as_ref().map(PathBuf::from).unwrap_or(probe_dir);
fn shell_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', r"'\''"))
}
let db_path_str = cfg.db_path.to_string_lossy().into_owned();
let mut shell_cmd = format!(
"cd {} && {} --db {} {}",
shell_quote(work_dir.to_string_lossy().as_ref()),
shell_quote(&asd_bin),
shell_quote(&db_path_str),
shell_quote(subcmd),
);
for arg in &probe.args {
shell_cmd.push(' ');
shell_cmd.push_str(&shell_quote(arg));
}
let mut cmd = ProcessCommand::new("/bin/sh");
cmd.arg("-c").arg(&shell_cmd);
let output = match cmd.output() {
Ok(o) => o,
Err(e) => {
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms: start.elapsed().as_millis(),
timed_out: false,
error: Some(format!(
"failed to execute asd ({:?}) via sh: {}",
asd_bin, e
)),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: None, };
}
};
let stdout = String::from_utf8_lossy(&output.stdout);
let parsed: Option<Value> = serde_json::from_str(&stdout).ok();
let duration_ms = start.elapsed().as_millis();
if !output.status.success() && parsed.is_none() {
let stderr = String::from_utf8_lossy(&output.stderr);
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: Some(format!(
"command exited with {}: {}",
output.status,
stderr.trim()
)),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: None, };
}
let json = match parsed {
Some(v) => v,
None => {
return ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: Some("command output was not valid JSON".to_string()),
debug_payload: None,
debug_payload_summary: None,
calibration_signal: None, };
}
};
match eval_assert(&probe.assert, &json) {
Ok(()) => ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: None,
debug_payload: None,
debug_payload_summary: None,
calibration_signal: extract_calibration_signal(Some(&json)),
},
Err(msg) => {
let summary = summarize_debug_payload(&json);
let cal = extract_calibration_signal(Some(&json));
ProbeResult {
name: probe.name.clone(),
command: probe.command.clone(),
assertion: assertion_kind,
tags: probe.tags.clone(),
duration_ms,
timed_out: false,
error: Some(msg),
debug_payload: Some(json),
debug_payload_summary: summary,
calibration_signal: cal,
}
}
}
}
fn run_hydrate_roundtrip_probe(_cfg: &Config) -> Result<String, String> {
use agentstatedeveloper_core::{
AsgIndexStore, AsgLedgerStore, Engine, IndexStore, LedgerStore, hydrate_from_dir,
schema::{Author, AuthorKind, LedgerEntry, LedgerKind, Position, Symbol, SymbolKind},
symbol_fingerprint, sync_to_dir,
};
let tmp = tempfile::TempDir::new().map_err(|e| format!("failed to create temp dir: {e}"))?;
let db_a = tmp.path().join("roundtrip_a.db");
let db_b = tmp.path().join("roundtrip_b.db");
let engine_a =
Engine::open_sqlite(&db_a).map_err(|e| format!("failed to open temp Engine A: {e}"))?;
let qname = "asd::probe::roundtrip_sentinel";
let file = "__asd_probe__/roundtrip.rs";
let sym = Symbol {
symbol_id: "asd-probe-roundtrip-sym".to_string(),
symbol_fp: symbol_fingerprint(qname),
qname: qname.to_string(),
kind: SymbolKind::Function,
file: file.to_string(),
language: "rust".to_string(),
start: Position { line: 1, col: 0 },
end: Position { line: 1, col: 0 },
doc: Some("ASD hydrate roundtrip probe sentinel symbol".to_string()),
signature: Some("fn roundtrip_sentinel()".to_string()),
};
let store_a = AsgIndexStore::from_engine(&engine_a);
store_a
.put_symbol(&engine_a.ref_name, &sym, "asd-roundtrip-probe")
.map_err(|e| format!("failed to write sentinel symbol: {e}"))?;
let sentinel_text = "asd-hydrate-roundtrip-proof";
let author = Author {
kind: AuthorKind::Agent,
id: "asd-roundtrip-probe".to_string(),
};
let mut entry = LedgerEntry::new(
"asd-probe-roundtrip-sym",
LedgerKind::Decision,
sentinel_text,
author,
);
entry.tags = vec!["trust-probe".to_string(), "probe-roundtrip".to_string()];
let ledger_a = AsgLedgerStore::new(&engine_a.repo);
ledger_a
.append_entry(&engine_a.ref_name, &entry, "asd-roundtrip-probe")
.map_err(|e| format!("failed to write sentinel ledger entry: {e}"))?;
let sidecar_root = tmp.path();
sync_to_dir(&engine_a.repo, &engine_a.ref_name, sidecar_root)
.map_err(|e| format!("sync_to_dir failed: {e}"))?;
let engine_b =
Engine::open_sqlite(&db_b).map_err(|e| format!("failed to open temp Engine B: {e}"))?;
hydrate_from_dir(
&engine_b.repo,
&engine_b.ref_name,
sidecar_root,
"asd-roundtrip-probe",
)
.map_err(|e| format!("hydrate_from_dir failed: {e}"))?;
let ledger_b = AsgLedgerStore::new(&engine_b.repo);
let entries = ledger_b
.list_entries(&engine_b.ref_name, "asd-probe-roundtrip-sym")
.map_err(|e| format!("failed to read ledger from Engine B: {e}"))?;
let survived = entries.iter().any(|e| e.summary == sentinel_text);
if survived {
Ok(format!(
"roundtrip OK — sentinel {:?} survived sync→hydrate cycle ({} entries in B)",
sentinel_text,
entries.len()
))
} else {
Err(format!(
"roundtrip FAILED — sentinel {:?} not found in Engine B after hydrate \
({} entries present: {:?})",
sentinel_text,
entries.len(),
entries
.iter()
.map(|e| e.summary.as_str())
.collect::<Vec<_>>()
))
}
}
fn eval_assert(assert: &toml::Value, output: &Value) -> Result<(), String> {
let map = match assert.as_table() {
Some(m) => m,
None => return Ok(()), };
let kind = map.get("kind").and_then(|v| v.as_str()).unwrap_or("");
let empty_arr: Vec<Value> = Vec::new();
match kind {
"file_not_in_key" => {
let key = str_field(map, "key")?;
let field = str_field(map, "field")?;
let value = str_field(map, "value")?;
let arr = dot_path(output, key)
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let found: Vec<&str> = arr
.iter()
.filter_map(|item| item.get(field).and_then(|v| v.as_str()))
.filter(|s| s.contains(value))
.collect();
if !found.is_empty() {
Err(format!(
"file_not_in_key: found {:?} in {}[].{} (should be absent)",
found, key, field
))
} else {
Ok(())
}
}
"file_in_key" => {
let key = str_field(map, "key")?;
let field = str_field(map, "field")?;
let value = str_field(map, "value")?;
let arr = dot_path(output, key)
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let found = arr.iter().any(|item| {
item.get(field)
.and_then(|v| v.as_str())
.map_or(false, |s| s.contains(value))
});
if found {
Ok(())
} else {
Err(format!(
"file_in_key: no item in {}[].{} contains {:?}",
key, field, value
))
}
}
"qname_rank_lte" => eval_qname_rank_lte(map, output),
"qname_rank_eq" => {
let fragment = str_field(map, "fragment")?;
let exact_rank = u64_field(map, "exact_rank")?;
let results = output
.get("results")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let pos = results.iter().position(|r| {
r.get("qname")
.and_then(|v| v.as_str())
.map_or(false, |q| q.contains(fragment))
});
match pos {
Some(idx) if (idx as u64 + 1) == exact_rank => Ok(()),
Some(idx) => Err(format!(
"qname_rank_eq: {:?} found at rank {}, expected exactly {}",
fragment,
idx + 1,
exact_rank
)),
None => Err(format!(
"qname_rank_eq: no result qname contains {:?} (checked {} results)",
fragment,
results.len()
)),
}
}
"result_count_lte" => {
let max = u64_field(map, "max")?;
let results = output
.get("results")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let n = results.len() as u64;
if n <= max {
Ok(())
} else {
Err(format!("result_count_lte: got {} results (max={})", n, max))
}
}
"cluster_winner_kind_not" => eval_cluster_winner_kind_not(map, output),
"cluster_winner_qname_contains" => eval_cluster_winner_qname_contains(map, output),
"no_duplicate_summaries" => {
let entries = output
.get("suggested_entries")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let mut seen_global: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
for e in entries {
if let Some(s) = e.get("summary").and_then(|v| v.as_str()) {
*seen_global.entry(s.to_string()).or_insert(0) += 1;
}
}
let dups: Vec<&String> = seen_global
.iter()
.filter(|(_, count)| **count > 1)
.map(|(s, _)| s)
.collect();
if !dups.is_empty() {
return Err(format!(
"no_duplicate_summaries: duplicate summary text: {:?}",
dups.iter().take(3).map(|s| s.as_str()).collect::<Vec<_>>()
));
}
Ok(())
}
"boosted_outranked_contains" => {
let fragment = str_field(map, "fragment")?;
let outranked = output
.get("boosted_outranked")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let found = outranked.iter().any(|s| {
s.as_str().map_or(false, |q| {
q.to_lowercase().contains(&fragment.to_lowercase())
})
});
if found {
Ok(())
} else {
Err(format!(
"boosted_outranked_contains: {:?} not in boosted_outranked; got {:?}",
fragment,
outranked
.iter()
.filter_map(|s| s.as_str())
.collect::<Vec<_>>()
))
}
}
"qname_not_in_results" => {
let fragment = str_field(map, "fragment")?;
let results = output
.get("results")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let hit = results.iter().find(|r| {
r.get("qname").and_then(|v| v.as_str()).map_or(false, |q| {
q.to_lowercase().contains(&fragment.to_lowercase())
})
});
match hit {
Some(r) => {
let qname = r.get("qname").and_then(|v| v.as_str()).unwrap_or("?");
Err(format!(
"qname_not_in_results: {:?} is present in results (expected suppressed)",
qname
))
}
None => Ok(()),
}
}
"ambiguous_terms_nonempty" => {
let terms = output
.get("ambiguous_terms")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
if terms.is_empty() {
Err("ambiguous_terms_nonempty: ambiguous_terms is empty — query may be too specific or detection not firing".to_string())
} else {
Ok(())
}
}
"scoped_suggestions_nonempty" => {
let suggestions = output
.get("scoped_suggestions")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
if suggestions.is_empty() {
Err("scoped_suggestions_nonempty: scoped_suggestions is empty — no narrowing hints emitted".to_string())
} else {
Ok(())
}
}
"scoped_suggestions_contains" => {
let fragment = str_field(map, "fragment")?;
let suggestions = output
.get("scoped_suggestions")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let found = suggestions.iter().any(|s| {
s.as_str().map_or(false, |t| {
t.to_lowercase().contains(&fragment.to_lowercase())
})
});
if found {
Ok(())
} else {
Err(format!(
"scoped_suggestions_contains: no suggestion contains {:?}; got {:?}",
fragment,
suggestions
.iter()
.filter_map(|s| s.as_str())
.collect::<Vec<_>>()
))
}
}
"uncertainty_level_lte" => {
let max_level = str_field(map, "max_level")?;
let level_rank = |l: &str| match l {
"low" => 0u8,
"medium" => 1,
"high" => 2,
"critical" => 3,
_ => 4,
};
let actual_level = output
.get("uncertainty")
.and_then(|u| u.get("level"))
.and_then(Value::as_str)
.unwrap_or("low");
if level_rank(actual_level) <= level_rank(max_level) {
Ok(())
} else {
Err(format!(
"uncertainty_level_lte: uncertainty.level = {:?} (expected <= {:?})",
actual_level, max_level
))
}
}
"uncertainty_reason_contains" => {
let code = str_field(map, "code")?;
let reasons = output
.get("uncertainty")
.and_then(|u| u.get("reasons"))
.and_then(Value::as_array)
.unwrap_or(&empty_arr);
let found = reasons.iter().any(|r| {
r.get("code")
.and_then(Value::as_str)
.map_or(false, |c| c == code)
});
if found {
Ok(())
} else {
let codes: Vec<&str> = reasons
.iter()
.filter_map(|r| r.get("code").and_then(Value::as_str))
.collect();
Err(format!(
"uncertainty_reason_contains: reason code {:?} not found; got {:?}",
code, codes
))
}
}
"uncertainty_action_eq" => {
let action = str_field(map, "action")?;
let actual = output
.get("uncertainty")
.and_then(|u| u.get("recommended_action"))
.and_then(Value::as_str)
.unwrap_or("");
if actual == action {
Ok(())
} else {
Err(format!(
"uncertainty_action_eq: recommended_action = {:?} (expected {:?})",
actual, action
))
}
}
"recovery_suggestions_nonempty" => {
let suggestions = output
.get("uncertainty")
.and_then(|u| u.get("recovery_suggestions"))
.and_then(Value::as_array)
.unwrap_or(&empty_arr);
if !suggestions.is_empty() {
Ok(())
} else {
Err(
"recovery_suggestions_nonempty: uncertainty.recovery_suggestions is empty"
.to_string(),
)
}
}
"recovery_suggestion_estimated" => {
let strength = str_field(map, "strength")?;
let suggestions = output
.get("uncertainty")
.and_then(|u| u.get("recovery_suggestions"))
.and_then(Value::as_array)
.unwrap_or(&empty_arr);
let found = suggestions.iter().any(|s| {
s.get("estimated_recovery")
.and_then(Value::as_str)
.map_or(false, |e| e == strength)
});
if found {
Ok(())
} else {
let found_strengths: Vec<&str> = suggestions
.iter()
.filter_map(|s| s.get("estimated_recovery").and_then(Value::as_str))
.collect();
Err(format!(
"recovery_suggestion_estimated: no suggestion with estimated_recovery {:?}; got {:?}",
strength, found_strengths
))
}
}
"feedback_summary_gte" => {
let field = str_field(map, "field")?;
let min_value = u64_field(map, "min_value")?;
let actual = output
.get("feedback_summary")
.and_then(|s| s.get(field))
.and_then(Value::as_u64)
.unwrap_or(0);
if actual >= min_value {
Ok(())
} else {
Err(format!(
"feedback_summary_gte: feedback_summary.{} = {} (expected >= {})",
field, actual, min_value
))
}
}
"feedback_summary_eq" => {
let field = str_field(map, "field")?;
let expected = u64_field(map, "value")?;
let actual = output
.get("feedback_summary")
.and_then(|s| s.get(field))
.and_then(Value::as_u64)
.unwrap_or(0);
if actual == expected {
Ok(())
} else {
Err(format!(
"feedback_summary_eq: feedback_summary.{} = {} (expected {})",
field, actual, expected
))
}
}
"feedback_rules_contains" => {
let rule = str_field(map, "rule")?;
let rules = output
.get("feedback_summary")
.and_then(|s| s.get("rules_applied"))
.and_then(Value::as_array)
.cloned()
.unwrap_or_default();
let found = rules
.iter()
.any(|r| r.as_str().map_or(false, |s| s == rule));
if found {
Ok(())
} else {
Err(format!(
"feedback_rules_contains: rule {:?} not in rules_applied {:?}",
rule,
rules.iter().filter_map(|r| r.as_str()).collect::<Vec<_>>()
))
}
}
"field_gte" => {
let field = str_field(map, "field")?;
let min_val: f64 = map
.get("min_value")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.ok_or_else(|| format!("assertion missing required numeric field \"min_value\""))?;
let actual = dot_path(output, field)
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
if actual >= min_val {
Ok(())
} else {
Err(format!(
"field_gte: {field} = {actual} (expected >= {min_val})"
))
}
}
"field_lte" => {
let field = str_field(map, "field")?;
let max_val: f64 = map
.get("max_value")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.ok_or_else(|| format!("assertion missing required numeric field \"max_value\""))?;
let actual = dot_path(output, field)
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
if actual <= max_val {
Ok(())
} else {
Err(format!(
"field_lte: {field} = {actual} (expected <= {max_val})"
))
}
}
"array_field_count_lte" => {
let field = str_field(map, "field")?;
let max_count = u64_field(map, "max_count")?;
let val = dot_path(output, field);
let len = val.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0) as u64;
if len <= max_count {
Ok(())
} else {
Err(format!(
"array_field_count_lte: {field} has {len} elements (max {})",
max_count
))
}
}
"array_field_count_gte" => {
let field = str_field(map, "field")?;
let min_count = u64_field(map, "min_count")?;
let val = dot_path(output, field);
let len = val.and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0) as u64;
if len >= min_count {
Ok(())
} else {
Err(format!(
"array_field_count_gte: {field} has {len} elements (min {})",
min_count
))
}
}
"field_eq" => {
let field = str_field(map, "field")?;
let expected = str_field(map, "expected")?;
let actual = dot_path(output, field)
.and_then(|v| v.as_str())
.unwrap_or("");
if actual == expected {
Ok(())
} else {
Err(format!(
"field_eq: {field} = {actual:?} (expected {:?})",
expected
))
}
}
"workflow_steps_contains" => {
let step = str_field(map, "step")?;
let steps = dot_path(output, "workflow.steps_detected")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let found = steps.iter().any(|s| s.as_str() == Some(step));
if found {
Ok(())
} else {
Err(format!(
"workflow_steps_contains: step {:?} not found in {:?}",
step,
steps.iter().filter_map(|s| s.as_str()).collect::<Vec<_>>()
))
}
}
"evidence_score_gte" => {
let min_val: f64 = map
.get("min_value")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.ok_or_else(|| {
"assertion missing required numeric field \"min_value\"".to_string()
})?;
let actual = dot_path(output, "workflow.evidence_quality.evidence_quality_score")
.and_then(|v| v.as_f64())
.unwrap_or(0.0);
if actual >= min_val {
Ok(())
} else {
Err(format!(
"evidence_score_gte: evidence_quality_score = {actual:.2} (expected >= {min_val})"
))
}
}
"data_quality_state_eq" => {
let expected = str_field(map, "expected")?;
let actual = dot_path(output, "data_quality.state")
.and_then(|v| v.as_str())
.unwrap_or("");
if actual == expected {
Ok(())
} else {
Err(format!(
"data_quality_state_eq: data_quality.state = {actual:?} (expected {:?})",
expected
))
}
}
"feedback_state_eq" => {
let field = str_field(map, "field")?;
let expected = map
.get("value")
.and_then(|v| v.as_bool())
.ok_or_else(|| "assertion missing required bool field \"value\"".to_string())?;
let actual = output
.get("feedback_state")
.and_then(|s| s.get(field))
.and_then(Value::as_bool);
match actual {
Some(v) if v == expected => Ok(()),
Some(v) => Err(format!(
"feedback_state_eq: feedback_state.{} = {} (expected {})",
field, v, expected
)),
None => Err(format!(
"feedback_state_eq: feedback_state.{} not found in output",
field
)),
}
}
"feedback_state_field_eq" => {
let field = str_field(map, "field")?;
let expected = str_field(map, "value")?;
let actual = output
.get("feedback_state")
.and_then(|s| s.get(field))
.and_then(Value::as_str)
.unwrap_or("");
if actual == expected {
Ok(())
} else {
Err(format!(
"feedback_state_field_eq: feedback_state.{} = {:?} (expected {:?})",
field, actual, expected
))
}
}
"feedback_coverage_eq" => {
let expected = str_field(map, "value")?;
let actual = output
.get("feedback_summary")
.and_then(|s| s.get("coverage"))
.and_then(Value::as_str)
.unwrap_or("none");
if actual == expected {
Ok(())
} else {
Err(format!(
"feedback_coverage_eq: feedback_summary.coverage = {:?} (expected {:?})",
actual, expected
))
}
}
"uncertainty_exact_symbol_match" => {
let expected = map
.get("expected")
.and_then(|v| v.as_bool())
.ok_or_else(|| "assertion missing required bool field \"expected\"".to_string())?;
let actual = output
.get("uncertainty")
.and_then(|u| u.get("exact_symbol_match"))
.and_then(Value::as_bool)
.unwrap_or(false);
if actual == expected {
Ok(())
} else {
Err(format!(
"uncertainty_exact_symbol_match: exact_symbol_match = {} (expected {})",
actual, expected
))
}
}
"uncertainty_primary_source_eq" => {
let expected = str_field(map, "expected")?;
let actual = output
.get("uncertainty")
.and_then(|u| u.get("sources"))
.and_then(|s| s.get("primary"))
.and_then(Value::as_str)
.unwrap_or("none");
if actual == expected {
Ok(())
} else {
Err(format!(
"uncertainty_primary_source_eq: sources.primary = {:?} (expected {:?})",
actual, expected
))
}
}
"uncertainty_source_gte" => {
let source = str_field(map, "source")?;
let min_val: f64 = map
.get("min_value")
.and_then(|v| v.as_float().or_else(|| v.as_integer().map(|i| i as f64)))
.ok_or_else(|| {
"assertion missing required numeric field \"min_value\"".to_string()
})?;
let actual = output
.get("uncertainty")
.and_then(|u| u.get("sources"))
.and_then(|s| s.get(source))
.and_then(Value::as_f64)
.unwrap_or(0.0);
if actual >= min_val {
Ok(())
} else {
Err(format!(
"uncertainty_source_gte: sources.{} = {:.2} (expected >= {})",
source, actual, min_val
))
}
}
"array_field_contains" => {
let field = str_field(map, "field")?;
let value = str_field(map, "value")?;
let arr = dot_path(output, field)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let found = arr.iter().any(|v| v.as_str().map_or(false, |s| s == value));
if found {
Ok(())
} else {
Err(format!(
"array_field_contains: {:?} not found in {} {:?}",
value,
field,
arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>()
))
}
}
"array_field_excludes" => {
let field = str_field(map, "field")?;
let value = str_field(map, "value")?;
let arr = dot_path(output, field)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let found = arr.iter().any(|v| v.as_str().map_or(false, |s| s == value));
if !found {
Ok(())
} else {
Err(format!(
"array_field_excludes: {:?} unexpectedly found in {} {:?}",
value,
field,
arr.iter().filter_map(|v| v.as_str()).collect::<Vec<_>>()
))
}
}
"all_items_have_field" => eval_all_items_have_field(map, output),
"file_field_contains" => eval_file_field_contains(map, output),
"json_field_present" => {
let field = str_field(map, "field")?;
let present = dot_path(output, field).map_or(false, |v| !v.is_null());
if present {
Ok(())
} else {
Err(format!(
"json_field_present: field {:?} is absent or null",
field
))
}
}
"json_nested_eq" => {
let path = str_field(map, "path")?;
let actual = dot_path(output, path);
if let Some(expected_int) = map.get("value").and_then(|v| v.as_integer()) {
let actual_int = actual.and_then(Value::as_i64);
match actual_int {
Some(a) if a == expected_int => Ok(()),
Some(a) => Err(format!(
"json_nested_eq: {}.{} = {} (expected {})",
path, "", a, expected_int
)),
None => Err(format!(
"json_nested_eq: {}: field absent or non-numeric (expected {})",
path, expected_int
)),
}
} else if let Some(expected_str) = map.get("value").and_then(|v| v.as_str()) {
let actual_str = actual.and_then(Value::as_str).unwrap_or("");
if actual_str == expected_str {
Ok(())
} else {
Err(format!(
"json_nested_eq: {} = {:?} (expected {:?})",
path, actual_str, expected_str
))
}
} else {
Err("json_nested_eq: assertion missing required field \"value\" (integer or string)".to_string())
}
}
"" => Ok(()), other => Err(format!("unknown assertion kind: {:?}", other)),
}
}
fn dot_path<'a>(mut val: &'a Value, path: &str) -> Option<&'a Value> {
for key in path.split('.') {
val = val.get(key)?;
}
Some(val)
}
fn str_field<'a>(
map: &'a toml::map::Map<String, toml::Value>,
key: &str,
) -> Result<&'a str, String> {
map.get(key)
.and_then(|v| v.as_str())
.ok_or_else(|| format!("assertion missing required string field {:?}", key))
}
fn u64_field(map: &toml::map::Map<String, toml::Value>, key: &str) -> Result<u64, String> {
map.get(key)
.and_then(|v| v.as_integer())
.map(|i| i as u64)
.ok_or_else(|| format!("assertion missing required integer field {:?}", key))
}
fn summarize_debug_payload(json: &Value) -> Option<String> {
if let Some(arr) = json.get("classification_debug").and_then(|v| v.as_array()) {
let rules: Vec<&str> = arr
.iter()
.filter_map(|e| e.get("rule_that_won").and_then(|v| v.as_str()))
.collect();
if !rules.is_empty() {
return Some(format!("classification rules: {:?}", rules));
}
}
if let Some(arr) = json.get("results").and_then(|v| v.as_array()) {
let top3: Vec<&str> = arr
.iter()
.take(3)
.filter_map(|r| r.get("qname").and_then(|v| v.as_str()))
.collect();
if !top3.is_empty() {
return Some(format!("top results: {:?}", top3));
}
}
if let Some(arr) = json.get("cluster_debug").and_then(|v| v.as_array()) {
let winners: Vec<&str> = arr
.iter()
.filter_map(|e| {
e.get("winner_selected")
.and_then(|w| w.get("qname"))
.and_then(|v| v.as_str())
})
.collect();
if !winners.is_empty() {
return Some(format!("cluster winners: {:?}", winners));
}
}
None
}
fn reindex_analytics(cfg: &Config, args: ProbeReindexArgs) -> Result<()> {
let jsonl_path = history_path(cfg);
if !jsonl_path.exists() {
println!("No probe history found at {}", jsonl_path.display());
return Ok(());
}
let db_path = analytics_path(cfg);
if db_path.exists() {
if args.force {
std::fs::remove_file(&db_path)
.with_context(|| format!("removing {}", db_path.display()))?;
eprintln!("Dropped existing {}", db_path.display());
}
}
let conn =
open_analytics_db(&db_path).with_context(|| format!("opening {}", db_path.display()))?;
let raw = std::fs::read_to_string(&jsonl_path)
.with_context(|| format!("reading {}", jsonl_path.display()))?;
let mut runs = 0usize;
let mut probes = 0usize;
let mut skipped = 0usize;
for line in raw.lines().filter(|l| !l.trim().is_empty()) {
match serde_json::from_str::<Value>(line) {
Ok(record) => {
let run_id = record
.get("started_at")
.and_then(Value::as_str)
.unwrap_or("");
let already: bool = conn
.query_row(
"SELECT 1 FROM probe_runs WHERE run_id=?1",
params![run_id],
|_| Ok(true),
)
.unwrap_or(false);
if already {
skipped += 1;
continue;
}
let probe_count = record
.get("probes")
.and_then(Value::as_array)
.map(|a| a.len())
.unwrap_or(0);
insert_run_to_analytics(&conn, &record);
runs += 1;
probes += probe_count;
}
Err(_) => {} }
}
println!(
"Indexed {} run(s) ({} probe rows) into {} [{} already present, skipped]",
runs,
probes,
db_path.display(),
skipped
);
Ok(())
}
fn show_history(cfg: &Config, args: ProbeHistoryArgs) -> Result<()> {
let path = history_path(cfg);
if !path.exists() {
println!("No probe history yet. Run `asd probe run` to record the first entry.");
return Ok(());
}
let raw =
std::fs::read_to_string(&path).with_context(|| format!("reading {}", path.display()))?;
let mut records: Vec<Value> = raw
.lines()
.filter(|l| !l.trim().is_empty())
.filter_map(|l| serde_json::from_str(l).ok())
.collect();
if let Some(ref tag) = args.tag {
records.retain(|r| {
r.get("filter_tag")
.and_then(Value::as_str)
.map_or(false, |t| t == tag)
});
}
let total = records.len();
let start = total.saturating_sub(args.last);
let mut window: Vec<&Value> = records[start..].iter().rev().collect();
if window.is_empty() {
println!("No matching history records.");
return Ok(());
}
if args.json {
for r in &window {
println!("{}", serde_json::to_string(r)?);
}
} else {
println!(
"{:<10} {:<16} {:>5} {:>5} {:>10} {:<8} {}",
"version", "scope", "total", "pass", "wall_ms", "budget", "slowest"
);
println!("{}", "-".repeat(90));
for r in &mut window {
let version = r.get("asd_version").and_then(Value::as_str).unwrap_or("?");
let scope = match (
r.get("filter_name").and_then(Value::as_str),
r.get("filter_tag").and_then(Value::as_str),
) {
(Some(n), _) => format!("name:{}", n),
(_, Some(t)) => format!("tag:{}", t),
_ => "all".to_string(),
};
let total_n = r.get("total").and_then(Value::as_u64).unwrap_or(0);
let passed_n = r.get("passed").and_then(Value::as_u64).unwrap_or(0);
let wall = r.get("wall_time_ms").and_then(Value::as_u64).unwrap_or(0);
let budget_ok = r
.get("budget_failed")
.and_then(Value::as_bool)
.map_or("—", |b| if b { "FAIL" } else { "ok" });
let slowest_name = r
.get("slowest")
.and_then(Value::as_array)
.and_then(|a| a.first())
.and_then(|s| s.get("name"))
.and_then(Value::as_str)
.unwrap_or("—");
let slowest_ms = r
.get("slowest")
.and_then(Value::as_array)
.and_then(|a| a.first())
.and_then(|s| s.get("duration_ms"))
.and_then(Value::as_u64)
.map(|ms| format!("({}ms)", ms))
.unwrap_or_default();
println!(
"{:<10} {:<16} {:>5} {:>5} {:>10} {:<8} {} {}",
version, scope, total_n, passed_n, wall, budget_ok, slowest_name, slowest_ms
);
}
println!("\n{} run(s) shown ({} total recorded)", window.len(), total);
}
Ok(())
}
fn bootstrap_probes(cfg: &Config, args: ProbeBootstrapArgs) -> Result<()> {
let path = probe_file_path(cfg);
if path.exists() && !args.force {
eprintln!(
"probes.toml already exists at {}.\nUse `asd probe bootstrap --force` to overwrite.",
path.display()
);
std::process::exit(1);
}
let fts = SearchFtsDb::open(&cfg.db_path)
.map_err(|e| anyhow::anyhow!("Cannot open FTS DB: {}", e))?;
if !fts.has_data() {
eprintln!("FTS index is empty. Run `asd index` first, then re-run bootstrap.");
std::process::exit(1);
}
let filters = FtsFilters {
kind: None,
language: None,
include_tests: false,
tests_only: false,
exclude_terms: vec![],
paths_filter: vec![],
exclude_paths: vec![],
exclude_languages: vec![],
};
let try_terms = [
"manager",
"service",
"view",
"model",
"controller",
"handler",
"client",
"engine",
"store",
"state",
"view",
"scene",
"player",
"editor",
"session",
];
let mut all_hits: Vec<_> = Vec::new();
for term in try_terms {
let h = fts.search(term, &filters, args.top * 2).unwrap_or_default();
for hit in h {
if !all_hits
.iter()
.any(|x: &agentstatedeveloper_core::FtsHit| x.qname == hit.qname)
{
all_hits.push(hit);
}
}
if all_hits.len() >= args.top * 3 {
break;
}
}
let top_symbols: Vec<(String, String)> = all_hits
.iter()
.filter(|h| h.tier != 2u8) .take(args.top)
.filter_map(|h| {
let qname = h.qname.clone();
let short = qname.rsplit('.').next().unwrap_or(&qname).to_string();
if short.len() >= 3 {
Some((qname, short))
} else {
None
}
})
.collect();
let trust = compute_trust_score(&cfg.db_path);
let db_state = trust.data_quality.state.as_str();
let mut out = String::new();
out.push_str(&format!(
"# ASD golden benchmark probes — bootstrapped by `asd probe bootstrap`\n\
# DB: {}\n\
# Symbols indexed: {}\n\
# Data quality: {} ({})\n\
# Generated: {}\n\
#\n\
# Run: asd probe run\n\
# Run subset: asd probe run --tag smoke\n\
# asd probe run --tag ranking\n\n",
cfg.db_path.display(),
trust.signals.symbol_count,
db_state,
trust.data_quality.reason,
chrono::Utc::now().format("%Y-%m-%d"),
));
out.push_str("# ---------------------------------------------------------------------------\n");
out.push_str("# Structural smoke tests — always pass on a healthy indexed workspace\n");
out.push_str(
"# ---------------------------------------------------------------------------\n\n",
);
out.push_str("[[probe]]\n");
out.push_str("name = \"smoke-trust-score\"\n");
out.push_str("description = \"Trust score must be >= 0.5 for a populated index\"\n");
out.push_str("tags = [\"smoke\", \"trust\"]\n");
out.push_str("command = \"trust\"\n");
out.push_str("args = []\n");
out.push_str("assert = { kind = \"field_gte\", field = \"score\", min_value = 0.5 }\n\n");
out.push_str("[[probe]]\n");
out.push_str("name = \"smoke-symbol-count\"\n");
out.push_str("description = \"Index must contain at least 10 symbols\"\n");
out.push_str("tags = [\"smoke\", \"trust\"]\n");
out.push_str("command = \"trust\"\n");
out.push_str("args = []\n");
out.push_str(
"assert = { kind = \"field_gte\", field = \"signals.symbol_count\", min_value = 10 }\n\n",
);
out.push_str("[[probe]]\n");
out.push_str("name = \"smoke-search-returns-results\"\n");
out.push_str("description = \"A broad search must return at least 1 result\"\n");
out.push_str("tags = [\"smoke\", \"search\"]\n");
out.push_str("command = \"search\"\n");
let first_query = top_symbols
.first()
.map(|(_, s)| s.to_lowercase())
.unwrap_or_else(|| "state".to_string());
out.push_str(&format!("args = [\"{}\", \"--agent\"]\n", first_query));
out.push_str(
"assert = { kind = \"array_field_count_gte\", field = \"results\", min_count = 1 }\n\n",
);
out.push_str("[[probe]]\n");
out.push_str("name = \"smoke-feedback-state-field-present\"\n");
out.push_str("description = \"search output must include feedback_state field\"\n");
out.push_str("tags = [\"smoke\", \"feedback\"]\n");
out.push_str("command = \"search\"\n");
out.push_str(&format!("args = [\"{}\", \"--agent\"]\n", first_query));
out.push_str("assert = { kind = \"feedback_summary_gte\", field = \"entries_applied\", min_value = 0 }\n\n");
out.push_str("[[probe]]\n");
out.push_str("name = \"smoke-data-quality-state\"\n");
out.push_str(&format!(
"description = \"Data quality must be '{}' for this workspace state\"\n",
db_state
));
out.push_str("tags = [\"smoke\", \"trust\", \"data-quality\"]\n");
out.push_str("command = \"trust\"\n");
out.push_str("args = []\n");
out.push_str(&format!(
"assert = {{ kind = \"data_quality_state_eq\", expected = \"{}\" }}\n\n",
db_state
));
if !top_symbols.is_empty() {
out.push_str(
"# ---------------------------------------------------------------------------\n",
);
out.push_str("# Symbol ranking probes — discovered from current index\n");
out.push_str("# Edit these to match your domain's key symbols.\n");
out.push_str(
"# ---------------------------------------------------------------------------\n\n",
);
for (i, (qname, short)) in top_symbols.iter().enumerate() {
let slug = short
.to_lowercase()
.replace(|c: char| !c.is_alphanumeric(), "-");
let probe_name = format!("rank-{}-top5", slug);
let desc = format!("{} must appear in top 5 results for its own name", qname);
out.push_str(&format!("[[probe]]\nname = {:?}\n", probe_name));
out.push_str(&format!("description = {:?}\n", desc));
out.push_str("tags = [\"ranking\"]\n");
out.push_str("command = \"search\"\n");
out.push_str(&format!("args = [{:?}, \"--agent\"]\n", short));
out.push_str(&format!(
"assert = {{ kind = \"qname_rank_lte\", fragment = {:?}, max_rank = 5 }}\n\n",
qname
));
let prec_name = format!("rank-{}-eq1", slug);
let prec_desc = format!(
"{} must be the #1 result for its own name (precision-mode for calibration)",
qname
);
out.push_str(&format!("[[probe]]\nname = {:?}\n", prec_name));
out.push_str(&format!("description = {:?}\n", prec_desc));
out.push_str("tags = [\"ranking\", \"precision\"]\n");
out.push_str("command = \"search\"\n");
out.push_str(&format!("args = [{:?}, \"--agent\"]\n", short));
out.push_str(&format!(
"assert = {{ kind = \"qname_rank_eq\", fragment = {:?}, exact_rank = 1 }}\n\n",
qname
));
if i >= args.top.saturating_sub(1) {
break;
}
}
}
if !top_symbols.is_empty() {
let query_term = top_symbols
.first()
.map(|(_, s)| s.to_lowercase())
.unwrap_or_default();
if query_term.len() >= 3 {
out.push_str(
"# ---------------------------------------------------------------------------\n",
);
out.push_str("# Change-model smoke tests\n");
out.push_str(
"# ---------------------------------------------------------------------------\n\n",
);
out.push_str("[[probe]]\n");
out.push_str("name = \"change-model-returns-edit-files\"\n");
out.push_str(
"description = \"prepare-change must return at least 1 likely_edit_files entry\"\n",
);
out.push_str("tags = [\"change-model\", \"smoke\"]\n");
out.push_str("command = \"prepare-change\"\n");
out.push_str(&format!("args = [{:?}, \"--agent\"]\n", query_term));
out.push_str("assert = { kind = \"array_field_count_gte\", field = \"likely_edit_files\", min_count = 1 }\n\n");
out.push_str("[[probe]]\n");
out.push_str("name = \"change-model-edit-confidence-present\"\n");
out.push_str(
"description = \"classification_summary must include an edit_confidence field\"\n",
);
out.push_str("tags = [\"change-model\", \"smoke\"]\n");
out.push_str("command = \"prepare-change\"\n");
out.push_str(&format!("args = [{:?}, \"--agent\"]\n", query_term));
out.push_str("assert = { kind = \"array_field_count_gte\", field = \"likely_edit_files\", min_count = 0 }\n\n");
}
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, &out)?;
println!("✓ Generated {} probes.toml", path.display());
println!(" Symbols indexed : {}", trust.signals.symbol_count);
println!(
" Data quality : {} ({})",
db_state, trust.data_quality.reason
);
println!(
" Ranking probes : {} (for top {} symbols)",
top_symbols.len(),
args.top
);
println!("\nRun: asd probe run");
println!("Tag subsets: asd probe run --tag smoke");
Ok(())
}
fn add_probe(cfg: &Config, args: ProbeAddArgs) -> Result<()> {
let path = probe_file_path(cfg);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let mut content = if path.exists() {
std::fs::read_to_string(&path)?
} else {
String::new()
};
let mut block = format!("\n[[probe]]\nname = {:?}\n", args.name);
if let Some(ref desc) = args.description {
block.push_str(&format!("description = {:?}\n", desc));
}
block.push_str(&format!("command = {:?}\n", args.command));
let toml_args: Vec<String> = args.args.iter().map(|a| format!("{:?}", a)).collect();
block.push_str(&format!("args = [{}]\n", toml_args.join(", ")));
if let Some(ref assert_str) = args.assert {
block.push_str(&format!("assert = {}\n", assert_str));
}
content.push_str(&block);
std::fs::write(&path, &content)?;
println!("Added probe {:?} to {}", args.name, path.display());
Ok(())
}
fn eval_qname_rank_lte(
map: &toml::map::Map<String, toml::Value>,
output: &Value,
) -> Result<(), String> {
let fragment = str_field(map, "fragment")?;
let max_rank = u64_field(map, "max_rank")?;
let empty_arr: Vec<Value> = Vec::new();
let results = output
.get("results")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let pos = results.iter().position(|r| {
r.get("qname")
.and_then(|v| v.as_str())
.map_or(false, |q| q.contains(fragment))
});
match pos {
Some(idx) if (idx as u64 + 1) <= max_rank => Ok(()),
Some(idx) => Err(format!(
"qname_rank_lte: {:?} found at rank {} (max_rank={})",
fragment,
idx + 1,
max_rank
)),
None => Err(format!(
"qname_rank_lte: no result qname contains {:?} (checked {} results)",
fragment,
results.len()
)),
}
}
fn eval_cluster_winner_kind_not(
map: &toml::map::Map<String, toml::Value>,
output: &Value,
) -> Result<(), String> {
let doc_stem = str_field(map, "doc_stem")?;
let kind_not = str_field(map, "kind_not")?;
let empty_arr: Vec<Value> = Vec::new();
let dbg = output
.get("cluster_debug")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let entry = dbg.iter().find(|e| {
e.get("doc_file")
.and_then(|v| v.as_str())
.map_or(false, |f| {
f.to_lowercase().contains(&doc_stem.to_lowercase())
})
});
match entry {
None => Err(format!(
"cluster_winner_kind_not: no cluster_debug entry matches doc_stem {:?}",
doc_stem
)),
Some(e) => {
let winner = e.get("winner_selected").unwrap_or(&Value::Null);
let qname = winner.get("qname").and_then(|v| v.as_str()).unwrap_or("");
if qname.contains(kind_not) {
Err(format!(
"cluster_winner_kind_not: winner {:?} contains {:?}",
qname, kind_not
))
} else {
Ok(())
}
}
}
}
fn eval_cluster_winner_qname_contains(
map: &toml::map::Map<String, toml::Value>,
output: &Value,
) -> Result<(), String> {
let doc_stem = str_field(map, "doc_stem")?;
let fragment = str_field(map, "fragment")?;
let empty_arr: Vec<Value> = Vec::new();
let dbg = output
.get("cluster_debug")
.and_then(|v| v.as_array())
.unwrap_or(&empty_arr);
let entry = dbg.iter().find(|e| {
e.get("doc_file")
.and_then(|v| v.as_str())
.map_or(false, |f| {
f.to_lowercase().contains(&doc_stem.to_lowercase())
})
});
match entry {
None => Err(format!(
"cluster_winner_qname_contains: no cluster_debug entry matches doc_stem {:?}",
doc_stem
)),
Some(e) => {
let winner = e.get("winner_selected").unwrap_or(&Value::Null);
let qname = winner.get("qname").and_then(|v| v.as_str()).unwrap_or("");
if qname.to_lowercase().contains(&fragment.to_lowercase()) {
Ok(())
} else {
Err(format!(
"cluster_winner_qname_contains: winner {:?} does not contain {:?}",
qname, fragment
))
}
}
}
}
fn eval_all_items_have_field(
map: &toml::map::Map<String, toml::Value>,
output: &Value,
) -> Result<(), String> {
let array_path = str_field(map, "array")?;
let field = str_field(map, "field")?;
let arr = dot_path(output, array_path)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
if arr.is_empty() {
return Ok(());
}
let missing: Vec<String> = arr
.iter()
.filter_map(|item| {
let present = item
.get(field)
.map(|v| !v.is_null() && v.as_str().map_or(true, |s| !s.is_empty()))
.unwrap_or(false);
if !present {
Some(
item.get("file")
.and_then(Value::as_str)
.unwrap_or("?")
.to_string(),
)
} else {
None
}
})
.collect();
if missing.is_empty() {
Ok(())
} else {
Err(format!(
"all_items_have_field: {} items in {} missing field {:?}: {:?}",
missing.len(),
array_path,
field,
&missing[..missing.len().min(5)]
))
}
}
fn eval_file_field_contains(
map: &toml::map::Map<String, toml::Value>,
output: &Value,
) -> Result<(), String> {
let array_path = str_field(map, "array")?;
let file_fragment = str_field(map, "file_fragment")?;
let field = str_field(map, "field")?;
let value_contains = str_field(map, "value_contains")?;
let arr = dot_path(output, array_path)
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let item = arr.iter().find(|item| {
item.get("file")
.and_then(Value::as_str)
.map_or(false, |f| f.contains(file_fragment))
});
match item {
None => Err(format!(
"file_field_contains: no item with file containing {:?} found in {}",
file_fragment, array_path
)),
Some(item) => {
let val = item.get(field).and_then(Value::as_str).unwrap_or("");
if val.contains(value_contains) {
Ok(())
} else {
Err(format!(
"file_field_contains: item {:?}.{} = {:?} does not contain {:?}",
item.get("file").and_then(Value::as_str).unwrap_or("?"),
field,
val,
value_contains
))
}
}
}
}
#[cfg(test)]
mod plan_j_t019_qname_rank_eq_tests {
use super::eval_assert;
use serde_json::json;
fn assert_block(kind: &str, fragment: &str, exact_rank: u64) -> toml::Value {
let s = format!(
"kind = {:?}\nfragment = {:?}\nexact_rank = {}",
kind, fragment, exact_rank
);
toml::from_str(&s).unwrap()
}
fn results_with(qnames: &[&str]) -> serde_json::Value {
json!({
"results": qnames
.iter()
.map(|q| json!({ "qname": q }))
.collect::<Vec<_>>(),
})
}
#[test]
fn passes_when_exact_rank_match() {
let assert = assert_block("qname_rank_eq", "ProjectManager", 1);
let output = results_with(&["App.Foo.ProjectManager", "App.Bar.OtherSym"]);
assert!(eval_assert(&assert, &output).is_ok());
}
#[test]
fn fails_when_match_at_wrong_rank() {
let assert = assert_block("qname_rank_eq", "ProjectManager", 1);
let output = results_with(&[
"App.Bar.OtherSym", "App.Foo.ProjectManager", ]);
let err = eval_assert(&assert, &output).expect_err("must fail");
assert!(err.contains("rank 2"), "got: {err}");
assert!(err.contains("expected exactly 1"), "got: {err}");
}
#[test]
fn fails_when_no_match() {
let assert = assert_block("qname_rank_eq", "MissingSymbol", 1);
let output = results_with(&["App.Foo.SomethingElse"]);
let err = eval_assert(&assert, &output).expect_err("must fail");
assert!(err.contains("no result qname contains"), "got: {err}");
assert!(err.contains("MissingSymbol"), "got: {err}");
}
#[test]
fn precision_distinguishes_from_lte_on_same_input() {
let output = results_with(&[
"App.Bar.OtherSym", "App.Bar.OtherSym2", "App.Foo.ProjectManager", ]);
let lenient: toml::Value = toml::from_str(
"kind = \"qname_rank_lte\"\nfragment = \"ProjectManager\"\nmax_rank = 5",
)
.unwrap();
assert!(
eval_assert(&lenient, &output).is_ok(),
"rank_lte(5) must accept rank 3"
);
let precision = assert_block("qname_rank_eq", "ProjectManager", 1);
assert!(
eval_assert(&precision, &output).is_err(),
"rank_eq(1) must reject rank 3 — the very split that drives calibration"
);
}
#[test]
fn rank_eq_2_passes_when_match_at_rank_2() {
let assert = assert_block("qname_rank_eq", "AlphaCanon", 2);
let output = results_with(&["App.Bar.BetaSibling", "App.Foo.AlphaCanon"]);
assert!(eval_assert(&assert, &output).is_ok());
}
}