Skip to main content

meta_ast/output/shard/
file.rs

1//! Shard file record representation and JSONL read/write operations.
2
3use std::io::{BufRead, Write};
4use std::path::{Path, PathBuf};
5
6use petgraph::visit::EdgeRef;
7use serde::{Deserialize, Serialize};
8
9use crate::error::Diagnostic;
10use crate::graph::{CodeGraph, NodeData};
11use crate::language::LangId;
12use crate::model::{
13    FileExtraction, IdGenerator, SourceRange, Symbol, SymbolId, SymbolKind, UnresolvedImport,
14    UnresolvedReference, Visibility,
15};
16use crate::output::shard::edge::{ShardEdge, validate_edge};
17use crate::output::shard::error::ShardError;
18use crate::output::shard::name::{node_belongs_to_file, normalized_path, stable_node_name};
19
20pub const SHARD_SCHEMA_VERSION: u32 = 2;
21
22/// A per-file shard record stored in `.meta-ast/shards/<n>.jsonl`.
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct ShardFile {
25    pub schema_version: u32,
26    pub path: PathBuf,
27    pub language: LangId,
28    pub symbols: Vec<ShardSymbol>,
29    pub imports: Vec<UnresolvedImport>,
30    pub references: Vec<UnresolvedReference>,
31    pub diagnostics: Vec<Diagnostic>,
32    pub ast_node_count: usize,
33    pub edges: Vec<ShardEdge>,
34}
35
36/// Symbol representation in a shard file without runtime-local identifiers.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct ShardSymbol {
39    pub name: String,
40    pub kind: SymbolKind,
41    pub source_range: SourceRange,
42    pub visibility: Option<Visibility>,
43    pub signature: Option<String>,
44    pub docstring: Option<String>,
45    pub is_async: bool,
46}
47
48/// Extraction result reconstituted from a shard record.
49#[derive(Debug)]
50pub struct LoadedShard {
51    pub file: FileExtraction,
52    pub edges: Vec<ShardEdge>,
53}
54
55impl ShardFile {
56    /// Convert an in-memory `FileExtraction` and graph into a shard record.
57    pub fn from_extraction(
58        extraction: &FileExtraction,
59        graph: &CodeGraph,
60    ) -> Result<Self, ShardError> {
61        normalized_path(&extraction.path)?;
62        for diagnostic in &extraction.diagnostics {
63            normalized_path(&diagnostic.path)?;
64        }
65        let symbols = extraction.symbols.iter().map(ShardSymbol::from).collect();
66        let mut edges = graph
67            .graph()
68            .edge_references()
69            .filter(|edge| {
70                !matches!(graph.graph()[edge.source()], NodeData::Data(_))
71                    && !matches!(graph.graph()[edge.target()], NodeData::Data(_))
72                    && (node_belongs_to_file(graph, edge.source(), &extraction.path)
73                        || node_belongs_to_file(graph, edge.target(), &extraction.path))
74            })
75            .map(|edge| {
76                let source_name = stable_node_name(graph, edge.source())?;
77                let target_name = stable_node_name(graph, edge.target())?;
78                Ok(ShardEdge {
79                    source_name,
80                    target_name,
81                    kind: edge.weight().kind.into(),
82                    confidence: edge.weight().confidence,
83                    flow_kind: edge.weight().flow_kind.map(Into::into),
84                })
85            })
86            .collect::<Result<Vec<_>, ShardError>>()?;
87        edges.sort_by(|left, right| {
88            (
89                &left.source_name,
90                &left.target_name,
91                left.kind,
92                left.flow_kind,
93            )
94                .cmp(&(
95                    &right.source_name,
96                    &right.target_name,
97                    right.kind,
98                    right.flow_kind,
99                ))
100        });
101        for (edge_index, edge) in edges.iter().enumerate() {
102            validate_edge(edge, 1, edge_index)?;
103        }
104
105        Ok(Self {
106            schema_version: SHARD_SCHEMA_VERSION,
107            path: extraction.path.clone(),
108            language: extraction.lang,
109            symbols,
110            imports: extraction.imports.clone(),
111            references: extraction.references.clone(),
112            diagnostics: extraction.diagnostics.clone(),
113            ast_node_count: extraction.ast_node_count,
114            edges,
115        })
116    }
117
118    /// Reconstitute a `FileExtraction` and assign fresh IDs using the provided generator.
119    pub fn load(self, id_gen: &IdGenerator<SymbolId>) -> Result<LoadedShard, ShardError> {
120        if self.schema_version != SHARD_SCHEMA_VERSION {
121            return Err(ShardError::SchemaVersion {
122                line: 1,
123                found: self.schema_version,
124                expected: SHARD_SCHEMA_VERSION,
125            });
126        }
127
128        let symbols = self
129            .symbols
130            .into_iter()
131            .map(|symbol| symbol.into_symbol(&self.path, self.language, id_gen.next()))
132            .collect();
133        let mut file = FileExtraction::empty(self.path, self.language);
134        file.symbols = symbols;
135        file.imports = self.imports;
136        file.references = self.references;
137        file.diagnostics = self.diagnostics;
138        file.ast_node_count = self.ast_node_count;
139
140        Ok(LoadedShard {
141            file,
142            edges: self.edges,
143        })
144    }
145}
146
147impl From<&Symbol> for ShardSymbol {
148    fn from(symbol: &Symbol) -> Self {
149        Self {
150            name: symbol.name.clone(),
151            kind: symbol.kind,
152            source_range: symbol.source_range.clone(),
153            visibility: symbol.visibility,
154            signature: symbol.signature.clone(),
155            docstring: symbol.docstring.clone(),
156            is_async: symbol.is_async,
157        }
158    }
159}
160
161impl ShardSymbol {
162    fn into_symbol(self, file_path: &Path, language: LangId, id: SymbolId) -> Symbol {
163        Symbol {
164            id,
165            name: self.name,
166            kind: self.kind,
167            language,
168            file_path: file_path.to_path_buf(),
169            source_range: self.source_range,
170            visibility: self.visibility,
171            signature: self.signature,
172            docstring: self.docstring,
173            is_async: self.is_async,
174        }
175    }
176}
177
178/// Write shard records to a writer in JSONL format.
179///
180/// Use `std::io::BufWriter` when writing to a file to prevent frequent write system calls.
181pub fn write_shard<W: Write>(mut writer: W, files: &[ShardFile]) -> Result<(), ShardError> {
182    for (line_index, file) in files.iter().enumerate() {
183        validate_file(file, line_index + 1)?;
184        serde_json::to_writer(&mut writer, file).map_err(ShardError::Encode)?;
185        writer.write_all(b"\n")?;
186    }
187    writer.flush()?;
188    Ok(())
189}
190
191pub fn read_shard<R: BufRead>(reader: R) -> Result<Vec<ShardFile>, ShardError> {
192    let mut files = Vec::new();
193    for (line_index, line) in reader.lines().enumerate() {
194        let line_number = line_index + 1;
195        let line = line?;
196        if line.trim().is_empty() {
197            continue;
198        }
199        let file: ShardFile = serde_json::from_str(&line).map_err(|source| ShardError::Decode {
200            line: line_number,
201            source,
202        })?;
203        validate_file(&file, line_number)?;
204        files.push(file);
205    }
206    Ok(files)
207}
208
209pub(crate) fn validate_file(file: &ShardFile, line: usize) -> Result<(), ShardError> {
210    if file.schema_version != SHARD_SCHEMA_VERSION {
211        return Err(ShardError::SchemaVersion {
212            line,
213            found: file.schema_version,
214            expected: SHARD_SCHEMA_VERSION,
215        });
216    }
217    normalized_path(&file.path)?;
218    for diagnostic in &file.diagnostics {
219        normalized_path(&diagnostic.path)?;
220    }
221    for (edge_index, edge) in file.edges.iter().enumerate() {
222        validate_edge(edge, line, edge_index)?;
223    }
224    Ok(())
225}