use std::collections::BTreeSet;
use super::super::{
AiDepth, CodewikiProgress, DeprecationIndex, FileDoc, Generation, LeadingChunk, PromptTier,
RelationshipFacts, ReusePlan, SourceSpan, SymbolDoc, TestIndex, TextGenerator, TextVerifier,
VerifyNote, VerifyOutcome, citation_list, component_label, file_doc_path, ground_text,
maybe_generate, prompts, structural_file_summary, structural_symbol_purpose, verify_with_notes,
write_section,
};
use crate::models::Symbol;
#[derive(Clone, Copy, Debug)]
pub(crate) struct FileDocPosition {
pub(crate) index: usize,
pub(crate) total: usize,
}
#[expect(clippy::too_many_arguments)]
pub(crate) fn build_file_doc(
file: &str,
module: String,
symbols: Vec<Symbol>,
leading_chunk: Option<&LeadingChunk>,
relationships: &RelationshipFacts,
deprecations: Option<&DeprecationIndex>,
tests: Option<&TestIndex>,
generate: &mut Option<&mut TextGenerator<'_>>,
verify: &mut Option<&mut TextVerifier<'_>>,
reuse: &mut Option<&mut ReusePlan>,
ai_depth: AiDepth,
progress: &mut CodewikiProgress,
position: FileDocPosition,
) -> FileDoc {
let sources = BTreeSet::from([file.to_string()]);
let neighbors = relationships.neighbor_files(file);
let reused = reuse.as_deref_mut().and_then(|plan| {
plan.reusable_page_with_summary_and_neighbors(&file_doc_path(file), &sources, &neighbors)
});
let file_verb = if reused.is_some() {
"reusing"
} else if ai_depth.includes_files() {
"generating"
} else {
"building"
};
progress.emit(format!(
"{file_verb} file doc file {}/{} {}",
position.index, position.total, file
));
let symbol_total = symbols.len();
let mut model_degraded = false;
let mut degraded_sources = BTreeSet::new();
let mut verify_notes = Vec::new();
let symbol_docs = symbols
.into_iter()
.enumerate()
.map(|(index, symbol)| {
let fallback = structural_symbol_purpose(&symbol);
let generated = if reused.is_none() && ai_depth.includes_symbols() {
progress.emit(format!(
"generating symbol doc file {}/{} symbol {}/{} {}",
position.index,
position.total,
index + 1,
symbol_total,
symbol.qualified_name
));
maybe_generate(
generate,
&prompts::symbol_prompt(&symbol),
prompts::SYMBOL_SYSTEM,
PromptTier::Standard,
)
} else {
Generation::Skipped
}
.unwrap_or_record(fallback, &mut model_degraded);
let component_id = symbol.id.clone();
let component_label = component_label(&symbol);
let source_span = SourceSpan::from_symbol(&symbol);
let purpose = ground_text(
&generated,
std::slice::from_ref(&source_span),
Some(&source_span.citation()),
);
let deprecation = deprecations
.and_then(|index| index.get(&component_id))
.cloned();
let is_test = tests.is_some_and(|index| index.contains(&component_id));
SymbolDoc {
symbol,
purpose,
component_id,
component_label,
source_span,
deprecation,
is_test,
}
})
.collect::<Vec<_>>();
if model_degraded {
degraded_sources.insert("model-unavailable".to_string());
}
let source_excerpt = leading_chunk.map(|chunk| prompts::SourceExcerpt {
path: file.to_string(),
line_start: chunk.line_start.max(1),
line_end: chunk.line_end.max(chunk.line_start.max(1)),
excerpt: chunk.content.clone(),
});
let mut source_spans = symbol_docs
.iter()
.map(|symbol| symbol.source_span.clone())
.collect::<Vec<_>>();
if source_spans.is_empty()
&& let Some(excerpt) = &source_excerpt
{
source_spans.push(SourceSpan {
file: file.to_string(),
line_start: excerpt.line_start,
line_end: excerpt.line_end,
});
}
let prompt_symbols = symbol_docs
.iter()
.map(|symbol| prompts::SymbolSummary {
name: symbol.symbol.qualified_name.clone(),
kind: symbol.symbol.kind.clone(),
component_id: symbol.component_id.clone(),
component_label: symbol.component_label.clone(),
line_start: symbol.symbol.line_start,
line_end: symbol.symbol.line_end,
purpose: symbol.purpose.clone(),
})
.collect::<Vec<_>>();
let component_ids = symbol_docs
.iter()
.map(|symbol| symbol.component_id.clone())
.collect::<Vec<_>>();
let (summary, body, reused_page) = match reused {
Some((page, summary)) => (summary, String::new(), Some(page)),
None => {
let body = build_file_body(
file,
&prompt_symbols,
source_excerpt.as_slice(),
&source_spans,
relationships,
&symbol_docs,
generate,
verify,
ai_depth,
&mut degraded_sources,
&mut verify_notes,
);
let summary = file_summary_from_body(&body, file, &symbol_docs);
(summary, body, None)
}
};
FileDoc {
path: file.to_string(),
module,
summary,
body,
source_spans,
symbols: symbol_docs,
component_ids,
degraded: !degraded_sources.is_empty(),
degraded_sources: degraded_sources.into_iter().collect(),
verify_notes,
reused_page,
}
}
#[expect(clippy::too_many_arguments)]
fn build_file_body(
file: &str,
prompt_symbols: &[prompts::SymbolSummary],
sources: &[prompts::SourceExcerpt],
source_spans: &[SourceSpan],
relationships: &RelationshipFacts,
symbol_docs: &[SymbolDoc],
generate: &mut Option<&mut TextGenerator<'_>>,
verify: &mut Option<&mut TextVerifier<'_>>,
ai_depth: AiDepth,
degraded_sources: &mut BTreeSet<String>,
verify_notes: &mut Vec<VerifyNote>,
) -> String {
let generated = if ai_depth.includes_files() {
let (prompt, system) = match (prompt_symbols.is_empty(), sources.first()) {
(true, Some(excerpt)) => (
prompts::content_file_prompt(file, excerpt),
prompts::CONTENT_FILE_SYSTEM,
),
_ => (
prompts::file_prompt(file, prompt_symbols, sources, relationships),
prompts::FILE_SYSTEM,
),
};
maybe_generate(generate, &prompt, system, PromptTier::Module)
} else {
Generation::Skipped
};
let text = match generated {
Generation::Generated(text) => text,
Generation::Failed => {
degraded_sources.insert("model-unavailable".to_string());
return structural_file_body(file, symbol_docs);
}
Generation::Skipped => return structural_file_body(file, symbol_docs),
};
let text = match verify_with_notes(verify, &text, prompt_symbols, sources, relationships) {
VerifyOutcome::Skipped => text,
VerifyOutcome::Verified { text, notes } => {
verify_notes.extend(notes);
text
}
};
let mut grounding_spans = source_spans.to_vec();
grounding_spans.extend(relationships.endpoint_spans());
let citations = citation_list(&grounding_spans, &text);
let grounded = ground_text(&text, &grounding_spans, Some(&citations));
if grounded.trim().is_empty() {
degraded_sources.insert("grounding-empty".to_string());
return structural_file_body(file, symbol_docs);
}
grounded
}
fn structural_file_body(file: &str, symbols: &[SymbolDoc]) -> String {
let mut body = String::new();
write_section(
&mut body,
"Overview",
&structural_file_summary(file, symbols),
);
write_section(
&mut body,
"How it fits",
&format!(
"`{file}` is documented from its indexed symbols; see the Reference table below \
and the module page for how it connects to sibling files."
),
);
body
}
fn file_summary_from_body(body: &str, file: &str, symbols: &[SymbolDoc]) -> String {
body.split("\n\n")
.map(str::trim)
.find(|block| !block.is_empty() && !block.starts_with('#'))
.map(str::to_string)
.unwrap_or_else(|| structural_file_summary(file, symbols))
}