use crate::generate::chunk::Chunk;
use crate::generate::llm::Message;
use crate::model::{KnowledgeGraph, ModuleCluster};
fn module_summary_system_prompt(language: &str) -> String {
format!(
r#"你是一个资深软件工程师,负责分析代码并生成模块摘要。
请按以下步骤**在内部**完成分析(不要输出思考过程,只输出最终结果):
1. 通读实体列表与导入语句,识别模块的核心职责与边界;
2. 依据实体间调用/导入关系判断模块对外的依赖;
3. 总结关键设计决策与模式;
4. 输出以下结构。
输出结构:
## 模块概述
简要描述这个模块的职责和功能。
## 核心实体
列出所有重要的结构体、trait、函数,每条一行:
- `实体名`(类型)— 描述
## 依赖关系
列出这个模块引用的外部模块和依赖。
## 设计要点
关键的设计决策和模式。
请用 {} 语言输出。"#,
language
)
}
fn module_summary_user_prompt(chunk: &Chunk) -> String {
let mut parts = Vec::new();
parts.push(format!("模块路径: {}", chunk.module_path.join("::")));
if !chunk.entities.is_empty() {
parts.push("\n## 实体列表".to_string());
for entity in &chunk.entities {
let doc = entity
.doc_comment
.as_deref()
.map(|d| d.lines().next().unwrap_or(""))
.unwrap_or("");
parts.push(format!(
"- {} ({}): {} [行 {}..{}]",
entity.name, entity.kind, doc, entity.line_start, entity.line_end
));
}
}
if !chunk.imports.is_empty() {
parts.push("\n## 导入语句".to_string());
for import in &chunk.imports {
parts.push(format!("- {}", import.source));
}
}
if !chunk.file_paths.is_empty() {
parts.push("\n## 关联文件".to_string());
for path in &chunk.file_paths {
parts.push(format!("- {}", path.display()));
}
}
parts.join("\n")
}
pub fn module_summary_prompt(
chunk: &Chunk,
language: &str,
) -> Vec<Message> {
let system = module_summary_system_prompt(language);
vec![
Message::system(system),
Message::user(module_summary_user_prompt(chunk)),
]
}
fn architecture_overview_system_prompt(language: &str) -> String {
format!(
r#"你是一个资深软件架构师,负责分析整个项目的模块结构并生成架构概览文档。
请基于输入的模块聚类信息和依赖关系,输出以下结构:
# 项目架构概览
## 架构风格
描述项目采用的架构风格(如分层架构、模块化单体、微服务等)。
## 模块划分
列出所有模块及其职责:
- `模块名` — 职责描述
## 模块间依赖关系
描述各个模块之间的依赖关系和通信方式。
## 数据流
数据在模块间的流转方式。
## 架构决策
可以从此架构中推断出的关键架构决策。
**模块真实性约束(必须遵守)**:模块划分小节只列出输入模块聚类信息中给出的
模块名,不得添加、改名或合并输入中不存在的模块。
请用 {} 语言输出。保留 Markdown 格式。"#,
language
)
}
fn architecture_overview_user_prompt(modules: &[ModuleCluster], graph: &KnowledgeGraph) -> String {
let mut parts = Vec::new();
parts.push("## 模块聚类信息".to_string());
for module in modules {
parts.push(format!(
"- {} (内聚度: {:.2}, 耦合度: {:.2}, 节点数: {})",
module.name,
module.cohesion,
module.coupling,
module.node_ids.len()
));
}
parts.push("\n## 图统计".to_string());
parts.push(format!("- 总节点数: {}", graph.graph.node_count()));
parts.push(format!("- 总边数: {}", graph.graph.edge_count()));
parts.join("\n")
}
pub fn module_description_prompt(
module_name: &str,
entity_names: &[String],
language: &str,
) -> Vec<Message> {
let entities = if entity_names.is_empty() {
"(无实体)".to_string()
} else {
entity_names.join(", ")
};
vec![
Message::system(format!(
"你是代码架构分析专家。请用一句中文({} 字以内)概括给定模块的职责。\
只输出职责描述本身,不要前缀、引号或换行。",
if language == "zh" { 30 } else { 60 }
)),
Message::user(format!(
"模块名: {module_name}\n包含实体: {entities}\n\n请输出该模块的一句话职责描述。"
)),
]
}
pub fn architecture_overview_prompt(
modules: &[ModuleCluster],
graph: &KnowledgeGraph,
language: &str,
) -> Vec<Message> {
let system = architecture_overview_system_prompt(language);
vec![
Message::system(system),
Message::user(architecture_overview_user_prompt(modules, graph)),
]
}
fn knowledge_card_system_prompt(language: &str) -> String {
format!(
r#"你是一个代码分析专家,负责生成结构化的 Knowledge Card。
Knowledge Card 是给 AI Agent 阅读的模块级结构化摘要。
请按以下步骤**在内部**完成分析(不要输出思考过程,只输出最终 JSON):
1. 归纳模块职责与边界,形成一句话总结;
2. 识别关键实体及其对外契约(可见性、职责);
3. 推断设计模式、技术栈与编码规范;
4. 若输入中含"人工修改待同步"记录,将其内容纳入描述(不要删除记录本身);
5. 严格按 JSON 格式输出最终结果。
**实体真实性约束(必须遵守)**:key_entities 只允许列出输入实体信息中真实存在的
实体(名称与输入一致),不得编造不存在的实体;找不到时列表可以为空。
请严格按以下 JSON 格式输出,不包含其他内容:
```json
{{
"summary": "模块功能的一句话总结",
"key_entities": [
{{"name": "实体名", "kind": "结构体/函数/Trait", "visibility": "public/private/crate", "doc": "文档描述"}}
],
"design_patterns": ["用到的设计模式"],
"todo_notes": ["待办事项或注意点"],
"coding_spec": "该模块遵循的编码规范(无则省略该字段)",
"tech_stack": ["该模块用到的技术栈,如 tokio/serde/petgraph"],
"architecture": "该模块的内部架构或关键设计说明(无则省略该字段)"
}}
```
请用 {} 语言输出描述性字段。不要添加 Markdown 代码块标记之外的文字。"#,
language
)
}
pub fn knowledge_card_prompt(
chunk: &Chunk,
language: &str,
pending_manual_edits: &[String],
) -> Vec<Message> {
let system = knowledge_card_system_prompt(language);
let mut user = module_summary_user_prompt(chunk);
if !pending_manual_edits.is_empty() {
user.push_str("\n\n## 人工修改待同步\n\n");
user.push_str(
"以下页面被人工修改,与代码最新状态可能不一致。\
请结合这些修改生成卡片描述(如更新摘要、实体说明),但不要删除下述记录本身:\n",
);
for note in pending_manual_edits {
user.push_str(&format!("- {note}\n"));
}
}
vec![Message::system(system), Message::user(user)]
}
pub fn edit_card_prompt(
mode: &str,
module: &str,
existing: &str,
instruction: &str,
references: &str,
language: &str,
) -> Vec<Message> {
let system = format!(
r#"你是一个代码分析专家,负责编辑 Knowledge Card。
Knowledge Card 是给 AI Agent 阅读的模块级结构化摘要,使用固定 Markdown 格式:
# 模块名
## 摘要
模块功能总结
## 核心实体
- `实体名`(类型)— 描述
## 相关文件
- 文件路径
## 设计模式
- 模式
缺失的字段省略对应小节。请直接输出编辑后的完整卡片 Markdown,不要代码块包裹,不要添加无关内容。请用 {} 语言输出描述。"#,
language
);
let mut user = if mode == "rewrite" {
format!("模块: {module}\n\n指令: {instruction}\n\n忽略任何旧版本内容,全量重写该模块的卡片。")
} else {
let hint = if mode == "supplement" {
"保留现有卡片内容不变,按指令在末尾追加新内容"
} else {
"按指令修改现有卡片内容,其余部分保持不变"
};
format!("模块: {module}\n\n指令: {instruction}({hint})\n\n## 现有卡片内容\n{existing}")
};
if !references.is_empty() {
user.push_str(&format!("\n\n## 参考材料\n{references}"));
}
vec![Message::system(system), Message::user(user)]
}
fn wiki_page_system_prompt(language: &str) -> String {
format!(
r#"你是一个技术文档写手,负责生成项目 Wiki 页面。
Wiki 页面是给人类开发者阅读的叙述性文档。
请基于模块信息和卡片摘要,生成以下格式的 Wiki 页面:
# 模块名称
## 概述
用 2-3 句话描述模块的职责和功能。
## 核心实体
- `StructName` — 描述
- `fn_name()` — 描述
- `TraitName` — 描述
## 依赖关系
- `模块A` — 依赖说明
## 使用方式
简要说明如何使用这个模块。
## 源码引用契约(必须遵守)
- 提及任何具体函数、结构体、文件时,必须携带真实存在的源码引用:
`相对路径:行号`(如 `src/fs.rs:28`)或 `相对路径:起始行-结束行`(如 `src/fs.rs:28-45`),
写在提及处所在行内。
- 引用必须真实存在:只引用输入实体列表/关联文件中给出的文件与行号,
不得编造不存在的文件或行号。
- 每个小节至少包含一条引用。
请用 {} 语言输出。保持简洁、清晰。"#,
language
)
}
fn entity_signature_line(e: &crate::ingest::parser::Entity) -> String {
let Some(raw) = &e.signature else {
return String::new();
};
let trimmed = raw.trim();
if trimmed.is_empty() {
return String::new();
}
let line_count = trimmed.lines().count();
let mut flat = trimmed.lines().collect::<Vec<_>>().join(" ");
if line_count > 8 || flat.chars().count() > 160 {
flat = flat.chars().take(160).collect();
flat.push('…');
}
format!(",签名: {flat}")
}
fn wiki_page_user_prompt(chunk: &Chunk, module_summary: &str, notes: &[String]) -> String {
let mut entity_lines: Vec<String> = chunk
.entities
.iter()
.enumerate()
.map(|(i, e)| {
let sig = entity_signature_line(e);
match chunk.entity_sources.get(i) {
Some(path) => format!("- `{}` ({}) — {}:{}{}", e.name, e.kind, path.display(), e.line_start, sig),
None => format!(
"- `{}` ({}) — 第 {}-{} 行(所属文件未记录){}",
e.name, e.kind, e.line_start, e.line_end, sig
),
}
})
.take(80)
.collect();
if chunk.entities.len() > 80 {
entity_lines.push(format!("- …共 {} 个实体,仅列出前 80 个", chunk.entities.len()));
}
let guide_section = if notes.is_empty() {
String::new()
} else {
format!(
"\n\n## 项目引导说明\n{}\n",
notes
.iter()
.map(|n| format!("- {}", n))
.collect::<Vec<_>>()
.join("\n")
)
};
format!(
"模块路径: {}\n\n## 代码信息\n实体数: {}, 文件数: {}\n\n## 实体引用清单\n{}\n\n## 卡片摘要\n{}{}",
chunk.module_path.join("::"),
chunk.entity_count(),
chunk.file_paths.len(),
entity_lines.join("\n"),
module_summary,
guide_section
)
}
pub fn wiki_page_prompt(
chunk: &Chunk,
module_summary: &str,
language: &str,
notes: &[String],
) -> Vec<Message> {
let system = wiki_page_system_prompt(language);
let user = wiki_page_user_prompt(chunk, module_summary, notes);
vec![Message::system(system), Message::user(user)]
}
pub fn schema_doc_system_prompt(language: &str) -> String {
format!(
r#"你是一个数据库专家,负责分析 SQL 迁移文件并生成 Schema 文档。
请基于输入的建表语句,输出以下格式的 Markdown:
# 数据库 Schema 文档
## 表结构
对每张表用表格列出字段:列名 | 类型 | 约束 | 说明
## 关系说明
描述表之间的外键关系和约束。
## ER 图
用 Mermaid erDiagram 代码块画出实体关系图。
请用 {} 语言输出。保留 Markdown 与 Mermaid 代码块格式。"#,
language
)
}
pub fn schema_doc_prompt(
path: &std::path::Path,
blocks: &[&str],
language: &str,
) -> Vec<Message> {
let system = schema_doc_system_prompt(language);
let mut user = format!("SQL 文件路径: {}\n\n## 建表语句块\n", path.display());
for (i, block) in blocks.iter().enumerate() {
user.push_str(&format!("### 语句块 {}\n```sql\n{}\n```\n\n", i + 1, block));
}
vec![Message::system(system), Message::user(user)]
}
#[cfg(test)]
mod tests {
use super::*;
fn make_test_chunk(module_path: &[&str]) -> Chunk {
Chunk {
module_path: module_path.iter().map(|s| s.to_string()).collect(),
entities: vec![],
imports: vec![],
dependencies: vec![],
file_paths: vec![],
entity_sources: vec![],
}
}
#[test]
fn test_knowledge_card_prompt_injects_pending_manual_edits() {
let chunk = make_test_chunk(&["src", "config"]);
let pending = vec!["人工修改待同步: wiki/zh/src_config.md 内容摘要: 用户改的".into()];
let messages = knowledge_card_prompt(&chunk, "zh", &pending);
let user = &messages[1].content;
assert!(user.contains("## 人工修改待同步"));
assert!(user.contains("wiki/zh/src_config.md"));
let messages = knowledge_card_prompt(&chunk, "zh", &[]);
assert!(!messages[1].content.contains("人工修改待同步"));
}
#[test]
fn test_schema_doc_prompt_contains_path_and_blocks() { let blocks = vec!["CREATE TABLE users (\n id INTEGER\n);"];
let messages = schema_doc_prompt(
std::path::Path::new("db/migrations/001_init.sql"),
&blocks,
"zh",
);
let user = &messages[1].content;
assert!(user.contains("db/migrations/001_init.sql"));
assert!(user.contains("CREATE TABLE users"));
assert!(user.contains("```sql"));
assert!(messages[0].content.contains("erDiagram"));
}
#[test]
fn test_anti_fabrication_constraints_in_card_and_architecture_prompts() {
let chunk = make_test_chunk(&["src", "config"]);
let card_messages = knowledge_card_prompt(&chunk, "zh", &[]);
assert!(
card_messages[0].content.contains("不得编造"),
"卡片 prompt 必须含实体真实性约束: {}",
card_messages[0].content
);
let arch = architecture_overview_prompt(&[], &KnowledgeGraph::default(), "zh");
assert!(
arch[0].content.contains("不得添加"),
"架构 prompt 必须含模块真实性约束: {}",
arch[0].content
);
}
#[test]
fn test_wiki_page_user_prompt_contains_entity_reference_list() {
let mut chunk = make_test_chunk(&["src", "alpha"]);
chunk.entities = vec![crate::ingest::parser::Entity {
name: "alpha_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 3,
doc_comment: None,
signature: None,
visibility: None,
}];
chunk.entity_sources = vec![std::path::PathBuf::from("src/alpha.rs")];
let user = wiki_page_user_prompt(&chunk, "卡片摘要", &[]);
assert!(
user.contains("src/alpha.rs:1"),
"引用清单必须含文件:行号: {}",
user
);
assert!(user.contains("alpha_fn"));
chunk.entity_sources = vec![];
let user = wiki_page_user_prompt(&chunk, "卡片摘要", &[]);
assert!(user.contains("所属文件未记录"));
}
#[test]
fn test_wiki_page_user_prompt_injects_guide_notes() {
let chunk = Chunk {
module_path: vec!["src".into(), "alpha".into()],
entities: vec![crate::ingest::parser::Entity {
name: "alpha_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 3,
doc_comment: None,
signature: None,
visibility: None,
}],
imports: vec![],
dependencies: vec![],
entity_sources: vec![std::path::PathBuf::from("src/alpha.rs")],
file_paths: vec![std::path::PathBuf::from("src/alpha.rs")],
};
let user = wiki_page_user_prompt(&chunk, "卡片摘要", &[]);
assert!(!user.contains("项目引导说明"), "空 notes 不应生成引导节");
let notes = vec!["命名规范:公开函数必须写文档注释".to_string(), "必写小节:用法示例".to_string()];
let user = wiki_page_user_prompt(&chunk, "卡片摘要", ¬es);
assert!(user.contains("## 项目引导说明"), "notes 非空应生成引导节: {}", user);
assert!(user.contains("命名规范:公开函数必须写文档注释"), "应包含第一条 note");
assert!(user.contains("必写小节:用法示例"), "应包含第二条 note");
}
#[test]
fn test_wiki_page_user_prompt_injects_entity_signature() {
let e = crate::ingest::parser::Entity {
name: "short_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 2,
doc_comment: None,
signature: Some("pub fn short_fn(x: u32) -> u32".into()),
visibility: None,
};
assert_eq!(
entity_signature_line(&e),
",签名: pub fn short_fn(x: u32) -> u32"
);
let long = (0..10).map(|i| format!("line {i}")).collect::<Vec<_>>().join("\n");
let e2 = crate::ingest::parser::Entity {
name: "long_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 12,
doc_comment: None,
signature: Some(long),
visibility: None,
};
let out = entity_signature_line(&e2);
assert!(out.starts_with(",签名: "));
assert!(out.ends_with('…'), "超限签名必须截断加 …: {out}");
assert!(out.chars().count() <= 167, "截断后不超过 160+签名前缀: {out}");
let wide = "w".repeat(200);
let e_wide = crate::ingest::parser::Entity {
name: "wide_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 2,
doc_comment: None,
signature: Some(wide),
visibility: None,
};
let out_wide = entity_signature_line(&e_wide);
assert!(out_wide.ends_with('…'), "160 字符截断分支: {out_wide}");
assert_eq!(out_wide.chars().count(), 166, "160 截断+前缀5+…1");
let e8 = crate::ingest::parser::Entity {
name: "eight_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 9,
doc_comment: None,
signature: Some((0..8).map(|i| format!("l{i}")).collect::<Vec<_>>().join("\n")),
visibility: None,
};
let out8 = entity_signature_line(&e8);
assert!(!out8.ends_with('…'), "恰好 8 行不截断: {out8}");
let e9 = crate::ingest::parser::Entity {
name: "nine_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 10,
doc_comment: None,
signature: Some((0..9).map(|i| format!("l{i}")).collect::<Vec<_>>().join("\n")),
visibility: None,
};
let out9 = entity_signature_line(&e9);
assert!(out9.ends_with('…'), "恰好 9 行截断: {out9}");
let e160 = crate::ingest::parser::Entity {
name: "exact160_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 2,
doc_comment: None,
signature: Some("w".repeat(160)),
visibility: None,
};
let out160 = entity_signature_line(&e160);
assert!(!out160.ends_with('…'), "恰好 160 字符不截断: {out160}");
let crlf = "pub fn a(\r\n x: u32,\r\n) -> u32".to_string();
let e_crlf = crate::ingest::parser::Entity {
name: "crlf_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 4,
doc_comment: None,
signature: Some(crlf),
visibility: None,
};
let out_crlf = entity_signature_line(&e_crlf);
assert!(!out_crlf.contains('\r'), "CRLF 残留 \r: {out_crlf}");
assert!(
out_crlf.contains("pub fn a( x: u32, ) -> u32"),
"CRLF 压平: {out_crlf}"
);
let e3 = crate::ingest::parser::Entity {
name: "no_sig".into(),
kind: "fn".into(),
line_start: 1,
line_end: 2,
doc_comment: None,
signature: None,
visibility: None,
};
assert_eq!(entity_signature_line(&e3), "");
let e4 = crate::ingest::parser::Entity {
name: "blank_sig".into(),
kind: "fn".into(),
line_start: 1,
line_end: 2,
doc_comment: None,
signature: Some(" \n ".into()),
visibility: None,
};
assert_eq!(entity_signature_line(&e4), "");
let mut chunk = make_test_chunk(&["src", "alpha"]);
chunk.entities = vec![crate::ingest::parser::Entity {
name: "alpha_fn".into(),
kind: "fn".into(),
line_start: 1,
line_end: 3,
doc_comment: None,
signature: Some("pub fn alpha_fn()".into()),
visibility: None,
}];
chunk.entity_sources = vec![std::path::PathBuf::from("src/alpha.rs")];
let user = wiki_page_user_prompt(&chunk, "卡片摘要", &[]);
assert!(
user.contains("src/alpha.rs:1,签名: pub fn alpha_fn()"),
"引用清单行必须含签名: {}",
user
);
}
}