use std::collections::{BTreeSet, HashMap};
use chrono::Utc;
use petgraph::visit::{EdgeRef, IntoEdgeReferences};
use crate::config::schema::WikiConfig;
use crate::generate::llm::{LlmProvider, Message};
use crate::model::{
DocumentKind, EdgeKind, KnowledgeCard, KnowledgeGraph, NodeId, Reference, WikiDocument,
};
const INDEX_RETRY_MAX: usize = 1;
struct ModuleGuideInfo {
name: String,
description: String,
dependents: Vec<String>,
dependencies: Vec<String>,
in_degree: usize,
}
pub async fn generate_index_guide<P: LlmProvider>(
provider: &P,
graph: &KnowledgeGraph,
cards: &[KnowledgeCard],
config: &WikiConfig,
) -> WikiDocument {
let infos = collect_module_infos(graph, cards);
let messages = index_guide_prompt(&infos, &config.wiki.language);
let mut last_err = None;
for _ in 0..=INDEX_RETRY_MAX {
match provider.complete(&messages).await {
Ok(content) => {
let issues = crate::output::mermaid_check::validate_mermaid_blocks(&content);
let content = if issues.is_empty() {
content
} else {
tracing::warn!(
"阅读指南含 {} 个坏 Mermaid 块,降级为 text 块",
issues.len()
);
crate::output::mermaid_check::degrade_mermaid_blocks(&content, &issues)
};
return make_document(content, config, &infos);
}
Err(e) => last_err = Some(e),
}
}
tracing::warn!(
"阅读指南 LLM 生成失败(重试 {} 次),降级为确定性骨架: {:?}",
INDEX_RETRY_MAX,
last_err
);
fallback_index_guide(graph, config)
}
pub fn fallback_index_guide(graph: &KnowledgeGraph, config: &WikiConfig) -> WikiDocument {
let infos = collect_module_infos(graph, &[]);
let mut body = String::new();
body.push_str("# 阅读指南\n\n");
body.push_str("> LLM 生成不可用,本指南为确定性骨架:按模块被依赖程度(入度中心度)降序推荐阅读顺序。\n\n");
body.push_str("## 推荐阅读顺序\n\n");
for info in &infos {
body.push_str(&format!(
"- [{}](wiki/{}/{}.md) — 入度 {}",
info.name,
config.wiki.language,
info.name.replace("::", "_"),
info.in_degree
));
if !info.dependents.is_empty() {
body.push_str(&format!(", 被 {} 依赖", info.dependents.join(", ")));
}
body.push('\n');
}
make_document(body, config, &infos)
}
fn collect_module_infos(graph: &KnowledgeGraph, cards: &[KnowledgeCard]) -> Vec<ModuleGuideInfo> {
let mut node_module: HashMap<NodeId, String> = HashMap::new();
for module in &graph.modules {
for nid in &module.node_ids {
node_module
.entry(*nid)
.or_insert_with(|| module.name.clone());
}
}
let mut edges: BTreeSet<(String, String)> = BTreeSet::new();
for edge in graph.graph.edge_references() {
if matches!(
graph.graph[edge.id()].kind,
EdgeKind::Calls | EdgeKind::Imports
) {
let (Some(src), Some(tgt)) = (
node_module.get(&edge.source()),
node_module.get(&edge.target()),
) else {
continue;
};
if src != tgt {
edges.insert((src.clone(), tgt.clone()));
}
}
}
let mut infos: Vec<ModuleGuideInfo> = graph
.modules
.iter()
.filter(|m| !m.node_ids.is_empty())
.map(|m| ModuleGuideInfo {
name: m.name.clone(),
description: cards
.iter()
.find(|c| c.module_name == m.name)
.map(|c| c.summary.clone())
.unwrap_or_default(),
dependents: Vec::new(),
dependencies: Vec::new(),
in_degree: 0,
})
.collect();
let mut index_of: HashMap<String, usize> = HashMap::new();
for (i, info) in infos.iter().enumerate() {
index_of.insert(info.name.clone(), i);
}
for (src, tgt) in &edges {
if let (Some(&si), Some(&ti)) = (index_of.get(src), index_of.get(tgt)) {
infos[ti].in_degree += 1;
infos[ti].dependents.push(src.clone());
infos[si].dependencies.push(tgt.clone());
}
}
for info in &mut infos {
info.dependents.sort();
info.dependencies.sort();
}
infos.sort_by(|a, b| {
b.in_degree
.cmp(&a.in_degree)
.then_with(|| a.name.cmp(&b.name))
});
infos
}
fn index_guide_prompt(infos: &[ModuleGuideInfo], language: &str) -> Vec<Message> {
let system = format!(
r#"你是一个资深软件架构师,负责为代码仓库生成人类可读的阅读指南(index.md)。
请基于模块列表与模块间依赖信息,输出以下结构:
# 阅读指南
## 推荐阅读顺序
按从基础到应用、从被依赖方到依赖方的顺序推荐阅读路径,每条给出理由。
## 主题分组
将模块按主题/分层分组(如基础设施、核心领域、接口层),每组给出组内阅读顺序。
要求:
1. 使用 Markdown 格式输出;
2. 模块链接必须写成 [模块名](wiki/{language}/{{模块名去"::"为"_"}}.md) 形式;
3. 覆盖输入的全部模块,不得遗漏;
4. 用 {language} 语言输出。"#
);
let mut user = String::from("## 模块列表\n");
for info in infos {
let desc = if info.description.is_empty() {
"无"
} else {
info.description.as_str()
};
user.push_str(&format!(
"- {}: {}(入度 {},被 {} 依赖,依赖 {})\n",
info.name,
desc,
info.in_degree,
if info.dependents.is_empty() {
"无".to_string()
} else {
info.dependents.join(", ")
},
if info.dependencies.is_empty() {
"无".to_string()
} else {
info.dependencies.join(", ")
},
));
}
vec![Message::system(system), Message::user(user)]
}
fn make_document(content: String, config: &WikiConfig, infos: &[ModuleGuideInfo]) -> WikiDocument {
WikiDocument {
title: "index".into(),
kind: DocumentKind::TableOfContents,
content,
language: config.wiki.language.clone(),
module_path: vec![],
references: infos
.iter()
.map(|info| Reference {
target_title: info.name.clone(),
target_path: format!(
"wiki/{}/{}.md",
config.wiki.language,
info.name.replace("::", "_")
),
relation: "module".into(),
})
.collect(),
last_updated: Utc::now().to_rfc3339(),
based_on_commit: None,
fingerprint: None,
}
}