use std::path::Path;
use uuid::Uuid;
use super::{ContinuityFinding, Severity};
use crate::config::Config;
use crate::project::ProjectLayout;
use crate::store::hierarchy::Hierarchy;
use crate::store::node::{Node, NodeKind};
use crate::store::Store;
pub(crate) fn run(
project: &Path,
scope: Option<Uuid>,
max_cost: usize,
force: bool,
) -> Result<Vec<ContinuityFinding>, String> {
use crate::world::fact_check_slow::{build_coherence_prompt, magic_summary, COHERENCE_SYSTEM};
let layout = ProjectLayout::new(project);
layout.require_initialized().map_err(|e| e.to_string())?;
let cfg = Config::load_layered(&layout.config_path()).map_err(|e| e.to_string())?;
let store = Store::open(layout.clone(), &cfg).map_err(|e| e.to_string())?;
let h = Hierarchy::load(&store).map_err(|e| e.to_string())?;
let labeled = gather_paragraphs(&store, &h, scope);
if labeled.iter().all(|(_, t)| t.trim().is_empty()) {
return Ok(Vec::new());
}
let ledger = std::fs::read_to_string(layout.root.join("world.hjson"))
.ok()
.and_then(|raw| crate::world::types::WorldDefinition::from_hjson(&raw).ok())
.and_then(|d| d.magic)
.unwrap_or_default();
let magic = magic_summary(&ledger);
let (prompt, _kept) = build_coherence_prompt(&labeled, "", &magic);
let findings = crate::cli::realworld::slow_llm_call(
project,
"continuity-coherence",
COHERENCE_SYSTEM,
prompt,
max_cost,
force,
)
.map_err(|e| e.to_string())?;
Ok(findings.into_iter().enumerate().map(|(i, f)| map_finding(i, f)).collect())
}
fn gather_paragraphs(store: &Store, h: &Hierarchy, scope: Option<Uuid>) -> Vec<(String, String)> {
let ids: Vec<Uuid> = match scope {
Some(root) => h
.collect_subtree(root)
.into_iter()
.filter(|id| {
h.get(*id)
.map(|n| n.kind == NodeKind::Paragraph && n.event.is_none())
.unwrap_or(false)
})
.collect(),
None => h
.flatten()
.into_iter()
.filter(|(n, _)| n.kind == NodeKind::Paragraph && n.event.is_none())
.filter(|(n, _)| under_user_book(h, n))
.map(|(n, _)| n.id)
.collect(),
};
ids.into_iter()
.map(|id| {
let label = h.get(id).map(|n| h.slug_path(n)).unwrap_or_else(|| id.to_string());
let text = store
.get_content(id)
.ok()
.flatten()
.map(|b| String::from_utf8_lossy(&b).into_owned())
.unwrap_or_default();
(label, text)
})
.collect()
}
fn under_user_book(h: &Hierarchy, node: &Node) -> bool {
let mut cur = Some(node);
while let Some(n) = cur {
if n.kind == NodeKind::Book {
return n.system_tag.is_none();
}
cur = n.parent_id.and_then(|p| h.get(p));
}
false
}
fn map_finding(i: usize, f: crate::world::fact_check::Finding) -> ContinuityFinding {
let severity = match f.severity.as_str() {
"contradiction" => Severity::Contradiction,
"warning" => Severity::Warning,
_ => Severity::Info,
};
ContinuityFinding {
kind: "coherence",
severity,
chapter: 0,
anchor: None,
dedup_key: format!("coherence|{i}|{}", f.body),
entities: Vec::new(),
message: f.body,
source: "coherence",
}
}