use std::collections::BTreeMap;
use std::path::PathBuf;
use std::time::Duration;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, serde::Deserialize)]
pub struct Config {
#[serde(default)]
pub enabled: bool,
#[serde(default)]
pub key_file: Option<String>,
#[serde(default)]
pub key_cmd: Option<Vec<String>>,
#[serde(default = "default_model")]
pub model: String,
#[serde(default = "default_budget")]
pub budget_ms: u64,
#[serde(default = "default_endpoint")]
pub endpoint: String,
#[serde(default = "default_monthly")]
pub monthly_usd: f64,
#[serde(default = "default_min_words")]
pub min_words: usize,
#[serde(default = "default_min_candidates")]
pub min_candidates: usize,
#[serde(default = "default_price_in")]
pub usd_per_mtok_in: f64,
#[serde(default = "default_cut")]
pub bears_at: f64,
#[serde(default = "default_cut")]
pub cue_at: f64,
#[serde(default = "default_escalate")]
pub escalate_below: f64,
#[serde(default = "default_cache_days")]
pub cache_days: u64,
}
fn default_model() -> String {
"jev-1.13.0".into()
}
fn default_budget() -> u64 {
2000
}
fn default_endpoint() -> String {
"https://api.typesafe.ai/v1/systemone".into()
}
fn default_monthly() -> f64 {
4.0
}
fn default_min_words() -> usize {
4
}
fn default_min_candidates() -> usize {
2
}
fn default_cache_days() -> u64 {
7
}
fn default_escalate() -> f64 {
0.8
}
fn default_cut() -> f64 {
0.5
}
fn default_price_in() -> f64 {
0.042
}
fn cost_of(body: &Value, usd_per_mtok_in: f64) -> f64 {
body["usage"]["cost"].as_f64().unwrap_or_else(|| {
body["usage"]["input_tokens"].as_f64().unwrap_or(0.0) * usd_per_mtok_in / 1e6
})
}
const PROMPT_CHARS: usize = 2000;
fn config_path() -> PathBuf {
std::env::var_os("XDG_CONFIG_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
.unwrap_or_else(|| PathBuf::from(".config"))
.join("ljos")
.join("jev.toml")
}
fn expand(path: &str) -> PathBuf {
match path.strip_prefix("~/") {
Some(rest) => std::env::var_os("HOME")
.map_or_else(|| PathBuf::from(path), |h| PathBuf::from(h).join(rest)),
None => PathBuf::from(path),
}
}
fn read_config() -> Option<Config> {
let text = std::fs::read_to_string(config_path()).ok()?;
toml::from_str(&text).ok()
}
#[must_use]
pub fn enabled() -> bool {
read_config().is_some_and(|c| c.enabled)
}
fn key_from(text: &str) -> Option<String> {
let line = text.lines().next()?.trim();
let value = line
.rsplit(|c: char| c == ':' || c == '=' || c.is_whitespace())
.next()
.unwrap_or(line)
.trim();
(!value.is_empty()).then(|| value.to_string())
}
fn key_cache() -> Option<PathBuf> {
let dir = std::env::var_os("XDG_RUNTIME_DIR").filter(|v| !v.is_empty())?;
Some(PathBuf::from(dir).join("ljos").join("jev-key"))
}
fn key_by_command(argv: &[String]) -> Option<String> {
use std::io::Write;
use std::os::unix::fs::OpenOptionsExt;
let cache = key_cache();
if let Some(key) = cache
.as_ref()
.and_then(|p| std::fs::read_to_string(p).ok())
.and_then(|t| key_from(&t))
{
return Some(key);
}
let (prog, args) = argv.split_first()?;
let out = std::process::Command::new("timeout")
.arg("3")
.arg(prog)
.args(args)
.stdin(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.ok()?;
if !out.status.success() {
return None;
}
let key = key_from(&String::from_utf8_lossy(&out.stdout))?;
if let Some(path) = cache {
let _ = std::fs::create_dir_all(path.parent()?);
if let Ok(mut f) = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.mode(0o600)
.open(&path)
{
let _ = writeln!(f, "{key}");
}
}
Some(key)
}
#[must_use]
pub fn config() -> Option<(Config, String)> {
let cfg = read_config()?;
if !cfg.enabled || month_cost().unwrap_or(0.0) >= cfg.monthly_usd {
return None;
}
let key = match (&cfg.key_cmd, &cfg.key_file) {
(Some(argv), _) => key_by_command(argv)?,
(None, Some(file)) => key_from(&std::fs::read_to_string(expand(file)).ok()?)?,
(None, None) => return None,
};
Some((cfg, key))
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Judgment {
pub bears: Vec<f64>,
pub correction: f64,
pub choice: f64,
pub cost: f64,
pub bears_at: f64,
pub cue_at: f64,
}
impl Judgment {
#[must_use]
pub fn bears(&self, i: usize) -> bool {
self.bears.get(i).is_some_and(|p| *p >= self.bears_at)
}
}
#[must_use]
pub fn request(model: &str, prompt: &str, candidates: &[&str]) -> Value {
let prompt: String = prompt.chars().take(PROMPT_CHARS).collect();
let mut state = format!("Prompt from the person to the agent:\n{prompt}\n\nStored claims:\n");
for (i, text) in candidates.iter().enumerate() {
state.push_str(&format!("[{i}] {text}\n"));
}
let mut questions = serde_json::Map::new();
for i in 0..candidates.len() {
questions.insert(
format!("bears_{i}"),
serde_json::json!({
"type": "noul",
"instructions": format!("Does stored claim [{i}] bear on what the prompt asks the agent to do now?"),
"criteria": {
"true": "The claim changes or informs how the agent should act on this prompt",
"false": "The claim is about something else, or only shares words with the prompt"
}
}),
);
}
questions.insert(
"correction".into(),
serde_json::json!({
"type": "noul",
"instructions": "Does the person correct the agent for something it did, forgot or was already told?",
"criteria": {
"true": "The prompt tells the agent it was wrong or should already know",
"false": "The prompt asks for work or information without correcting the agent"
}
}),
);
questions.insert(
"choice".into(),
serde_json::json!({
"type": "noul",
"instructions": "Does the prompt put to the agent a choice between two or more defensible options?",
"criteria": {
"true": "The person asks which of several ways to take, or weighs options",
"false": "The person names one thing to do, or asks a factual question"
}
}),
);
serde_json::json!({ "model": model, "state": state, "questions": questions })
}
#[must_use]
pub fn parse(body: &Value, candidates: usize) -> Option<Judgment> {
let answers = body.get("answers")?.as_object()?;
let noul = |key: &str| answers.get(key)?.get("noul")?.as_f64();
let bears: Vec<f64> = (0..candidates)
.map(|i| noul(&format!("bears_{i}")))
.collect::<Option<_>>()?;
Some(Judgment {
bears,
correction: noul("correction")?,
choice: noul("choice")?,
cost: 0.0,
bears_at: 0.5,
cue_at: 0.5,
})
}
fn post(cfg: &Config, key: &str, body: Value, kind: &str, about: Value) -> Option<Value> {
let request = format!("{}\n{body}", cfg.endpoint);
if let Some(reply) = cached(&request, cfg.cache_days) {
count("cached");
return Some(reply);
}
let reply: Value = ureq::post(&cfg.endpoint)
.timeout(Duration::from_millis(cfg.budget_ms))
.set("Authorization", &format!("Bearer {key}"))
.set("Content-Type", "application/json")
.send_json(body)
.ok()?
.into_json()
.ok()?;
let answers = reply.get("answers")?.clone();
let cost = cost_of(&reply, cfg.usd_per_mtok_in);
record_cost(cost);
if cfg.cache_days > 0 {
keep(&request, &reply);
}
log(&serde_json::json!({
"ts": crate::now_utc(),
"kind": kind,
"model": cfg.model,
"about": about,
"answers": answers,
"cost": cost,
}));
Some(reply)
}
#[must_use]
pub fn judge(prompt: &str, candidates: &[&str]) -> Option<Judgment> {
let (cfg, key) = config()?;
let body = request(&cfg.model, prompt, candidates);
let reply = post(&cfg, &key, body, "hook", Value::Null)?;
let mut judged = parse(&reply, candidates.len())?;
judged.cost = cost_of(&reply, cfg.usd_per_mtok_in);
judged.bears_at = cfg.bears_at;
judged.cue_at = cfg.cue_at;
Some(judged)
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ballot {
pub choice: String,
pub confidence: f64,
pub probabilities: BTreeMap<String, f64>,
pub forecast: BTreeMap<String, f64>,
pub escalate_below: f64,
}
impl Ballot {
#[must_use]
pub fn escalates(&self) -> bool {
self.confidence < self.escalate_below
}
}
#[must_use]
pub fn ballot_request(model: &str, brief: &str, options: &[String]) -> Value {
let criteria = |verb: &str| -> Value {
options
.iter()
.map(|o| (o.clone(), Value::String(format!("{verb} {o}"))))
.collect::<serde_json::Map<_, _>>()
.into()
};
serde_json::json!({
"model": model,
"state": brief,
"questions": {
"ballot": {
"type": "choice",
"instructions": "You are the persona the state describes. Which option do you vote for, from your own view and what you know?",
"criteria": criteria("vote for"),
},
"forecast": {
"type": "choice",
"instructions": "Which option will most of the other reviewers on this panel vote for?",
"criteria": criteria("most others pick"),
},
}
})
}
#[must_use]
pub fn parse_ballot(body: &Value, options: &[String]) -> Option<Ballot> {
let answers = body.get("answers")?;
let probs = |key: &str| -> Option<BTreeMap<String, f64>> {
let map = answers.get(key)?.get("probabilities")?.as_object()?;
Some(
map.iter()
.filter_map(|(k, v)| Some((k.clone(), v.as_f64()?)))
.collect(),
)
};
let ballot = answers.get("ballot")?;
let choice = ballot.get("choice")?.as_str()?.to_string();
if !options.contains(&choice) {
return None;
}
Some(Ballot {
confidence: ballot.get("confidence")?.as_f64()?,
probabilities: probs("ballot")?,
forecast: probs("forecast")?,
choice,
escalate_below: 0.8,
})
}
#[must_use]
pub fn ballot(persona: &str, issue: &str, brief: &str, options: &[String]) -> Option<Ballot> {
let (cfg, key) = config()?;
let body = ballot_request(&cfg.model, brief, options);
let about = serde_json::json!({"issue": issue, "persona": persona, "options": options});
let reply = post(&cfg, &key, body, "ballot", about)?;
let mut b = parse_ballot(&reply, options)?;
b.escalate_below = cfg.escalate_below;
Some(b)
}
fn cache_dir() -> Option<PathBuf> {
Some(
std::env::var_os("XDG_CACHE_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".cache")))?
.join("ljos")
.join("jev"),
)
}
fn cache_file(request: &str) -> Option<PathBuf> {
use std::hash::{Hash, Hasher};
let mut h = std::collections::hash_map::DefaultHasher::new();
request.hash(&mut h);
Some(cache_dir()?.join(format!("{:016x}.json", h.finish())))
}
fn cached(request: &str, days: u64) -> Option<Value> {
if days == 0 {
return None;
}
let path = cache_file(request)?;
let age = std::fs::metadata(&path)
.ok()?
.modified()
.ok()?
.elapsed()
.ok()?;
if age > Duration::from_secs(days * 86_400) {
let _ = std::fs::remove_file(&path);
return None;
}
let entry: Value = serde_json::from_str(&std::fs::read_to_string(&path).ok()?).ok()?;
(entry["request"].as_str() == Some(request)).then(|| entry["reply"].clone())
}
fn keep(request: &str, reply: &Value) {
let Some(path) = cache_file(request) else {
return;
};
if let Some(dir) = path.parent() {
let _ = std::fs::create_dir_all(dir);
}
let entry = serde_json::json!({"request": request, "reply": reply});
let _ = std::fs::write(path, entry.to_string());
}
fn count(what: &str) {
let Some(dir) = state_dir() else { return };
let _ = std::fs::create_dir_all(&dir);
let path = dir.join("jev-cost.toml");
let mut totals: BTreeMap<String, f64> = std::fs::read_to_string(&path)
.ok()
.and_then(|t| toml::from_str(&t).ok())
.unwrap_or_default();
*totals
.entry(format!("{}-{what}", this_month()))
.or_default() += 1.0;
if let Ok(text) = toml::to_string(&totals) {
let _ = std::fs::write(path, text);
}
}
pub const AUDIT_CLAIM_AT: f64 = 0.9;
pub const AUDIT_RED_BELOW: f64 = 0.1;
pub const AUDIT_DEFER_AT: f64 = 0.9;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Audit {
pub claims_complete: f64,
pub tests_green: f64,
pub deferral: f64,
}
#[must_use]
pub fn audit_request(model: &str, state: &str) -> Value {
serde_json::json!({
"model": model,
"state": state,
"questions": {
"claims_complete": {
"type": "noul",
"instructions": "Does the agent's final message claim the asked work is done, complete, passing, green or ready?",
"criteria": {
"true": "It says the work is finished or the tests pass",
"false": "It reports progress, a failure, a question or what is still open"
}
},
"tests_green": {
"type": "noul",
"instructions": "Does the most recent test output in the state pass, with no failed, errored or crashed test?",
"criteria": {
"true": "The latest run reports every test passing",
"false": "The latest run reports a failure, an error, a crash or a build that did not finish"
}
},
"deferral": {
"type": "noul",
"instructions": "Does the final message put part of what the person asked off to later, or call it out of scope, without naming something outside the agent's control that blocks it?",
"criteria": {
"true": "It leaves asked work for a later change, session or person, with no external block",
"false": "It finishes the asked work, or names a real block such as a missing credential or a failing external service"
}
}
}
})
}
#[must_use]
pub fn parse_audit(body: &Value) -> Option<Audit> {
let a = body.get("answers")?;
let noul = |k: &str| a.get(k)?.get("noul")?.as_f64();
Some(Audit {
claims_complete: noul("claims_complete")?,
tests_green: noul("tests_green")?,
deferral: noul("deferral")?,
})
}
#[must_use]
pub fn audit(state: &str) -> Option<Audit> {
let (cfg, key) = config()?;
let body = audit_request(&cfg.model, state);
let reply = post(&cfg, &key, body, "stop-audit", Value::Null)?;
parse_audit(&reply)
}
#[must_use]
pub fn audit_reason(a: &Audit, test_ran: bool) -> Option<String> {
if test_ran && a.claims_complete >= AUDIT_CLAIM_AT && a.tests_green <= AUDIT_RED_BELOW {
return Some(
"The final message says the work is done, and the last test run shown is red. \
Say what still fails, or fix it, before stopping."
.to_string(),
);
}
if a.deferral >= AUDIT_DEFER_AT {
return Some(
"The final message leaves part of the asked work for later without naming what blocks it. \
Do that part, or say in one sentence what outside the work blocks it."
.to_string(),
);
}
None
}
fn state_dir() -> Option<PathBuf> {
Some(
std::env::var_os("XDG_STATE_HOME")
.filter(|v| !v.is_empty())
.map(PathBuf::from)
.or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".local/state")))?
.join("ljos"),
)
}
fn log(entry: &Value) {
use std::io::Write;
let Some(dir) = state_dir() else { return };
let _ = std::fs::create_dir_all(&dir);
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(dir.join("jev-log.jsonl"))
{
let _ = writeln!(f, "{entry}");
}
}
fn record_cost(cost: f64) {
let Some(dir) = state_dir() else { return };
let _ = std::fs::create_dir_all(&dir);
let month = crate::now_utc().chars().take(7).collect::<String>();
let path = dir.join("jev-cost.toml");
let mut totals: BTreeMap<String, f64> = std::fs::read_to_string(&path)
.ok()
.and_then(|t| toml::from_str(&t).ok())
.unwrap_or_default();
*totals.entry(format!("{month}-calls")).or_default() += 1.0;
*totals.entry(month).or_default() += cost;
if let Ok(text) = toml::to_string(&totals) {
let _ = std::fs::write(path, text);
}
}
fn month_totals() -> Option<BTreeMap<String, f64>> {
let dir = state_dir()?;
let text = std::fs::read_to_string(dir.join("jev-cost.toml")).ok()?;
toml::from_str(&text).ok()
}
fn this_month() -> String {
crate::now_utc().chars().take(7).collect()
}
#[must_use]
pub fn month_cost() -> Option<f64> {
month_totals()?.get(&this_month()).copied()
}
#[must_use]
pub fn month_count(what: &str) -> u64 {
month_totals()
.and_then(|t| t.get(&format!("{}-{what}", this_month())).copied())
.unwrap_or(0.0) as u64
}
#[must_use]
pub fn month_calls() -> u64 {
month_count("calls")
}
#[must_use]
pub fn doctor_row() -> Option<crate::Habitat> {
let text = std::fs::read_to_string(config_path()).ok()?;
let (state, ok) = match toml::from_str::<Config>(&text) {
Err(e) => (format!("{}: {e}", config_path().display()), false),
Ok(cfg) if !cfg.enabled => ("off".to_string(), true),
Ok(cfg) => {
let spent = month_cost().unwrap_or(0.0);
let head = format!(
"{} {} calls, {} cached ${spent:.4} of ${:.2} this month",
cfg.model,
month_calls(),
month_count("cached"),
cfg.monthly_usd
);
if spent >= cfg.monthly_usd {
(format!("capped {head}"), true)
} else if config().is_none() {
(
"on, but the key file or command gave no key".to_string(),
false,
)
} else {
(format!("on {head}"), true)
}
}
};
Some(crate::Habitat {
name: "jev",
state,
ok,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn one_request_asks_about_every_candidate_and_both_cues() {
let body = request("jev-1.13.0", "fix the ci", &["alpha claim", "beta claim"]);
let q = body["questions"].as_object().unwrap();
assert_eq!(q.len(), 4);
assert_eq!(q["bears_1"]["type"], "noul");
assert!(body["state"].as_str().unwrap().contains("[1] beta claim"));
}
#[test]
fn a_full_answer_is_read_and_a_partial_one_is_refused() {
let full = serde_json::json!({
"answers": {
"bears_0": {"type": "noul", "noul": 0.9},
"bears_1": {"type": "noul", "noul": 0.1},
"correction": {"type": "noul", "noul": 0.2},
"choice": {"type": "noul", "noul": 0.7}
},
"usage": {"input_tokens": 900, "output_tokens": 40, "cost": 0.0000378}
});
let j = parse(&full, 2).unwrap();
assert_eq!(j.bears, vec![0.9, 0.1]);
assert!(j.bears(0) && !j.bears(1));
let strict = Judgment {
bears_at: 0.95,
..j.clone()
};
assert!(!strict.bears(0), "a higher cut drops the 0.9");
assert!((j.choice - 0.7).abs() < 1e-9);
assert!(
(cost_of(&full, 0.042) - 0.0000378).abs() < 1e-12,
"the API's figure"
);
let direct = serde_json::json!({"usage": {"input_tokens": 1000, "output_tokens": 60}});
assert!(
(cost_of(&direct, 0.042) - 0.000042).abs() < 1e-12,
"tokens at the price"
);
let partial = serde_json::json!({"answers": {"bears_0": {"noul": 0.9}}});
assert!(parse(&partial, 2).is_none());
}
#[test]
fn jev_is_off_without_a_file_and_off_when_the_file_says_so() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("XDG_CONFIG_HOME", dir.path()) };
assert!(config().is_none(), "no file, no call");
std::fs::create_dir_all(dir.path().join("ljos")).unwrap();
let key = dir.path().join("key");
std::fs::write(&key, "sk-or-test\n").unwrap();
std::fs::write(
dir.path().join("ljos/jev.toml"),
format!("enabled = false\nkey_file = \"{}\"\n", key.display()),
)
.unwrap();
assert!(config().is_none(), "a file that says off is off");
std::fs::write(
dir.path().join("ljos/jev.toml"),
format!("enabled = true\nkey_file = \"{}\"\n", key.display()),
)
.unwrap();
let (cfg, k) = config().unwrap();
assert_eq!(k, "sk-or-test");
assert_eq!(cfg.budget_ms, 2000);
assert_eq!(cfg.min_candidates, 2);
unsafe { std::env::remove_var("XDG_CONFIG_HOME") };
}
#[test]
fn a_key_line_gives_its_value() {
assert_eq!(key_from("sk-or-v1-abc\n").as_deref(), Some("sk-or-v1-abc"));
assert_eq!(
key_from("apikey: sk-or-v1-abc\nurl: x\n").as_deref(),
Some("sk-or-v1-abc")
);
assert_eq!(
key_from("apikey=sk-or-v1-abc").as_deref(),
Some("sk-or-v1-abc")
);
assert_eq!(key_from("\n"), None);
}
#[test]
fn a_ballot_carries_its_confidence_and_forecast_and_escalates_under_the_cut() {
let options = vec!["age".to_string(), "gpg".to_string()];
let body = ballot_request("jev-1.13.0", "You are brio.", &options);
assert_eq!(body["questions"]["ballot"]["type"], "choice");
assert_eq!(
body["questions"]["forecast"]["criteria"]["gpg"],
"most others pick gpg"
);
let reply = serde_json::json!({"answers": {
"ballot": {"type": "choice", "choice": "age", "confidence": 0.97,
"probabilities": {"age": 0.98, "gpg": 0.02}},
"forecast": {"type": "choice", "choice": "age", "confidence": 0.95,
"probabilities": {"age": 0.97, "gpg": 0.03}}}});
let b = parse_ballot(&reply, &options).unwrap();
assert_eq!(b.choice, "age");
assert!(!b.escalates(), "0.97 stands at the 0.8 cut");
assert!((b.forecast["gpg"] - 0.03).abs() < 1e-9);
let unsure = Ballot {
confidence: 0.6,
..b.clone()
};
assert!(unsure.escalates(), "0.6 goes to a subagent");
let off = serde_json::json!({"answers": {
"ballot": {"choice": "rsa", "confidence": 0.9, "probabilities": {}},
"forecast": {"choice": "age", "confidence": 0.9, "probabilities": {}}}});
assert!(
parse_ballot(&off, &options).is_none(),
"a choice off the list is refused"
);
}
#[test]
fn an_identical_request_is_answered_from_the_cache_and_only_that_one() {
let dir = tempfile::tempdir().unwrap();
unsafe { std::env::set_var("XDG_CACHE_HOME", dir.path()) };
let reply = serde_json::json!({"answers": {"x": {"noul": 0.9}}});
assert!(cached("req-a", 7).is_none(), "nothing kept yet");
keep("req-a", &reply);
assert_eq!(cached("req-a", 7), Some(reply));
assert!(cached("req-b", 7).is_none(), "another request misses");
assert!(cached("req-a", 0).is_none(), "0 days is off");
unsafe { std::env::remove_var("XDG_CACHE_HOME") };
}
#[test]
fn a_stop_is_held_only_on_done_beside_red_or_an_open_deferral() {
let a = |c: f64, g: f64, d: f64| Audit {
claims_complete: c,
tests_green: g,
deferral: d,
};
assert!(
audit_reason(&a(0.95, 0.05, 0.1), true).is_some(),
"done beside red"
);
assert!(
audit_reason(&a(0.95, 0.05, 0.1), false).is_none(),
"no test ran, nothing to be red"
);
assert!(
audit_reason(&a(0.95, 0.9, 0.1), true).is_none(),
"done beside green"
);
assert!(
audit_reason(&a(0.5, 0.05, 0.1), true).is_none(),
"a red run reported as red"
);
assert!(
audit_reason(&a(0.2, 0.9, 0.95), false).is_some(),
"work put off"
);
let reply = serde_json::json!({"answers": {
"claims_complete": {"noul": 0.9}, "tests_green": {"noul": 0.1}, "deferral": {"noul": 0.0}}});
assert_eq!(parse_audit(&reply), Some(a(0.9, 0.1, 0.0)));
assert_eq!(
audit_request("m", "s")["questions"]
.as_object()
.unwrap()
.len(),
3
);
}
}