use std::path::{Path, PathBuf};
use anyhow::Context;
use crate::hymns::{Arm, Band, Provenance, SealSet, Snippet};
use clap::Subcommand;
#[derive(Subcommand)]
pub(crate) enum PromptCommand {
Resolve {
#[arg(long, required = true)]
role: String,
#[arg(long)]
harness: Option<String>,
#[arg(long)]
model: Vec<String>,
#[arg(long)]
arm: Option<String>,
#[arg(long)]
stage: Option<String>,
#[arg(long, value_name = "BAND")]
band: Vec<String>,
#[arg(long)]
json: bool,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
#[clap(name = "model-keys")]
ModelKeys {
#[arg(long)]
harness: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Explain {
#[arg(long, required = true)]
role: String,
#[arg(long)]
harness: Option<String>,
#[arg(long)]
model: Vec<String>,
#[arg(long)]
arm: Option<String>,
#[arg(long)]
stage: Option<String>,
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
Check {
#[arg(short = 'p', long)]
path: Option<PathBuf>,
},
}
pub(crate) fn dispatch(cmd: PromptCommand, command_map: fn() -> String) -> anyhow::Result<()> {
use std::io::Write;
match cmd {
PromptCommand::Resolve {
role,
harness,
model,
arm,
stage,
band,
json,
path,
} => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let disk_root = root.join(".doctrine").join(crate::install::HYMNS_DIRNAME);
let embedded = crate::install::embedded_hymns();
let sealed = crate::install::embedded_seal_set()?;
let corpus = crate::install::load_full_corpus(&disk_root, &embedded, &sealed)?;
let exec = crate::boot::resolve_exec()?;
let universal = crate::boot::resolve_universal_snapshot(&root, &exec, command_map)?;
let ctx = build_ctx(&role, harness, model, arm.as_deref(), stage, &band)?;
let hymns = crate::hymns::resolve(&ctx, &corpus, &sealed)?;
let cascade = format!("{}\n{hymns}", universal.trim_end());
if json {
write!(
std::io::stdout(),
"{}",
crate::boot::session_start_hook_json(&cascade)?
)?;
} else {
writeln!(std::io::stdout(), "{cascade}")?;
}
Ok(())
}
PromptCommand::ModelKeys { harness, path } => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let labels = model_keys(&root, harness.as_deref())?;
let mut stdout = std::io::stdout();
for label in labels {
writeln!(stdout, "{label}")?;
}
Ok(())
}
PromptCommand::Explain {
role,
harness,
model,
arm,
stage,
path,
} => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let disk_root = root.join(".doctrine").join(crate::install::HYMNS_DIRNAME);
let embedded = crate::install::embedded_hymns();
let sealed = crate::install::embedded_seal_set()?;
let corpus = crate::install::load_full_corpus(&disk_root, &embedded, &sealed)?;
let ctx = build_ctx(&role, harness, model, arm.as_deref(), stage, &[])?;
let mut active: Vec<&Snippet> = corpus
.iter()
.filter(|s| !(s.provenance == Provenance::User && sealed.0.contains(&s.slot)))
.filter(|s| ctx.bands.includes(s.slot.band))
.filter(|s| crate::hymns::matches(&s.selector, &ctx))
.collect();
active.sort_by_key(|s| crate::hymns::precedence_key(s));
let mut stdout = std::io::stdout();
for (rank, s) in active.iter().enumerate() {
let winner = rank == active.len() - 1;
let spec = crate::hymns::specificity(s.slot.band, &s.selector);
let pairs = spec
.0
.iter()
.map(|(pair_root, depth)| format!("{pair_root}:{depth}"))
.collect::<Vec<_>>()
.join(",");
let prov_str = match s.provenance {
crate::hymns::Provenance::Framework => "Framework",
crate::hymns::Provenance::User => "User",
};
writeln!(
stdout,
"{:<50} prov={prov_str} spec=([{pairs}],{}) rank={}{}",
s.slot.path(),
spec.1,
rank,
if winner { " ★ WINNER" } else { "" }
)?;
}
Ok(())
}
PromptCommand::Check { path } => {
let root = crate::root::find(path, &crate::root::default_markers())?;
let disk_root = root.join(".doctrine").join(crate::install::HYMNS_DIRNAME);
let embedded = crate::install::embedded_hymns();
let sealed = crate::install::embedded_seal_set()?;
let corpus = crate::install::load_full_corpus(&disk_root, &embedded, &sealed)?;
let problems = check_corpus(&corpus, &sealed);
if problems.is_empty() {
writeln!(std::io::stdout(), "check: corpus OK")?;
} else {
for p in &problems {
writeln!(std::io::stderr(), "{p}")?;
}
anyhow::bail!("{} problem(s) found", problems.len());
}
Ok(())
}
}
}
fn build_ctx(
role: &str,
harness: Option<String>,
model: Vec<String>,
arm: Option<&str>,
stage: Option<String>,
band: &[String],
) -> anyhow::Result<crate::hymns::ContextVector> {
let role = crate::install::parse_role(role)?;
let arm: Option<Arm> = arm.map(crate::install::parse_arm).transpose()?;
let bands = if band.is_empty() {
crate::hymns::BandFilter::All
} else {
let mut set = std::collections::BTreeSet::new();
for seg in band {
let b = Band::from_segment(seg).with_context(|| format!("unknown band {seg:?}"))?;
set.insert(b);
}
crate::hymns::BandFilter::Only(set)
};
let model = model
.into_iter()
.collect::<std::collections::BTreeSet<String>>();
Ok(crate::hymns::ContextVector {
role,
harness,
model,
arm,
stage,
bands,
})
}
pub(crate) fn model_keys(root: &Path, harness: Option<&str>) -> anyhow::Result<Vec<String>> {
let disk_root = root.join(".doctrine").join(crate::install::HYMNS_DIRNAME);
let embedded = crate::install::embedded_hymns();
let sealed = crate::install::embedded_seal_set()?;
let corpus = crate::install::load_full_corpus(&disk_root, &embedded, &sealed)?;
let mut labels: Vec<String> = corpus
.iter()
.filter(|s| s.slot.band == Band::Model)
.filter(|s| match harness {
Some(h) => s.selector.harness.is_none() || s.selector.harness.as_deref() == Some(h),
None => true,
})
.map(|s| s.slot.label.clone())
.collect();
labels.sort_unstable();
labels.dedup();
Ok(labels)
}
pub(crate) fn check_corpus(corpus: &[Snippet], sealed: &SealSet) -> Vec<String> {
let mut problems = Vec::new();
if let Err(e) = crate::hymns::validate_replaces(corpus) {
problems.push(format!("replaces graph: {e}"));
}
let known: std::collections::BTreeSet<&str> =
crate::install::KNOWN_STAGE_LABELS.iter().copied().collect();
for s in corpus {
if s.slot.band == Band::Stage && !known.contains(s.slot.label.as_str()) {
problems.push(format!(
"unknown stage label {:?} in slot {}",
s.slot.label,
s.slot.path()
));
}
}
for slot in &sealed.0 {
let present = corpus
.iter()
.any(|s| s.slot == *slot && s.provenance == Provenance::Framework);
if !present {
problems.push(format!(
"sealed slot {} has no Framework snippet",
slot.path()
));
}
}
for s in corpus {
if s.provenance == Provenance::User && sealed.0.contains(&s.slot) {
problems.push(format!(
"User snippet occupies sealed slot {}",
s.slot.path()
));
}
}
for (name, bytes) in crate::install::embedded_agent_defs() {
let Ok(def_text) = std::str::from_utf8(&bytes) else {
continue;
};
if !def_text.contains(crate::install::WORKER_RESOLVE_MARKER) {
continue;
}
match crate::install::resolve_worker_role_body(corpus, sealed) {
Ok(body) if !body.trim().is_empty() => {}
Ok(_) | Err(_) => {
problems.push(format!(
"def {name}: marker present but role band unresolvable"
));
}
}
}
problems
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hymns::{Selector, Slot};
fn corpus_with_resolvable_worker_role(mut corpus: Vec<Snippet>) -> Vec<Snippet> {
corpus.push(Snippet {
slot: Slot::new(Band::Role, "worker"),
selector: Selector {
role: Some(crate::hymns::Role::Worker),
..Default::default()
},
provenance: Provenance::Framework,
body: "worker body".into(),
});
corpus
}
#[test]
fn check_duplicate_replaces_target_is_flagged() {
let target = Slot::new(Band::Project, "target");
let corpus = corpus_with_resolvable_worker_role(vec![
Snippet {
slot: Slot::new(Band::Project, "a"),
selector: Selector {
replaces: Some(target.clone()),
..Default::default()
},
provenance: Provenance::Framework,
body: "A".into(),
},
Snippet {
slot: Slot::new(Band::Project, "b"),
selector: Selector {
replaces: Some(target.clone()),
..Default::default()
},
provenance: Provenance::Framework,
body: "B".into(),
},
]);
let sealed = SealSet::default();
let problems = check_corpus(&corpus, &sealed);
assert!(
problems
.iter()
.any(|problem| problem.contains("two active snippets")),
"expected DuplicateTarget, got: {problems:?}"
);
}
#[test]
fn check_unknown_stage_label_is_flagged() {
let corpus = corpus_with_resolvable_worker_role(vec![Snippet {
slot: Slot::new(Band::Stage, "bogus-stage"),
selector: Selector {
stage: Some("bogus-stage".into()),
..Default::default()
},
provenance: Provenance::Framework,
body: "X".into(),
}]);
let sealed = SealSet::default();
let problems = check_corpus(&corpus, &sealed);
assert!(
problems
.iter()
.any(|problem| problem.contains("unknown stage label")),
"expected unknown stage, got: {problems:?}"
);
}
#[test]
fn check_clean_corpus_passes() {
let corpus = corpus_with_resolvable_worker_role(vec![Snippet {
slot: Slot::new(Band::Preamble, "core"),
selector: Selector::default(),
provenance: Provenance::Framework,
body: "FW".into(),
}]);
let sealed = SealSet::default();
let problems = check_corpus(&corpus, &sealed);
assert!(problems.is_empty(), "expected clean, got {:?}", problems);
}
#[test]
fn check_def_marker_present_role_unresolvable_is_flagged() {
let problems = check_corpus(&[], &SealSet::default());
let has_dispatch_worker = problems.iter().any(|problem| {
problem.contains("dispatch-worker")
&& problem.contains("marker present but role band unresolvable")
});
assert!(
has_dispatch_worker,
"expected marker resolve problem, got {problems:?}"
);
}
#[test]
fn explain_framework_exact_model_outranks_user_vendor_default() {
use crate::hymns;
let fw_exact = Snippet {
slot: Slot::new(Band::Model, "anthropic/claude-sonnet-4"),
selector: Selector {
model: ["anthropic/claude-sonnet-4".into()].into(),
..Default::default()
},
provenance: Provenance::Framework,
body: "FW-EXACT".into(),
};
let user_default = Snippet {
slot: Slot::new(Band::Model, "anthropic/_default"),
selector: Selector {
model: ["anthropic/_default".into()].into(),
..Default::default()
},
provenance: Provenance::User,
body: "USER-DEFAULT".into(),
};
let fw_spec = hymns::specificity(fw_exact.slot.band, &fw_exact.selector);
let user_spec = hymns::specificity(user_default.slot.band, &user_default.selector);
assert!(
fw_spec > user_spec,
"expected ([(anthropic,2)],0) > ([(anthropic,1)],0)"
);
let mut active: Vec<&Snippet> = vec![&fw_exact, &user_default];
active.sort_by_key(|s| hymns::precedence_key(s));
assert_eq!(active[0].body, "USER-DEFAULT");
assert_eq!(active[1].body, "FW-EXACT");
}
}