use crate::prose::ProseLanguage;
use super::QuestionCategory;
use super::TraditionLens;
use super::corpus::questions_for;
use super::lens::suggest_lenses;
pub(crate) const THEOLOGIAN_SYSTEM: &str = "You are a reader who approaches a manuscript through the \
lenses of the world's major moral and theological traditions — Catholic, Protestant, Orthodox, \
Gnostic, LDS, Islam, Judaism, Hinduism, Buddhism, Confucianism, and secular moral philosophy — not to \
judge the work by any of them, but to ask what each of them sees, and what those different visions \
reveal about what the work is doing.\n\n\
You belong to no tradition and advocate for none. When you raise a Buddhist question you are not \
recommending Buddhism; when you raise a Gnostic question you are not advocating Gnosticism. Each \
tradition is a lens you pick up and put down. You are always explicit about which tradition raises \
which question, and you always invite the author to say that a lens is irrelevant to their intention \
— that is useful information too.\n\n\
You assume neither that the author is religious nor that the manuscript intends theological content; \
every work has an implicit moral cosmology, and you make it visible whether or not the author placed \
it there consciously. You gloss every tradition-specific term inline so the author needs no prior \
knowledge. You do not adjudicate disputes between traditions, and you do not check works for \
doctrinal correctness.\n\n\
You ask questions and offer observations. You NEVER deliver a verdict, never tell the author their \
work is wrong, sinful, or deficient by any tradition's standard, and never prescribe a change. \
Everything you produce is a question or an invitation to reflection.";
pub(crate) fn language_name(lang: &ProseLanguage) -> &'static str {
match lang {
ProseLanguage::En => "English",
ProseLanguage::Ru => "Russian",
ProseLanguage::De => "German",
ProseLanguage::Fr => "French",
ProseLanguage::Es => "Spanish",
ProseLanguage::Other(_) => "English",
}
}
pub(crate) fn available_lenses(disabled: &[String]) -> Vec<TraditionLens> {
TraditionLens::ALL
.into_iter()
.filter(|l| !disabled.iter().any(|d| d.eq_ignore_ascii_case(l.as_code())))
.collect()
}
pub(crate) fn build_discovery_prompt(passage: &str, disabled: &[String]) -> String {
let avail = available_lenses(disabled);
let menu = avail
.iter()
.map(|l| format!("{} ({})", l.label(), l.as_code()))
.collect::<Vec<_>>()
.join(", ");
let suggested: Vec<_> = suggest_lenses(passage)
.into_iter()
.filter(|l| avail.contains(l))
.collect();
let hint = if suggested.is_empty() {
String::new()
} else {
format!(
"Surface markers point especially toward: {}. Weigh them, but you are not bound by them.\n\n",
suggested.iter().map(|l| l.as_code()).collect::<Vec<_>>().join(", ")
)
};
format!(
"You are about to read a manuscript passage through the lenses of the world's moral and \
theological traditions. First, decide which lenses are worth bringing to bear.\n\n\
Available lenses (code in parentheses): {menu}.\n\n\
{hint}\
Consider the passage through ALL of them. Then select the two to four whose questions would \
be GENUINELY most illuminating for THIS specific passage — vary by passage, do not default \
to a fixed set. Also note up to two lenses whose SILENCE is telling here (a moral question \
this work conspicuously never raises).\n\n\
Return ONLY this JSON, using the codes above:\n\
{{\"selected\":[\"code\",...],\"silent\":[\"code\",...]}}\n\n\
PASSAGE:\n{passage}"
)
}
pub(crate) fn parse_selected_lenses(raw: &str) -> (Vec<TraditionLens>, Vec<TraditionLens>) {
let json = super::llm::extract_json_object(raw);
let v: serde_json::Value = match serde_json::from_str(json) {
Ok(v) => v,
Err(_) => return (Vec::new(), Vec::new()),
};
let codes = |key: &str| -> Vec<TraditionLens> {
v.get(key)
.and_then(|x| x.as_array())
.map(|arr| {
arr.iter()
.filter_map(|c| c.as_str())
.filter_map(TraditionLens::from_code)
.collect::<Vec<_>>()
})
.unwrap_or_default()
};
(codes("selected"), codes("silent"))
}
pub(crate) fn build_session_prompt(
category: QuestionCategory,
passage: &str,
selected: &[TraditionLens],
silent: &[TraditionLens],
grounding_prefix: Option<&str>,
lang: &ProseLanguage,
) -> String {
let lens_instruction = if selected.is_empty() {
"Choose the two to four tradition lenses most illuminating for this passage yourself."
.to_string()
} else {
format!(
"Bring these lenses, chosen as most illuminating for this passage: {}.",
selected.iter().map(|l| l.label()).collect::<Vec<_>>().join(", ")
)
};
let silent_note = if silent.is_empty() {
String::new()
} else {
format!(
" Where it is telling, you may also name the silence of: {}.",
silent.iter().map(|l| l.label()).collect::<Vec<_>>().join(", ")
)
};
let qs = questions_for(category)
.iter()
.enumerate()
.map(|(i, q)| format!("{}. {q}", i + 1))
.collect::<Vec<_>>()
.join("\n");
let language = language_name(lang);
let grounding = grounding_prefix.map(|g| format!("{}\n\n", g.trim())).unwrap_or_default();
format!(
"{grounding}Category {} — {}.\n\n\
{lens_instruction}{silent_note} Name which tradition raises which question, and invite the \
author to say any lens is irrelevant to their intention.\n\n\
Question templates for this category (adapt them to this passage — do not recite verbatim):\n\
{qs}\n\n\
Pose two to four questions. Write them in {language}. Gloss every tradition-specific term \
inline. Ask only — never judge, never prescribe.\n\n\
PASSAGE:\n{passage}",
category.number(),
category.label(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_states_neutrality_and_no_verdict() {
assert!(THEOLOGIAN_SYSTEM.contains("belong to no tradition"));
assert!(THEOLOGIAN_SYSTEM.contains("NEVER deliver a verdict"));
}
#[test]
fn language_names_cover_all() {
assert_eq!(language_name(&ProseLanguage::Ru), "Russian");
assert_eq!(language_name(&ProseLanguage::Other("pl".into())), "English");
}
#[test]
fn analysis_prompt_uses_selected_lenses_language_grounding() {
let p = build_session_prompt(
QuestionCategory::MoralWeight,
"He gave his life as a sacrifice for the others.",
&[TraditionLens::Orthodox, TraditionLens::Catholic],
&[TraditionLens::Confucianism],
Some("GROUNDING: a stalled redemption arc was declared for Mara."),
&ProseLanguage::Fr,
);
assert!(p.contains("Category 1 — Moral weight"));
assert!(p.contains("Write them in French"));
assert!(p.contains("GROUNDING:"));
assert!(p.contains("Orthodox") && p.contains("Catholic"));
assert!(p.contains("silence of: Confucianism"));
assert!(p.contains("PASSAGE:"));
}
#[test]
fn discovery_prompt_lists_all_available_and_requests_json() {
let p = build_discovery_prompt("He gave his life as a sacrifice.", &["gnostic".into()]);
assert!(p.contains("\"selected\""));
assert!(p.contains("Buddhism (buddhism)"));
assert!(!p.contains("(gnostic)"));
}
#[test]
fn parse_selected_tolerant() {
let (sel, sil) = parse_selected_lenses(
"sure: {\"selected\":[\"orthodox\",\"judaism\",\"nope\"],\"silent\":[\"islam\"]}",
);
assert_eq!(sel, vec![TraditionLens::Orthodox, TraditionLens::Judaism]);
assert_eq!(sil, vec![TraditionLens::Islam]);
assert_eq!(parse_selected_lenses("not json"), (vec![], vec![]));
}
}