use std::collections::{BTreeMap, BTreeSet};
use gobby_core::config::AiRouting;
use serde::{Deserialize, Serialize};
use crate::models::Symbol;
use super::prompts;
#[derive(Debug, Clone)]
pub struct CodewikiInput {
pub files: Vec<String>,
pub graph_edges: Vec<CodewikiGraphEdge>,
pub graph_availability: CodewikiGraphAvailability,
pub symbols: Vec<Symbol>,
pub leading_chunks: BTreeMap<String, LeadingChunk>,
}
#[derive(Debug, Clone)]
pub struct LeadingChunk {
pub content: String,
pub line_start: usize,
pub line_end: usize,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct CodewikiTruthDigest {
pub(crate) schema_version: u8,
pub(crate) generated_at: String,
pub(crate) project_id: String,
pub(crate) repo_summary: String,
pub(crate) stack_authority: String,
pub(crate) stack: Vec<CodewikiTruthStackEntry>,
pub(crate) key_paths: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) superseded: Vec<CodewikiTruthSuperseded>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct CodewikiTruthStackEntry {
pub(crate) service: String,
pub(crate) kind: String,
pub(crate) adapter_module: String,
pub(crate) pulled_in_by: Vec<String>,
pub(crate) summary: String,
pub(crate) degradation: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct CodewikiTruthSuperseded {
pub(crate) old: String,
pub(crate) new: String,
}
pub(crate) fn source_excerpt_for_file(
file: &str,
leading_chunks: &BTreeMap<String, LeadingChunk>,
) -> Option<prompts::SourceExcerpt> {
leading_chunks
.get(file)
.map(|chunk| prompts::SourceExcerpt {
path: file.to_string(),
line_start: chunk.line_start,
line_end: chunk.line_end,
excerpt: chunk.content.clone(),
})
}
pub(crate) fn ranked_source_excerpts<'a>(
candidates: impl Iterator<Item = &'a FileDoc>,
leading_chunks: &BTreeMap<String, LeadingChunk>,
limit: usize,
) -> Vec<prompts::SourceExcerpt> {
let mut ranked = candidates.collect::<Vec<_>>();
ranked.sort_by_key(|file| (std::cmp::Reverse(file.symbols.len()), file.path.clone()));
ranked
.into_iter()
.filter_map(|file| source_excerpt_for_file(&file.path, leading_chunks))
.take(limit)
.collect()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CodewikiGraphEdge {
pub source_component_id: String,
pub target_component_id: String,
pub kind: CodewikiGraphEdgeKind,
}
impl CodewikiGraphEdge {
pub fn call(
source_component_id: impl Into<String>,
target_component_id: impl Into<String>,
) -> Self {
Self {
source_component_id: source_component_id.into(),
target_component_id: target_component_id.into(),
kind: CodewikiGraphEdgeKind::Call,
}
}
pub fn import(
source_component_id: impl Into<String>,
target_component_id: impl Into<String>,
) -> Self {
Self {
source_component_id: source_component_id.into(),
target_component_id: target_component_id.into(),
kind: CodewikiGraphEdgeKind::Import,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodewikiGraphEdgeKind {
Call,
Import,
}
#[derive(Debug, Clone)]
pub(crate) struct CodewikiGraph {
pub(crate) edges: Vec<CodewikiGraphEdge>,
pub(crate) availability: CodewikiGraphAvailability,
}
impl CodewikiGraph {
pub(crate) fn available(edges: Vec<CodewikiGraphEdge>) -> Self {
Self {
edges,
availability: CodewikiGraphAvailability::Available,
}
}
pub(crate) fn truncated(edges: Vec<CodewikiGraphEdge>) -> Self {
Self {
edges,
availability: CodewikiGraphAvailability::Truncated,
}
}
pub(crate) fn unavailable() -> Self {
Self {
edges: Vec::new(),
availability: CodewikiGraphAvailability::Unavailable,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CodewikiGraphAvailability {
Available,
Truncated,
Unavailable,
}
#[derive(Debug, Clone)]
pub(crate) struct FileDoc {
pub(crate) path: String,
pub(crate) module: String,
pub(crate) summary: String,
pub(crate) body: String,
pub(crate) source_spans: Vec<SourceSpan>,
pub(crate) symbols: Vec<SymbolDoc>,
pub(crate) component_ids: Vec<String>,
pub(crate) degraded: bool,
pub(crate) degraded_sources: Vec<String>,
pub(crate) verify_notes: Vec<VerifyNote>,
pub(crate) reused_page: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct SymbolDoc {
pub(crate) symbol: Symbol,
pub(crate) purpose: String,
pub(crate) component_id: String,
pub(crate) component_label: String,
pub(crate) source_span: SourceSpan,
pub(crate) deprecation: Option<String>,
pub(crate) is_test: bool,
}
#[derive(Debug, Clone)]
pub(crate) struct ModuleDoc {
pub(crate) module: String,
pub(crate) summary: String,
pub(crate) source_spans: Vec<SourceSpan>,
pub(crate) direct_files: Vec<FileLink>,
pub(crate) child_modules: Vec<ModuleLink>,
pub(crate) degraded: bool,
pub(crate) degraded_sources: Vec<String>,
pub(crate) verify_notes: Vec<VerifyNote>,
pub(crate) reused_page: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ArchitectureDoc {
pub(crate) source_spans: Vec<SourceSpan>,
pub(crate) subsystems: Vec<ArchitectureSubsystem>,
pub(crate) narrative: Option<String>,
pub(crate) diagrams: Option<String>,
pub(crate) service_matrix: Option<String>,
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct ArchitectureSubsystem {
pub(crate) module: String,
pub(crate) responsibility: String,
pub(crate) child_modules: Vec<String>,
pub(crate) source_spans: Vec<SourceSpan>,
}
#[derive(Debug, Clone)]
pub(crate) struct InfraSection {
pub(crate) service: String,
pub(crate) pulled_in_by: Vec<String>,
pub(crate) adapter_module: String,
pub(crate) summary: String,
pub(crate) degradation: String,
}
#[derive(Debug, Clone)]
pub(crate) struct InfrastructureDoc {
pub(crate) sections: Vec<InfraSection>,
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct FeatureEntry {
pub(crate) command: String,
pub(crate) summary: String,
pub(crate) key_flags: Vec<String>,
pub(crate) entry_symbol: String,
pub(crate) handler_file: String,
}
#[derive(Debug, Clone)]
pub(crate) struct FeatureBinarySection {
pub(crate) binary: String,
pub(crate) entries: Vec<FeatureEntry>,
}
#[derive(Debug, Clone)]
pub(crate) struct FeatureCatalogDoc {
pub(crate) sections: Vec<FeatureBinarySection>,
pub(crate) degraded_sources: Vec<String>,
}
pub(crate) type DeprecationIndex = BTreeMap<String, String>;
pub(crate) type TestIndex = BTreeSet<String>;
#[derive(Debug, Clone)]
pub(crate) struct DeprecatedSymbol {
pub(crate) file: String,
pub(crate) name: String,
pub(crate) kind: String,
pub(crate) line: usize,
pub(crate) reason: String,
}
#[derive(Debug, Clone)]
pub(crate) struct DeprecationsDoc {
pub(crate) symbols: Vec<DeprecatedSymbol>,
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct OnboardingDoc {
pub(crate) source_spans: Vec<SourceSpan>,
pub(crate) entry_points: Vec<OnboardingEntryPoint>,
pub(crate) reading_order: Vec<OnboardingStep>,
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct OnboardingEntryPoint {
pub(crate) link: String,
pub(crate) description: String,
pub(crate) source_span: SourceSpan,
}
#[derive(Debug, Clone)]
pub(crate) struct OnboardingStep {
pub(crate) module: String,
pub(crate) summary: String,
pub(crate) degree: usize,
pub(crate) score: f64,
}
#[derive(Debug, Clone)]
pub(crate) struct HotspotsDoc {
pub(crate) source_spans: Vec<SourceSpan>,
pub(crate) hotspots: Vec<HotspotFinding>,
pub(crate) god_nodes: Vec<HotspotFinding>,
pub(crate) bridges: Vec<HotspotFinding>,
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct HotspotFinding {
pub(crate) node: HotspotNode,
pub(crate) degree: Option<usize>,
pub(crate) score: Option<f64>,
pub(crate) frequency: Option<usize>,
pub(crate) weight: Option<f64>,
}
#[derive(Debug, Clone)]
pub(crate) struct HotspotNode {
pub(crate) id: String,
pub(crate) kind: String,
pub(crate) label: String,
pub(crate) wikilink: String,
pub(crate) file_wikilink: Option<String>,
pub(crate) source_span: Option<SourceSpan>,
}
#[derive(Debug, Clone)]
pub(crate) struct FileLink {
pub(crate) path: String,
pub(crate) summary: String,
pub(crate) source_spans: Vec<SourceSpan>,
}
#[derive(Debug, Clone)]
pub(crate) struct ModuleLink {
pub(crate) module: String,
pub(crate) summary: String,
pub(crate) source_spans: Vec<SourceSpan>,
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) struct SourceSpan {
pub(crate) file: String,
pub(crate) line_start: usize,
pub(crate) line_end: usize,
}
const VERIFY_NOTE_REASON_LIMIT: usize = 200;
const VERIFY_NOTE_TRUNCATION: &str = "...";
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub(crate) struct VerifyNote {
pub(crate) id: usize,
pub(crate) reason: String,
}
impl VerifyNote {
pub(crate) fn new(id: usize, reason: impl AsRef<str>) -> Self {
Self {
id,
reason: normalize_verify_note_reason(reason.as_ref()),
}
}
}
fn normalize_verify_note_reason(reason: &str) -> String {
let reason = reason.trim();
if reason.chars().count() <= VERIFY_NOTE_REASON_LIMIT {
return reason.to_string();
}
let keep = VERIFY_NOTE_REASON_LIMIT.saturating_sub(VERIFY_NOTE_TRUNCATION.len());
let mut truncated = reason.chars().take(keep).collect::<String>();
truncated.push_str(VERIFY_NOTE_TRUNCATION);
truncated
}
#[derive(Debug, Clone, Serialize)]
pub struct CodewikiRunSummary {
pub command: &'static str,
pub project_id: String,
pub project_root: String,
pub out_dir: String,
pub generated_pages: usize,
pub changed_paths: Vec<String>,
pub skipped: usize,
pub files: usize,
pub modules: usize,
pub symbols: usize,
pub ai_enabled: bool,
pub degraded_pages: Vec<String>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub(crate) struct CodewikiMeta {
pub(crate) docs: BTreeMap<String, CodewikiDocMeta>,
pub(crate) generated_docs: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) index_snapshot: Option<CodewikiIndexSnapshot>,
#[serde(default)]
pub(crate) ai_mode: String,
}
#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct CodewikiDocMeta {
pub(crate) source_hashes: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(crate) degraded: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) summary: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub(crate) ai_mode: String,
#[serde(default)]
pub(crate) render_version: u32,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub(crate) neighbor_hashes: BTreeMap<String, String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) invalidation_key: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct BuiltDoc {
pub(crate) path: String,
pub(crate) content: String,
pub(crate) degraded: bool,
pub(crate) summary: Option<String>,
pub(crate) neighbors: BTreeSet<String>,
pub(crate) invalidation_key: Option<String>,
pub(crate) invalidation_key_requires_sources: bool,
}
impl BuiltDoc {
pub(crate) fn healthy(path: impl Into<String>, content: String) -> Self {
Self {
path: path.into(),
content,
degraded: false,
summary: None,
neighbors: BTreeSet::new(),
invalidation_key: None,
invalidation_key_requires_sources: false,
}
}
pub(crate) fn derived(
path: impl Into<String>,
content: String,
invalidation_key: String,
) -> Self {
Self {
path: path.into(),
content,
degraded: false,
summary: None,
neighbors: BTreeSet::new(),
invalidation_key: Some(invalidation_key),
invalidation_key_requires_sources: false,
}
}
pub(crate) fn with_source_sensitive_key(mut self) -> Self {
self.invalidation_key_requires_sources = true;
self
}
pub(crate) fn with_neighbors(mut self, neighbors: BTreeSet<String>) -> Self {
self.neighbors = neighbors;
self
}
}
#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct CodewikiIndexSnapshot {
pub(crate) files: BTreeMap<String, CodewikiFileSnapshot>,
pub(crate) symbols: BTreeMap<String, CodewikiSymbolSnapshot>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) graph_neighborhoods: Option<BTreeMap<String, String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub(crate) degraded_sources: Vec<String>,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct CodewikiFileSnapshot {
pub(crate) content_hash: String,
pub(crate) symbol_count: usize,
}
#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct CodewikiSymbolSnapshot {
pub(crate) file_path: String,
pub(crate) name: String,
pub(crate) qualified_name: String,
pub(crate) kind: String,
pub(crate) line_start: usize,
}
pub type TextGenerator<'a> = dyn FnMut(&str, &str, PromptTier) -> Option<String> + 'a;
pub type TextVerifier<'a> = dyn FnMut(&str, &str) -> Option<String> + 'a;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum PromptTier {
#[default]
Standard,
Module,
Aggregate,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum AiDepth {
Sections,
#[default]
Files,
Symbols,
}
impl AiDepth {
pub(crate) fn includes_files(self) -> bool {
self >= AiDepth::Files
}
pub(crate) fn includes_symbols(self) -> bool {
self >= AiDepth::Symbols
}
pub(crate) fn mode_label(self) -> &'static str {
match self {
AiDepth::Sections => "sections",
AiDepth::Files => "files",
AiDepth::Symbols => "symbols",
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
pub enum ProseDepth {
Brief,
#[default]
Standard,
Deep,
}
impl ProseDepth {
pub(crate) fn max_tokens(self) -> Option<usize> {
match self {
ProseDepth::Brief => Some(640),
ProseDepth::Standard => None,
ProseDepth::Deep => Some(2_400),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ProseRegister {
Newcomer,
Maintainer,
Agent,
}
#[derive(Clone, Debug, Default)]
pub struct CodewikiAiOptions {
pub routing: Option<AiRouting>,
pub depth: AiDepth,
pub prose_depth: ProseDepth,
pub register: Option<ProseRegister>,
pub aggregate_profile: Option<String>,
pub verify_profile: Option<String>,
pub verify_model: Option<String>,
pub verify_api_key: Option<String>,
}
impl SourceSpan {
pub(crate) fn from_symbol(symbol: &Symbol) -> Self {
Self {
file: symbol.file_path.clone(),
line_start: symbol.line_start,
line_end: symbol.line_end,
}
}
pub(crate) fn citation(&self) -> String {
if self.line_start == self.line_end {
format!("[{}:{}]", self.file, self.line_start)
} else {
format!("[{}:{}-{}]", self.file, self.line_start, self.line_end)
}
}
pub(crate) fn contains(&self, file: &str, line_start: usize, line_end: usize) -> bool {
self.file == file && self.line_start <= line_start && line_end <= self.line_end
}
}