use std::collections::{BTreeMap, BTreeSet};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::domain::version::Version;
use crate::errors::{Error, Result};
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct FileFilter {
#[serde(default)]
pub include: Vec<String>,
#[serde(default)]
pub exclude: Vec<String>,
}
impl FileFilter {
pub fn is_empty(&self) -> bool {
self.include.is_empty() && self.exclude.is_empty()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct OneharnessCfg {
#[serde(default)]
pub config: Vec<String>,
#[serde(default)]
pub bin: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
#[schemars(range(min = 1))]
pub timeout: Option<u64>,
#[serde(default)]
pub schema_max_retries: Option<u32>,
}
impl OneharnessCfg {
pub fn merge_under(&mut self, other: OneharnessCfg) {
if self.config.is_empty() {
self.config = other.config;
}
self.bin = self.bin.take().or(other.bin);
self.model = self.model.take().or(other.model);
self.timeout = self.timeout.or(other.timeout);
self.schema_max_retries = self.schema_max_retries.or(other.schema_max_retries);
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Agent {
#[serde(default)]
pub harness: Option<String>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
#[schemars(range(min = 1))]
pub batch_size: Option<usize>,
#[serde(default)]
pub prompt_template: Option<String>,
#[serde(default)]
pub files: Option<FileFilter>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(untagged)]
pub enum Relevance {
Always(bool),
When(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RelevanceMode {
Always,
Never,
Conditional(String),
}
fn is_false(b: &bool) -> bool {
!*b
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct Rule {
#[schemars(regex(pattern = r"^[A-Za-z][A-Za-z0-9_]*$"))]
pub name: String,
#[serde(default)]
#[schemars(length(min = 1))]
pub description: String,
#[serde(default, rename = "override", skip_serializing_if = "is_false")]
pub r#override: bool,
#[serde(default)]
pub agent: Option<String>,
#[serde(default)]
#[schemars(range(min = 1))]
pub judges: Option<u32>,
#[serde(default)]
pub files: Option<FileFilter>,
#[serde(default)]
pub rationale: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub relevance: Option<Relevance>,
}
impl Rule {
pub fn judges(&self) -> u32 {
self.judges.unwrap_or(1)
}
pub fn wants_rationale(&self, session_default: bool) -> bool {
self.rationale.unwrap_or(session_default)
}
pub fn relevance_mode(&self) -> RelevanceMode {
match &self.relevance {
None | Some(Relevance::Always(true)) => RelevanceMode::Always,
Some(Relevance::Always(false)) => RelevanceMode::Never,
Some(Relevance::When(cond)) => RelevanceMode::Conditional(cond.clone()),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
#[schemars(
title = "llmlint configuration",
description = "Configuration for llmlint, an LLM-as-judge linter for code-quality checks \
deterministic linters can't express. Docs: https://github.com/nickderobertis/llmlint"
)]
pub struct Config {
#[serde(default)]
pub version: Option<Version>,
#[serde(default)]
pub prompt_template: Option<String>,
#[serde(default)]
pub files: FileFilter,
#[serde(default)]
pub oneharness: OneharnessCfg,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rationales: Option<bool>,
#[serde(default)]
pub plugins: Vec<String>,
#[serde(default)]
pub agents: BTreeMap<String, Agent>,
#[serde(default)]
pub rules: Vec<Rule>,
}
impl Config {
pub fn merge_plugin(&mut self, other: Config) {
self.version = self.version.take().or(other.version);
self.prompt_template = self.prompt_template.take().or(other.prompt_template);
if self.files.is_empty() {
self.files = other.files;
}
self.oneharness.merge_under(other.oneharness);
self.rationales = self.rationales.or(other.rationales);
for (name, agent) in other.agents {
self.agents.entry(name).or_insert(agent);
}
self.rules.extend(other.rules);
}
pub fn agent_or_default(&self, name: &str) -> Agent {
self.agents.get(name).cloned().unwrap_or_default()
}
pub fn rationales_default(&self) -> bool {
self.rationales.unwrap_or(true)
}
}
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct Provenance {
pub settings: BTreeMap<String, String>,
pub agents: BTreeMap<String, String>,
pub rules: BTreeMap<String, RuleProvenance>,
}
pub const SETTING_KEYS: &[&str] = &[
"version",
"prompt_template",
"files",
"oneharness.config",
"oneharness.bin",
"oneharness.model",
"oneharness.timeout",
"oneharness.schema_max_retries",
"rationales",
];
const RULE_FIELDS: &[&str] = &[
"name",
"description",
"agent",
"judges",
"files",
"rationale",
"relevance",
];
#[derive(Debug, Clone, Default, Serialize, PartialEq, Eq)]
pub struct RuleProvenance {
pub source: String,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
pub fields: BTreeMap<String, String>,
}
#[derive(Debug, Default)]
pub struct ProvenanceBuilder {
settings: BTreeMap<String, String>,
agents: BTreeMap<String, String>,
rule_occurrences: BTreeMap<String, Vec<(String, Rule)>>,
}
impl ProvenanceBuilder {
pub fn record(&mut self, cfg: &Config, origin: &str) {
self.record_settings(cfg, origin);
self.record_items(cfg, origin);
}
pub fn record_items(&mut self, cfg: &Config, origin: &str) {
for name in cfg.agents.keys() {
self.agents
.entry(name.clone())
.or_insert_with(|| origin.to_string());
}
for rule in &cfg.rules {
self.rule_occurrences
.entry(rule.name.clone())
.or_default()
.push((origin.to_string(), rule.clone()));
}
}
fn record_settings(&mut self, cfg: &Config, origin: &str) {
let settings: &[(&str, bool)] = &[
("version", cfg.version.is_some()),
("prompt_template", cfg.prompt_template.is_some()),
("files", !cfg.files.is_empty()),
("oneharness.config", !cfg.oneharness.config.is_empty()),
("oneharness.bin", cfg.oneharness.bin.is_some()),
("oneharness.model", cfg.oneharness.model.is_some()),
("oneharness.timeout", cfg.oneharness.timeout.is_some()),
(
"oneharness.schema_max_retries",
cfg.oneharness.schema_max_retries.is_some(),
),
("rationales", cfg.rationales.is_some()),
];
for (key, present) in settings {
if *present {
self.settings
.entry((*key).to_string())
.or_insert_with(|| origin.to_string());
}
}
}
pub fn finish(self) -> Provenance {
let mut rules: BTreeMap<String, RuleProvenance> = BTreeMap::new();
for (name, occ) in self.rule_occurrences {
rules.insert(name, rule_provenance(&occ));
}
Provenance {
settings: self.settings,
agents: self.agents,
rules,
}
}
}
fn rule_provenance(occ: &[(String, Rule)]) -> RuleProvenance {
let bases: Vec<&(String, Rule)> = occ.iter().filter(|(_, r)| !r.r#override).collect();
let base = match bases.as_slice() {
[b] => *b,
_ => {
return RuleProvenance {
source: occ.first().map(|(s, _)| s.clone()).unwrap_or_default(),
fields: BTreeMap::new(),
}
}
};
let base_src = base.0.as_str();
let overrides: Vec<&Rule> = occ
.iter()
.filter(|(_, r)| r.r#override)
.map(|(_, r)| r)
.collect();
let override_srcs: Vec<&str> = occ
.iter()
.filter(|(_, r)| r.r#override)
.map(|(s, _)| s.as_str())
.collect();
let w = field_winners(&overrides);
let mut fields: BTreeMap<String, String> = BTreeMap::new();
let mut note = |field: &str, present: bool, winner: Option<usize>| {
if !present {
return;
}
let src = winner.map(|i| override_srcs[i]).unwrap_or(base_src);
if src != base_src {
fields.insert(field.to_string(), src.to_string());
}
};
note("description", true, w.description);
note(
"agent",
base.1.agent.is_some() || w.agent.is_some(),
w.agent,
);
note(
"judges",
base.1.judges.is_some() || w.judges.is_some(),
w.judges,
);
note(
"files",
base.1.files.is_some() || w.files.is_some(),
w.files,
);
note(
"rationale",
base.1.rationale.is_some() || w.rationale.is_some(),
w.rationale,
);
note(
"relevance",
base.1.relevance.is_some() || w.relevance.is_some(),
w.relevance,
);
RuleProvenance {
source: base_src.to_string(),
fields,
}
}
pub fn resolve_source(prov: &Provenance, path: &str) -> std::result::Result<String, String> {
if let Some(name) = path.strip_prefix("agents.") {
return prov.agents.get(name).cloned().ok_or_else(|| {
format!(
"no agent named {name:?} in the merged config (agents: {})",
list_or_none(prov.agents.keys())
)
});
}
if let Some(rest) = path.strip_prefix("rules.") {
let mut parts = rest.splitn(2, '.');
let name = parts.next().unwrap_or("");
let field = parts.next();
let rule = prov.rules.get(name).ok_or_else(|| {
format!(
"no rule named {name:?} in the merged config (rules: {})",
list_or_none(prov.rules.keys())
)
})?;
return match field {
None => Ok(rule.source.clone()),
Some(f) if RULE_FIELDS.contains(&f) => {
Ok(rule
.fields
.get(f)
.cloned()
.unwrap_or_else(|| rule.source.clone()))
}
Some(f) => Err(format!(
"unknown rule field {f:?}; valid fields: {}",
RULE_FIELDS.join(", ")
)),
};
}
if SETTING_KEYS.contains(&path) {
return prov.settings.get(path).cloned().ok_or_else(|| {
format!(
"`{path}` is not set by any config; the built-in default applies (nothing to edit)"
)
});
}
Err(format!(
"unknown config path {path:?}; expected a setting (e.g. `oneharness.model`, `version`), \
`agents.<name>`, `rules.<name>`, or `rules.<name>.<field>`"
))
}
fn list_or_none<'a>(keys: impl Iterator<Item = &'a String>) -> String {
let mut names: Vec<&str> = keys.map(String::as_str).collect();
names.sort_unstable();
if names.is_empty() {
"none".to_string()
} else {
names.join(", ")
}
}
pub fn resolve_overrides(config: &mut Config) -> Result<()> {
let mut order: Vec<&str> = Vec::new();
let mut groups: BTreeMap<&str, Vec<usize>> = BTreeMap::new();
for (i, r) in config.rules.iter().enumerate() {
let group = groups.entry(r.name.as_str()).or_default();
if group.is_empty() {
order.push(r.name.as_str());
}
group.push(i);
}
let mut problems: Vec<String> = Vec::new();
let mut resolved: Vec<Rule> = Vec::new();
for name in &order {
let idxs = &groups[name];
let bases: Vec<&Rule> = idxs
.iter()
.map(|&i| &config.rules[i])
.filter(|r| !r.r#override)
.collect();
let overrides: Vec<&Rule> = idxs
.iter()
.map(|&i| &config.rules[i])
.filter(|r| r.r#override)
.collect();
match bases.as_slice() {
[] => problems.push(format!(
"rule {name:?} is marked `override` but no base rule with that name is defined \
(a plugin must declare it first)"
)),
[base] if overrides.is_empty() => resolved.push((*base).clone()),
[base] => resolved.push(merge_override(base, &overrides)),
_ => problems.push(format!(
"duplicate rule name {name:?} (set `override: true` to extend a plugin's rule \
instead of redefining it)"
)),
}
}
if problems.is_empty() {
config.rules = resolved;
Ok(())
} else {
Err(Error::InvalidConfig(problems.join("; ")))
}
}
struct FieldWinners {
description: Option<usize>,
agent: Option<usize>,
judges: Option<usize>,
files: Option<usize>,
rationale: Option<usize>,
relevance: Option<usize>,
}
fn field_winners(overrides: &[&Rule]) -> FieldWinners {
FieldWinners {
description: overrides
.iter()
.position(|ov| !ov.description.trim().is_empty()),
agent: overrides.iter().position(|ov| ov.agent.is_some()),
judges: overrides.iter().position(|ov| ov.judges.is_some()),
files: overrides.iter().position(|ov| ov.files.is_some()),
rationale: overrides.iter().position(|ov| ov.rationale.is_some()),
relevance: overrides.iter().position(|ov| ov.relevance.is_some()),
}
}
fn merge_override(base: &Rule, overrides: &[&Rule]) -> Rule {
let w = field_winners(overrides);
let mut out = base.clone();
out.r#override = false;
if let Some(i) = w.description {
out.description = overrides[i].description.clone();
}
if let Some(i) = w.agent {
out.agent = overrides[i].agent.clone();
}
if let Some(i) = w.judges {
out.judges = overrides[i].judges;
}
if let Some(i) = w.files {
out.files = overrides[i].files.clone();
}
if let Some(i) = w.rationale {
out.rationale = overrides[i].rationale;
}
if let Some(i) = w.relevance {
out.relevance = overrides[i].relevance.clone();
}
out
}
pub fn is_valid_rule_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
pub fn validate(config: &Config) -> Result<()> {
let mut problems: Vec<String> = Vec::new();
let mut seen: BTreeSet<&str> = BTreeSet::new();
for rule in &config.rules {
if !is_valid_rule_name(&rule.name) {
problems.push(format!(
"rule name {:?} is not a valid identifier (letters, digits, underscore; \
must start with a letter)",
rule.name
));
}
if !seen.insert(rule.name.as_str()) {
problems.push(format!("duplicate rule name {:?}", rule.name));
}
if rule.description.trim().is_empty() {
problems.push(format!("rule {:?} has an empty description", rule.name));
}
if let Some(Relevance::When(cond)) = &rule.relevance {
if cond.trim().is_empty() {
problems.push(format!(
"rule {:?} has an empty relevance condition (use `true`/`false` for an \
always/never rule, or a non-empty condition)",
rule.name
));
}
}
if rule.judges == Some(0) {
problems.push(format!("rule {:?} has judges: 0 (must be >= 1)", rule.name));
} else if let Some(judges) = rule.judges {
if judges % 2 == 0 {
problems.push(format!(
"rule {:?} has judges: {} (must be odd so the majority verdict can't tie)",
rule.name, judges
));
}
}
if let Some(agent) = &rule.agent {
if !config.agents.contains_key(agent) {
problems.push(format!(
"rule {:?} references unknown agent {:?}",
rule.name, agent
));
}
}
}
for (name, agent) in &config.agents {
if agent.batch_size == Some(0) {
problems.push(format!("agent {:?} has batch_size: 0 (must be >= 1)", name));
}
}
if problems.is_empty() {
Ok(())
} else {
Err(Error::InvalidConfig(problems.join("; ")))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rule(name: &str) -> Rule {
Rule {
name: name.into(),
description: "true when ok; false otherwise.".into(),
r#override: false,
agent: None,
judges: None,
files: None,
rationale: None,
relevance: None,
}
}
#[test]
fn valid_config_passes() {
let c = Config {
rules: vec![rule("alpha_rule"), rule("beta_rule")],
..Default::default()
};
assert!(validate(&c).is_ok());
}
#[test]
fn name_validation_rules() {
assert!(is_valid_rule_name("good_name1"));
assert!(!is_valid_rule_name("1leading_digit"));
assert!(!is_valid_rule_name("has-dash"));
assert!(!is_valid_rule_name(""));
assert!(!is_valid_rule_name("with space"));
}
#[test]
fn collects_duplicate_invalid_and_unknown_agent() {
let mut bad = rule("dup");
bad.agent = Some("missing".into());
let c = Config {
rules: vec![
rule("dup"),
bad,
rule("bad-name"),
Rule {
judges: Some(0),
..rule("zero_judges")
},
],
..Default::default()
};
let err = validate(&c).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("duplicate rule name"));
assert!(msg.contains("unknown agent"));
assert!(msg.contains("not a valid identifier"));
assert!(msg.contains("judges: 0"));
}
#[test]
fn empty_description_is_invalid() {
let c = Config {
rules: vec![Rule {
description: " ".into(),
..rule("empty_desc")
}],
..Default::default()
};
assert!(validate(&c).is_err());
}
#[test]
fn merge_keeps_root_agent_and_appends_rules() {
let mut root = Config {
rules: vec![rule("root_rule")],
..Default::default()
};
root.agents.insert(
"shared".into(),
Agent {
harness: Some("claude-code".into()),
..Default::default()
},
);
let mut other = Config {
rules: vec![rule("plugin_rule")],
..Default::default()
};
other.agents.insert(
"shared".into(),
Agent {
harness: Some("codex".into()),
..Default::default()
},
);
root.merge_plugin(other);
assert_eq!(root.rules.len(), 2);
assert_eq!(
root.agents["shared"].harness.as_deref(),
Some("claude-code")
);
}
#[test]
fn merge_top_level_scalars_keep_root_then_fall_back_to_plugin() {
let mut root = Config {
prompt_template: Some("root template".into()),
rationales: Some(false),
..Default::default()
};
root.oneharness.model = Some("opus".into());
let plugin = Config {
prompt_template: Some("plugin template".into()),
rationales: Some(true),
files: FileFilter {
include: vec!["src/**".into()],
exclude: vec![],
},
oneharness: OneharnessCfg {
model: Some("haiku".into()),
timeout: Some(99),
..Default::default()
},
..Default::default()
};
root.merge_plugin(plugin);
assert_eq!(root.prompt_template.as_deref(), Some("root template"));
assert_eq!(root.rationales, Some(false));
assert_eq!(root.oneharness.model.as_deref(), Some("opus"));
assert_eq!(root.files.include, vec!["src/**".to_string()]);
assert_eq!(root.oneharness.timeout, Some(99));
}
#[test]
fn rationales_default_is_true_when_unset() {
assert!(Config::default().rationales_default());
let off = Config {
rationales: Some(false),
..Default::default()
};
assert!(!off.rationales_default());
}
#[test]
fn per_rule_rationale_overrides_session_default() {
let r = rule("r");
assert!(r.wants_rationale(true));
assert!(!r.wants_rationale(false));
let forced_on = Rule {
rationale: Some(true),
..rule("on")
};
let forced_off = Rule {
rationale: Some(false),
..rule("off")
};
assert!(forced_on.wants_rationale(false));
assert!(!forced_off.wants_rationale(true));
}
#[test]
fn even_judges_is_invalid() {
let c = Config {
rules: vec![Rule {
judges: Some(2),
..rule("even_judges")
}],
..Default::default()
};
let err = validate(&c).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("must be odd"), "got: {msg}");
}
#[test]
fn odd_judges_is_valid() {
let c = Config {
rules: vec![Rule {
judges: Some(3),
..rule("odd_judges")
}],
..Default::default()
};
assert!(validate(&c).is_ok());
}
#[test]
fn relevance_mode_resolves_the_default_and_the_three_forms() {
assert_eq!(rule("r").relevance_mode(), RelevanceMode::Always);
let always = Rule {
relevance: Some(Relevance::Always(true)),
..rule("r")
};
assert_eq!(always.relevance_mode(), RelevanceMode::Always);
let never = Rule {
relevance: Some(Relevance::Always(false)),
..rule("r")
};
assert_eq!(never.relevance_mode(), RelevanceMode::Never);
let when = Rule {
relevance: Some(Relevance::When("the change touches SQL".into())),
..rule("r")
};
assert_eq!(
when.relevance_mode(),
RelevanceMode::Conditional("the change touches SQL".into())
);
}
#[test]
fn empty_relevance_condition_is_invalid() {
let c = Config {
rules: vec![Rule {
relevance: Some(Relevance::When(" ".into())),
..rule("blank_relevance")
}],
..Default::default()
};
let err = validate(&c).unwrap_err();
assert!(err.to_string().contains("empty relevance condition"));
}
#[test]
fn override_inherits_unset_fields_and_replaces_set_ones() {
let base = Rule {
judges: Some(1),
..rule("style")
};
let over = Rule {
name: "style".into(),
description: String::new(), r#override: true,
agent: Some("strict".into()),
judges: Some(3),
..rule("style")
};
let mut c = Config {
rules: vec![over, base],
..Default::default()
};
resolve_overrides(&mut c).unwrap();
assert_eq!(c.rules.len(), 1);
let r = &c.rules[0];
assert_eq!(r.name, "style");
assert_eq!(r.description, "true when ok; false otherwise."); assert_eq!(r.judges, Some(3)); assert_eq!(r.agent.as_deref(), Some("strict")); assert!(!r.r#override); }
#[test]
fn override_can_replace_the_description() {
let base = rule("r");
let over = Rule {
name: "r".into(),
description: "a sharper invariant.".into(),
r#override: true,
..rule("r")
};
let mut c = Config {
rules: vec![over, base],
..Default::default()
};
resolve_overrides(&mut c).unwrap();
assert_eq!(c.rules[0].description, "a sharper invariant.");
}
#[test]
fn override_without_a_base_is_an_error() {
let mut c = Config {
rules: vec![Rule {
r#override: true,
..rule("orphan")
}],
..Default::default()
};
let err = resolve_overrides(&mut c).unwrap_err();
assert!(err.to_string().contains("no base rule"), "{err}");
}
#[test]
fn duplicate_name_without_override_is_an_error() {
let mut c = Config {
rules: vec![rule("dup"), rule("dup")],
..Default::default()
};
let err = resolve_overrides(&mut c).unwrap_err();
assert!(err.to_string().contains("duplicate rule name"), "{err}");
}
#[test]
fn nearest_root_override_wins_each_field() {
let near = Rule {
name: "r".into(),
r#override: true,
judges: Some(5),
..rule("r")
};
let far = Rule {
name: "r".into(),
r#override: true,
judges: Some(3),
agent: Some("far".into()),
..rule("r")
};
let base = rule("r");
let mut c = Config {
rules: vec![near, far, base],
..Default::default()
};
resolve_overrides(&mut c).unwrap();
assert_eq!(c.rules.len(), 1);
assert_eq!(c.rules[0].judges, Some(5)); assert_eq!(c.rules[0].agent.as_deref(), Some("far")); }
#[test]
fn override_keeps_first_occurrence_position() {
let mut c = Config {
rules: vec![
rule("a"),
Rule {
r#override: true,
judges: Some(3),
..rule("b")
},
rule("c"),
rule("b"), ],
..Default::default()
};
resolve_overrides(&mut c).unwrap();
let names: Vec<&str> = c.rules.iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, ["a", "b", "c"]);
assert_eq!(c.rules[1].judges, Some(3));
}
#[test]
fn provenance_traces_settings_agents_and_each_rule_field() {
let mut b = ProvenanceBuilder::default();
let mut near = Config {
version: Some(Version::parse("1").unwrap()),
rationales: Some(false),
files: FileFilter {
include: vec!["src/**".into()],
exclude: vec![],
},
rules: vec![
rule("a"),
Rule {
name: "shared".into(),
description: String::new(), r#override: true,
judges: Some(3),
..rule("shared")
},
],
..Default::default()
};
near.oneharness.model = Some("opus".into());
near.oneharness.config = vec!["oh.yml".into()];
near.agents.insert("g".into(), Agent::default());
let mut far = Config {
version: Some(Version::parse("2").unwrap()), prompt_template: Some("t".into()), rules: vec![rule("b"), rule("shared")],
..Default::default()
};
far.oneharness.model = Some("haiku".into()); far.oneharness.timeout = Some(9); far.oneharness.schema_max_retries = Some(2);
far.agents.insert("g".into(), Agent::default());
b.record(&near, "near.yml");
b.record(&far, "far.yml");
let prov = b.finish();
assert_eq!(prov.settings["version"], "near.yml");
assert_eq!(prov.settings["rationales"], "near.yml");
assert_eq!(prov.settings["files"], "near.yml");
assert_eq!(prov.settings["oneharness.model"], "near.yml");
assert_eq!(prov.settings["oneharness.config"], "near.yml");
assert_eq!(prov.settings["prompt_template"], "far.yml");
assert_eq!(prov.settings["oneharness.timeout"], "far.yml");
assert_eq!(prov.settings["oneharness.schema_max_retries"], "far.yml");
assert_eq!(prov.agents["g"], "near.yml");
assert!(!prov.settings.contains_key("oneharness.bin"));
assert_eq!(prov.rules["a"].source, "near.yml");
assert!(prov.rules["a"].fields.is_empty());
assert_eq!(prov.rules["b"].source, "far.yml");
assert!(prov.rules["b"].fields.is_empty());
let shared = &prov.rules["shared"];
assert_eq!(shared.source, "far.yml");
assert_eq!(shared.fields["judges"], "near.yml");
assert!(!shared.fields.contains_key("description"));
}
#[test]
fn setting_keys_match_what_record_emits() {
let mut b = ProvenanceBuilder::default();
let mut cfg = Config {
version: Some(Version::parse("1").unwrap()),
prompt_template: Some("t".into()),
rationales: Some(true),
files: FileFilter {
include: vec!["x".into()],
exclude: vec![],
},
..Default::default()
};
cfg.oneharness = OneharnessCfg {
config: vec!["c".into()],
bin: Some("b".into()),
model: Some("m".into()),
timeout: Some(1),
schema_max_retries: Some(1),
};
b.record(&cfg, "f.yml");
let prov = b.finish();
let got: BTreeSet<&str> = prov.settings.keys().map(String::as_str).collect();
let want: BTreeSet<&str> = SETTING_KEYS.iter().copied().collect();
assert_eq!(got, want);
}
#[test]
fn resolve_source_walks_settings_agents_and_rule_fields() {
let mut prov = Provenance::default();
prov.settings
.insert("oneharness.model".into(), "team.yml".into());
prov.agents.insert("security".into(), "team.yml".into());
prov.rules.insert(
"secrets".into(),
RuleProvenance {
source: "team.yml".into(),
fields: BTreeMap::from([("judges".to_string(), "root.yml".to_string())]),
},
);
assert_eq!(
resolve_source(&prov, "oneharness.model").unwrap(),
"team.yml"
);
assert_eq!(
resolve_source(&prov, "agents.security").unwrap(),
"team.yml"
);
assert_eq!(resolve_source(&prov, "rules.secrets").unwrap(), "team.yml");
assert_eq!(
resolve_source(&prov, "rules.secrets.judges").unwrap(),
"root.yml"
);
assert_eq!(
resolve_source(&prov, "rules.secrets.description").unwrap(),
"team.yml"
);
}
#[test]
fn resolve_source_errors_are_actionable() {
let mut prov = Provenance::default();
prov.agents.insert("a".into(), "f.yml".into());
prov.rules.insert(
"r".into(),
RuleProvenance {
source: "f.yml".into(),
..Default::default()
},
);
let unset = resolve_source(&prov, "oneharness.bin").unwrap_err();
assert!(unset.contains("built-in default applies"), "{unset}");
assert!(resolve_source(&prov, "agents.missing")
.unwrap_err()
.contains("agents: a"));
assert!(resolve_source(&prov, "rules.missing")
.unwrap_err()
.contains("rules: r"));
assert!(resolve_source(&prov, "rules.r.bogus")
.unwrap_err()
.contains("valid fields"));
assert!(resolve_source(&prov, "nonsense")
.unwrap_err()
.contains("expected a setting"));
}
#[test]
fn agent_batch_size_zero_is_invalid() {
let mut c = Config::default();
c.agents.insert(
"a".into(),
Agent {
batch_size: Some(0),
..Default::default()
},
);
assert!(validate(&c).is_err());
}
}