Skip to main content

sealmap_corpus/
index.rs

1//! `_index.json`: the merge map for orchestrating agents.
2
3use std::collections::BTreeMap;
4
5use sealmap_model::{Codebase, CodebaseStats, Confidence, ContentHash, Fingerprint, SourcePath, Span, SymbolId};
6use serde::{Deserialize, Serialize};
7
8/// Kind of diagram fragment.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "snake_case")]
11pub enum FragmentKind {
12    /// Per-file `classDiagram`.
13    Structure,
14    /// Per-callable `sequenceDiagram`.
15    Sequence,
16}
17
18/// One call made by a sequence fragment.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct CallRef {
21    /// Callee id.
22    pub target: SymbolId,
23    /// Resolution confidence.
24    pub confidence: Confidence,
25    /// 1-based line of the call site.
26    pub line: u32,
27    /// Id of the fragment that renders the callee's own sequence, if any.
28    /// Following these links is how an orchestrator inlines or stitches
29    /// sequences across files.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub expands: Option<String>,
32}
33
34/// One diagram inside a document.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36pub struct FragmentEntry {
37    /// Fragment id: the symbol id for sequences, `structure:<file>` for
38    /// structure diagrams. Unique across the corpus.
39    pub id: String,
40    /// Diagram kind.
41    pub kind: FragmentKind,
42    /// Document holding it.
43    pub document: SourcePath,
44    /// Symbol the fragment describes (callable or module).
45    pub symbol: SymbolId,
46    /// Source span of that symbol.
47    pub span: Span,
48    /// The symbol's contract fingerprint (see `sealmap_model::Symbol`).
49    pub sig_hash: Fingerprint,
50    /// The symbol's implementation fingerprint.
51    pub body_hash: Fingerprint,
52    /// Mermaid ids of participants / classes, in diagram order.
53    pub participants: Vec<SymbolId>,
54    /// Calls, in source order (sequence fragments only).
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub calls: Vec<CallRef>,
57    /// Messages elided by `max_messages` (0 when complete).
58    #[serde(default, skip_serializing_if = "is_zero")]
59    pub truncated: usize,
60    /// Hash of the fragment's Mermaid text, for caching merged results.
61    pub hash: ContentHash,
62}
63
64fn is_zero(n: &usize) -> bool {
65    *n == 0
66}
67
68/// One document (one per source file).
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
70pub struct DocumentEntry {
71    /// Source file.
72    pub source: SourcePath,
73    /// Document path.
74    pub document: SourcePath,
75    /// Module the source defines.
76    pub module: SymbolId,
77    /// Hash of the source the document was generated from.
78    pub source_hash: ContentHash,
79    /// Hash of the document text.
80    pub document_hash: ContentHash,
81    /// Fragments in document order.
82    pub fragments: Vec<FragmentEntry>,
83}
84
85/// The corpus index.
86#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87pub struct Index {
88    /// [`crate::CORPUS_SCHEMA_VERSION`].
89    pub schema_version: u32,
90    /// Generator name and version, e.g. `sealmap-corpus 0.1.0`.
91    pub generator: String,
92    /// Codebase name.
93    pub codebase: String,
94    /// Model statistics.
95    pub stats: CodebaseStats,
96    /// Documents in source-path order.
97    pub documents: Vec<DocumentEntry>,
98}
99
100impl Index {
101    pub(crate) fn new(cb: &Codebase) -> Self {
102        Self {
103            schema_version: crate::CORPUS_SCHEMA_VERSION,
104            generator: format!("sealmap-corpus {}", env!("CARGO_PKG_VERSION")),
105            codebase: cb.name.clone(),
106            stats: cb.stats(),
107            documents: Vec::new(),
108        }
109    }
110
111    /// Fill in [`CallRef::expands`] once all fragments are known.
112    pub(crate) fn link_expansions(&mut self) {
113        let seqs: BTreeMap<String, ()> =
114            self.fragments().filter(|f| f.kind == FragmentKind::Sequence).map(|f| (f.id.clone(), ())).collect();
115        for doc in &mut self.documents {
116            for frag in &mut doc.fragments {
117                for call in &mut frag.calls {
118                    let target = call.target.to_string();
119                    if seqs.contains_key(&target) {
120                        call.expands = Some(target);
121                    }
122                }
123            }
124        }
125    }
126
127    /// All fragments in document order.
128    pub fn fragments(&self) -> impl Iterator<Item = &FragmentEntry> {
129        self.documents.iter().flat_map(|d| d.fragments.iter())
130    }
131
132    /// Look up a fragment by id.
133    pub fn fragment(&self, id: &str) -> Option<&FragmentEntry> {
134        self.fragments().find(|f| f.id == id)
135    }
136}