1use std::collections::{BTreeMap, BTreeSet};
2
3use gobby_core::config::AiRouting;
4use serde::{Deserialize, Serialize};
5
6use crate::models::Symbol;
7
8use super::prompts;
9
10#[derive(Debug, Clone)]
11pub struct CodewikiInput {
12 pub files: Vec<String>,
13 pub graph_edges: Vec<CodewikiGraphEdge>,
14 pub graph_availability: CodewikiGraphAvailability,
15 pub symbols: Vec<Symbol>,
16 pub leading_chunks: BTreeMap<String, LeadingChunk>,
21}
22
23#[derive(Debug, Clone)]
26pub struct LeadingChunk {
27 pub content: String,
28 pub line_start: usize,
29 pub line_end: usize,
30}
31
32#[derive(Debug, Clone, Deserialize, Serialize)]
33pub(crate) struct CodewikiTruthDigest {
34 pub(crate) schema_version: u8,
35 pub(crate) generated_at: String,
36 pub(crate) project_id: String,
37 pub(crate) repo_summary: String,
38 pub(crate) stack_authority: String,
39 pub(crate) stack: Vec<CodewikiTruthStackEntry>,
40 pub(crate) key_paths: BTreeMap<String, String>,
41 #[serde(default, skip_serializing_if = "Vec::is_empty")]
42 pub(crate) superseded: Vec<CodewikiTruthSuperseded>,
43}
44
45#[derive(Debug, Clone, Deserialize, Serialize)]
46pub(crate) struct CodewikiTruthStackEntry {
47 pub(crate) service: String,
48 pub(crate) kind: String,
49 pub(crate) adapter_module: String,
50 pub(crate) pulled_in_by: Vec<String>,
51 pub(crate) summary: String,
52 pub(crate) degradation: String,
53}
54
55#[derive(Debug, Clone, Deserialize, Serialize)]
56pub(crate) struct CodewikiTruthSuperseded {
57 pub(crate) old: String,
58 pub(crate) new: String,
59}
60
61pub(crate) fn source_excerpt_for_file(
63 file: &str,
64 leading_chunks: &BTreeMap<String, LeadingChunk>,
65) -> Option<prompts::SourceExcerpt> {
66 leading_chunks
67 .get(file)
68 .map(|chunk| prompts::SourceExcerpt {
69 path: file.to_string(),
70 line_start: chunk.line_start,
71 line_end: chunk.line_end,
72 excerpt: chunk.content.clone(),
73 })
74}
75
76pub(crate) fn ranked_source_excerpts<'a>(
80 candidates: impl Iterator<Item = &'a FileDoc>,
81 leading_chunks: &BTreeMap<String, LeadingChunk>,
82 limit: usize,
83) -> Vec<prompts::SourceExcerpt> {
84 let mut ranked = candidates.collect::<Vec<_>>();
85 ranked.sort_by_key(|file| (std::cmp::Reverse(file.symbols.len()), file.path.clone()));
86 ranked
87 .into_iter()
88 .filter_map(|file| source_excerpt_for_file(&file.path, leading_chunks))
89 .take(limit)
90 .collect()
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct CodewikiGraphEdge {
95 pub source_component_id: String,
96 pub target_component_id: String,
97 pub kind: CodewikiGraphEdgeKind,
98}
99
100impl CodewikiGraphEdge {
101 pub fn call(
102 source_component_id: impl Into<String>,
103 target_component_id: impl Into<String>,
104 ) -> Self {
105 Self {
106 source_component_id: source_component_id.into(),
107 target_component_id: target_component_id.into(),
108 kind: CodewikiGraphEdgeKind::Call,
109 }
110 }
111
112 pub fn import(
113 source_component_id: impl Into<String>,
114 target_component_id: impl Into<String>,
115 ) -> Self {
116 Self {
117 source_component_id: source_component_id.into(),
118 target_component_id: target_component_id.into(),
119 kind: CodewikiGraphEdgeKind::Import,
120 }
121 }
122}
123
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub enum CodewikiGraphEdgeKind {
126 Call,
127 Import,
128}
129
130#[derive(Debug, Clone)]
131pub(crate) struct CodewikiGraph {
132 pub(crate) edges: Vec<CodewikiGraphEdge>,
133 pub(crate) availability: CodewikiGraphAvailability,
134}
135
136impl CodewikiGraph {
137 pub(crate) fn available(edges: Vec<CodewikiGraphEdge>) -> Self {
138 Self {
139 edges,
140 availability: CodewikiGraphAvailability::Available,
141 }
142 }
143
144 pub(crate) fn truncated(edges: Vec<CodewikiGraphEdge>) -> Self {
145 Self {
146 edges,
147 availability: CodewikiGraphAvailability::Truncated,
148 }
149 }
150
151 pub(crate) fn unavailable() -> Self {
152 Self {
153 edges: Vec::new(),
154 availability: CodewikiGraphAvailability::Unavailable,
155 }
156 }
157}
158
159#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub enum CodewikiGraphAvailability {
161 Available,
162 Truncated,
163 Unavailable,
164}
165
166#[derive(Debug, Clone)]
167pub(crate) struct FileDoc {
168 pub(crate) path: String,
169 pub(crate) module: String,
170 pub(crate) summary: String,
172 pub(crate) body: String,
177 pub(crate) source_spans: Vec<SourceSpan>,
178 pub(crate) symbols: Vec<SymbolDoc>,
179 pub(crate) component_ids: Vec<String>,
180 pub(crate) degraded: bool,
182 pub(crate) degraded_sources: Vec<String>,
183 pub(crate) verify_notes: Vec<VerifyNote>,
184 pub(crate) reused_page: Option<String>,
187}
188
189#[derive(Debug, Clone)]
190pub(crate) struct SymbolDoc {
191 pub(crate) symbol: Symbol,
192 pub(crate) purpose: String,
193 pub(crate) component_id: String,
194 pub(crate) component_label: String,
195 pub(crate) source_span: SourceSpan,
196 pub(crate) deprecation: Option<String>,
203 pub(crate) is_test: bool,
211}
212
213#[derive(Debug, Clone)]
214pub(crate) struct ModuleDoc {
215 pub(crate) module: String,
216 pub(crate) summary: String,
217 pub(crate) source_spans: Vec<SourceSpan>,
218 pub(crate) direct_files: Vec<FileLink>,
219 pub(crate) child_modules: Vec<ModuleLink>,
220 pub(crate) degraded: bool,
222 pub(crate) degraded_sources: Vec<String>,
223 pub(crate) verify_notes: Vec<VerifyNote>,
224 pub(crate) reused_page: Option<String>,
227}
228
229#[derive(Debug, Clone)]
230pub(crate) struct ArchitectureDoc {
231 pub(crate) source_spans: Vec<SourceSpan>,
232 pub(crate) subsystems: Vec<ArchitectureSubsystem>,
233 pub(crate) narrative: Option<String>,
234 pub(crate) diagrams: Option<String>,
239 pub(crate) service_matrix: Option<String>,
246 pub(crate) degraded_sources: Vec<String>,
247}
248
249#[derive(Debug, Clone)]
250pub(crate) struct ArchitectureSubsystem {
251 pub(crate) module: String,
252 pub(crate) responsibility: String,
253 pub(crate) child_modules: Vec<String>,
254 pub(crate) source_spans: Vec<SourceSpan>,
255}
256
257#[derive(Debug, Clone)]
263pub(crate) struct InfraSection {
264 pub(crate) service: String,
265 pub(crate) pulled_in_by: Vec<String>,
266 pub(crate) adapter_module: String,
267 pub(crate) summary: String,
268 pub(crate) degradation: String,
269}
270
271#[derive(Debug, Clone)]
276pub(crate) struct InfrastructureDoc {
277 pub(crate) sections: Vec<InfraSection>,
278 pub(crate) degraded_sources: Vec<String>,
279}
280
281#[derive(Debug, Clone)]
287pub(crate) struct FeatureEntry {
288 pub(crate) command: String,
289 pub(crate) summary: String,
290 pub(crate) key_flags: Vec<String>,
291 pub(crate) entry_symbol: String,
292 pub(crate) handler_file: String,
293}
294
295#[derive(Debug, Clone)]
298pub(crate) struct FeatureBinarySection {
299 pub(crate) binary: String,
300 pub(crate) entries: Vec<FeatureEntry>,
301}
302
303#[derive(Debug, Clone)]
308pub(crate) struct FeatureCatalogDoc {
309 pub(crate) sections: Vec<FeatureBinarySection>,
310 pub(crate) degraded_sources: Vec<String>,
311}
312
313pub(crate) type DeprecationIndex = BTreeMap<String, String>;
319
320pub(crate) type TestIndex = BTreeSet<String>;
325
326#[derive(Debug, Clone)]
330pub(crate) struct DeprecatedSymbol {
331 pub(crate) file: String,
332 pub(crate) name: String,
333 pub(crate) kind: String,
334 pub(crate) line: usize,
335 pub(crate) reason: String,
336}
337
338#[derive(Debug, Clone)]
343pub(crate) struct DeprecationsDoc {
344 pub(crate) symbols: Vec<DeprecatedSymbol>,
345 pub(crate) degraded_sources: Vec<String>,
346}
347
348#[derive(Debug, Clone)]
349pub(crate) struct OnboardingDoc {
350 pub(crate) source_spans: Vec<SourceSpan>,
351 pub(crate) entry_points: Vec<OnboardingEntryPoint>,
352 pub(crate) reading_order: Vec<OnboardingStep>,
353 pub(crate) degraded_sources: Vec<String>,
354}
355
356#[derive(Debug, Clone)]
357pub(crate) struct OnboardingEntryPoint {
358 pub(crate) link: String,
359 pub(crate) description: String,
360 pub(crate) source_span: SourceSpan,
361}
362
363#[derive(Debug, Clone)]
364pub(crate) struct OnboardingStep {
365 pub(crate) module: String,
366 pub(crate) summary: String,
367 pub(crate) degree: usize,
368 pub(crate) score: f64,
369}
370
371#[derive(Debug, Clone)]
372pub(crate) struct HotspotsDoc {
373 pub(crate) source_spans: Vec<SourceSpan>,
374 pub(crate) hotspots: Vec<HotspotFinding>,
375 pub(crate) god_nodes: Vec<HotspotFinding>,
376 pub(crate) bridges: Vec<HotspotFinding>,
377 pub(crate) degraded_sources: Vec<String>,
378}
379
380#[derive(Debug, Clone)]
381pub(crate) struct HotspotFinding {
382 pub(crate) node: HotspotNode,
383 pub(crate) degree: Option<usize>,
384 pub(crate) score: Option<f64>,
385 pub(crate) frequency: Option<usize>,
386 pub(crate) weight: Option<f64>,
387}
388
389#[derive(Debug, Clone)]
390pub(crate) struct HotspotNode {
391 pub(crate) id: String,
392 pub(crate) kind: String,
393 pub(crate) label: String,
394 pub(crate) wikilink: String,
395 pub(crate) file_wikilink: Option<String>,
396 pub(crate) source_span: Option<SourceSpan>,
397}
398
399#[derive(Debug, Clone)]
400pub(crate) struct FileLink {
401 pub(crate) path: String,
402 pub(crate) summary: String,
403 pub(crate) source_spans: Vec<SourceSpan>,
404}
405
406#[derive(Debug, Clone)]
407pub(crate) struct ModuleLink {
408 pub(crate) module: String,
409 pub(crate) summary: String,
410 pub(crate) source_spans: Vec<SourceSpan>,
411}
412
413#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)]
414pub(crate) struct SourceSpan {
415 pub(crate) file: String,
416 pub(crate) line_start: usize,
417 pub(crate) line_end: usize,
418}
419
420const VERIFY_NOTE_REASON_LIMIT: usize = 200;
421const VERIFY_NOTE_TRUNCATION: &str = "...";
422
423#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
424pub(crate) struct VerifyNote {
425 pub(crate) id: usize,
426 pub(crate) reason: String,
427}
428
429impl VerifyNote {
430 pub(crate) fn new(id: usize, reason: impl AsRef<str>) -> Self {
431 Self {
432 id,
433 reason: normalize_verify_note_reason(reason.as_ref()),
434 }
435 }
436}
437
438fn normalize_verify_note_reason(reason: &str) -> String {
439 let reason = reason.trim();
440 if reason.chars().count() <= VERIFY_NOTE_REASON_LIMIT {
441 return reason.to_string();
442 }
443
444 let keep = VERIFY_NOTE_REASON_LIMIT.saturating_sub(VERIFY_NOTE_TRUNCATION.len());
445 let mut truncated = reason.chars().take(keep).collect::<String>();
446 truncated.push_str(VERIFY_NOTE_TRUNCATION);
447 truncated
448}
449
450#[derive(Debug, Clone, Serialize)]
451pub struct CodewikiRunSummary {
452 pub command: &'static str,
453 pub project_id: String,
454 pub project_root: String,
455 pub out_dir: String,
456 pub generated_pages: usize,
457 pub changed_paths: Vec<String>,
458 pub skipped: usize,
459 pub files: usize,
460 pub modules: usize,
461 pub symbols: usize,
462 pub ai_enabled: bool,
463 pub degraded_pages: Vec<String>,
468}
469
470#[derive(Debug, Clone, Default, Deserialize, Serialize)]
471pub(crate) struct CodewikiMeta {
472 pub(crate) docs: BTreeMap<String, CodewikiDocMeta>,
473 pub(crate) generated_docs: Vec<String>,
474 #[serde(default, skip_serializing_if = "Option::is_none")]
475 pub(crate) index_snapshot: Option<CodewikiIndexSnapshot>,
476 #[serde(default)]
477 pub(crate) ai_mode: String,
478}
479
480#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
481pub(crate) struct CodewikiDocMeta {
482 pub(crate) source_hashes: BTreeMap<String, String>,
483 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
487 pub(crate) degraded: bool,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
492 pub(crate) summary: Option<String>,
493 #[serde(default, skip_serializing_if = "String::is_empty")]
496 pub(crate) ai_mode: String,
497 #[serde(default)]
500 pub(crate) render_version: u32,
501 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
506 pub(crate) neighbor_hashes: BTreeMap<String, String>,
507 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub(crate) invalidation_key: Option<String>,
515}
516
517#[derive(Debug, Clone)]
520pub(crate) struct BuiltDoc {
521 pub(crate) path: String,
522 pub(crate) content: String,
523 pub(crate) degraded: bool,
524 pub(crate) summary: Option<String>,
527 pub(crate) neighbors: BTreeSet<String>,
532 pub(crate) invalidation_key: Option<String>,
535 pub(crate) invalidation_key_requires_sources: bool,
538}
539
540impl BuiltDoc {
541 pub(crate) fn healthy(path: impl Into<String>, content: String) -> Self {
542 Self {
543 path: path.into(),
544 content,
545 degraded: false,
546 summary: None,
547 neighbors: BTreeSet::new(),
548 invalidation_key: None,
549 invalidation_key_requires_sources: false,
550 }
551 }
552
553 pub(crate) fn derived(
557 path: impl Into<String>,
558 content: String,
559 invalidation_key: String,
560 ) -> Self {
561 Self {
562 path: path.into(),
563 content,
564 degraded: false,
565 summary: None,
566 neighbors: BTreeSet::new(),
567 invalidation_key: Some(invalidation_key),
568 invalidation_key_requires_sources: false,
569 }
570 }
571
572 pub(crate) fn with_source_sensitive_key(mut self) -> Self {
573 self.invalidation_key_requires_sources = true;
574 self
575 }
576
577 pub(crate) fn with_neighbors(mut self, neighbors: BTreeSet<String>) -> Self {
579 self.neighbors = neighbors;
580 self
581 }
582}
583
584#[derive(Debug, Clone, Default, Deserialize, Eq, PartialEq, Serialize)]
585pub(crate) struct CodewikiIndexSnapshot {
586 pub(crate) files: BTreeMap<String, CodewikiFileSnapshot>,
587 pub(crate) symbols: BTreeMap<String, CodewikiSymbolSnapshot>,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub(crate) graph_neighborhoods: Option<BTreeMap<String, String>>,
590 #[serde(default, skip_serializing_if = "Vec::is_empty")]
591 pub(crate) degraded_sources: Vec<String>,
592}
593
594#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
595pub(crate) struct CodewikiFileSnapshot {
596 pub(crate) content_hash: String,
597 pub(crate) symbol_count: usize,
598}
599
600#[derive(Debug, Clone, Deserialize, Eq, PartialEq, Serialize)]
601pub(crate) struct CodewikiSymbolSnapshot {
602 pub(crate) file_path: String,
603 pub(crate) name: String,
604 pub(crate) qualified_name: String,
605 pub(crate) kind: String,
606 pub(crate) line_start: usize,
607}
608
609pub type TextGenerator<'a> = dyn FnMut(&str, &str, PromptTier) -> Option<String> + 'a;
610
611pub type TextVerifier<'a> = dyn FnMut(&str, &str) -> Option<String> + 'a;
618
619#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
625pub enum PromptTier {
626 #[default]
627 Standard,
628 Module,
629 Aggregate,
630}
631
632#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
635pub enum AiDepth {
636 Sections,
638 #[default]
640 Files,
641 Symbols,
643}
644
645impl AiDepth {
646 pub(crate) fn includes_files(self) -> bool {
647 self >= AiDepth::Files
648 }
649
650 pub(crate) fn includes_symbols(self) -> bool {
651 self >= AiDepth::Symbols
652 }
653
654 pub(crate) fn mode_label(self) -> &'static str {
655 match self {
656 AiDepth::Sections => "sections",
657 AiDepth::Files => "files",
658 AiDepth::Symbols => "symbols",
659 }
660 }
661}
662
663#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord)]
668pub enum ProseDepth {
669 Brief,
671 #[default]
673 Standard,
674 Deep,
676}
677
678impl ProseDepth {
679 pub(crate) fn max_tokens(self) -> Option<usize> {
683 match self {
684 ProseDepth::Brief => Some(640),
685 ProseDepth::Standard => None,
686 ProseDepth::Deep => Some(2_400),
687 }
688 }
689}
690
691#[derive(Clone, Copy, Debug, PartialEq, Eq)]
695pub enum ProseRegister {
696 Newcomer,
699 Maintainer,
702 Agent,
704}
705
706#[derive(Clone, Debug, Default)]
707pub struct CodewikiAiOptions {
708 pub routing: Option<AiRouting>,
709 pub depth: AiDepth,
710 pub prose_depth: ProseDepth,
712 pub register: Option<ProseRegister>,
715 pub aggregate_profile: Option<String>,
720 pub verify_profile: Option<String>,
726 pub verify_model: Option<String>,
727 pub verify_api_key: Option<String>,
728}
729
730impl SourceSpan {
731 pub(crate) fn from_symbol(symbol: &Symbol) -> Self {
732 Self {
733 file: symbol.file_path.clone(),
734 line_start: symbol.line_start,
735 line_end: symbol.line_end,
736 }
737 }
738
739 pub(crate) fn citation(&self) -> String {
740 if self.line_start == self.line_end {
741 format!("[{}:{}]", self.file, self.line_start)
742 } else {
743 format!("[{}:{}-{}]", self.file, self.line_start, self.line_end)
744 }
745 }
746
747 pub(crate) fn contains(&self, file: &str, line_start: usize, line_end: usize) -> bool {
748 self.file == file && self.line_start <= line_start && line_end <= self.line_end
749 }
750}