use serde::{Deserialize, Serialize};
use crate::action::SuggestedAction;
use crate::error_codes::ErrorCode;
use crate::hints::ToolHints;
use crate::relationships::Relationships;
pub const OPERATION_SCHEMA_VERSION: &str = "2.0.0";
pub const QUERY_SCHEMA_VERSION: &str = "1.0.0";
pub const TOOL_NAME: &str = "splice";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct JsonResponse<T> {
pub schema_version: String,
pub execution_id: String,
pub data: T,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial: Option<bool>,
}
impl<T> JsonResponse<T> {
pub fn new(data: T, execution_id: &str) -> Self {
JsonResponse {
schema_version: QUERY_SCHEMA_VERSION.to_string(),
execution_id: execution_id.to_string(),
data,
tool: Some(TOOL_NAME.to_string()),
timestamp: Some(chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
partial: None,
}
}
pub fn with_partial(mut self, partial: bool) -> Self {
self.partial = Some(partial);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OperationResult {
pub schema_version: String,
pub execution_id: String,
pub operation_type: String,
pub status: String,
pub message: String,
pub tool: String,
pub timestamp: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub workspace: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<OperationData>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorDetails>,
#[serde(skip_serializing_if = "Option::is_none")]
pub preview_report: Option<serde_json::Value>,
}
impl OperationResult {
pub fn new(operation_type: String) -> Self {
Self::with_execution_id(operation_type, None)
}
pub fn with_execution_id(operation_type: String, execution_id: Option<String>) -> Self {
use uuid::Uuid;
Self {
schema_version: OPERATION_SCHEMA_VERSION.to_string(),
execution_id: execution_id.unwrap_or_else(|| Uuid::new_v4().to_string()),
operation_type,
status: "ok".to_string(),
message: String::new(),
tool: TOOL_NAME.to_string(),
timestamp: chrono::Utc::now().to_rfc3339(),
workspace: None,
result: None,
error: None,
preview_report: None,
}
}
pub fn set_execution_id(mut self, execution_id: String) -> Self {
self.execution_id = execution_id;
self
}
pub fn success(mut self, message: String) -> Self {
self.status = "ok".to_string();
self.message = message;
self
}
pub fn error(mut self, message: String, error: ErrorDetails) -> Self {
self.status = "error".to_string();
self.message = message;
self.error = Some(error);
self
}
pub fn with_workspace(mut self, workspace: String) -> Self {
self.workspace = Some(workspace);
self
}
pub fn with_result(mut self, result: OperationData) -> Self {
self.result = Some(result);
self
}
pub fn with_preview_report(mut self, preview_report: serde_json::Value) -> Self {
self.preview_report = Some(preview_report);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum OperationData {
#[serde(rename = "patch")]
Patch(PatchResult),
#[serde(rename = "delete")]
Delete(DeleteResult),
#[serde(rename = "plan")]
Plan(PlanResult),
#[serde(rename = "query")]
Query(QueryResult),
#[serde(rename = "apply_files")]
ApplyFiles(ApplyFilesResult),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatchResult {
pub file: String,
pub symbol: String,
pub kind: String,
pub spans: Vec<SpanResult>,
pub before_hash: String,
pub after_hash: String,
pub lines_added: usize,
pub lines_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeleteResult {
pub file: String,
pub symbol: String,
pub kind: String,
pub spans: Vec<SpanResult>,
pub bytes_removed: usize,
pub lines_removed: usize,
pub references_removed: usize,
pub file_checksum_before: String,
pub span_checksums: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanResult {
pub total_steps: usize,
pub steps_completed: usize,
pub steps: Vec<StepResult>,
pub files_affected: Vec<String>,
pub total_bytes_changed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct StepResult {
pub step: usize,
pub status: String,
pub message: String,
pub file: String,
pub symbol: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryResult {
pub labels: Vec<String>,
pub count: usize,
pub symbols: Vec<SpanResult>,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_symbols: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub partial: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_reasons: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LabelQueryResponse {
pub labels: Vec<String>,
pub symbols: Vec<SymbolMatch>,
pub count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_symbols: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_bytes: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub next_offset: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub truncation_reasons: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetResponse {
pub symbol: SymbolMatch,
pub content: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApplyFilesResult {
pub glob_pattern: String,
pub find_pattern: String,
pub replace_pattern: String,
pub files_matched: usize,
pub files_modified: usize,
pub files: Vec<FilePatternResult>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilePatternResult {
pub file: String,
pub matches: usize,
pub replacements: usize,
pub spans: Vec<SpanResult>,
pub before_hash: String,
pub after_hash: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanContext {
pub before: Vec<String>,
pub selected: Vec<String>,
pub after: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanSemantics {
pub kind: String,
pub language: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanChecksums {
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_before: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksum_after: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_checksum_before: Option<String>,
}
impl Default for SpanChecksums {
fn default() -> Self {
Self {
checksum_before: None,
checksum_after: None,
file_checksum_before: None,
}
}
}
fn generate_span_id(file_path: &str, byte_start: usize, byte_end: usize) -> String {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(file_path.as_bytes());
hasher.update(b":");
hasher.update(byte_start.to_be_bytes());
hasher.update(b":");
hasher.update(byte_end.to_be_bytes());
let result = hasher.finalize();
format!(
"{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7],
)
}
fn normalize_checksum(value: String) -> String {
if value.starts_with("sha256:") {
value
} else {
format!("sha256:{}", value)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Span {
pub span_id: String,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<SpanContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantics: Option<SpanSemantics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationships: Option<Relationships>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksums: Option<SpanChecksums>,
}
impl Span {
pub fn new(
file_path: String,
byte_start: usize,
byte_end: usize,
start_line: usize,
start_col: usize,
end_line: usize,
end_col: usize,
) -> Self {
let span_id = generate_span_id(&file_path, byte_start, byte_end);
Span {
span_id,
file_path,
byte_start,
byte_end,
start_line,
start_col,
end_line,
end_col,
context: None,
semantics: None,
relationships: None,
checksums: None,
}
}
pub fn generate_id(file_path: &str, byte_start: usize, byte_end: usize) -> String {
generate_span_id(file_path, byte_start, byte_end)
}
pub fn with_context(mut self, context: SpanContext) -> Self {
self.context = Some(context);
self
}
pub fn with_semantics(mut self, semantics: SpanSemantics) -> Self {
self.semantics = Some(semantics);
self
}
pub fn with_relationships(mut self, relationships: Relationships) -> Self {
self.relationships = Some(relationships);
self
}
pub fn with_checksums(mut self, checksums: SpanChecksums) -> Self {
self.checksums = Some(checksums);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolMatch {
pub match_id: String,
pub span: Span,
pub name: String,
pub kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_id: Option<String>,
}
impl SymbolMatch {
pub fn generate_match_id(symbol_name: &str, file_path: &str, byte_start: usize) -> String {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
symbol_name.hash(&mut hasher);
file_path.hash(&mut hasher);
byte_start.hash(&mut hasher);
format!("{:x}", hasher.finish())
}
pub fn new(
name: String,
kind: String,
span: Span,
parent: Option<String>,
symbol_id: Option<String>,
) -> Self {
let match_id = Self::generate_match_id(&name, &span.file_path, span.byte_start);
SymbolMatch {
match_id,
span,
name,
kind,
parent,
symbol_id,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SpanResult {
pub file_path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
pub span_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub match_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub before_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after_hash: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<SpanContext>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semantics: Option<SpanSemantics>,
#[serde(skip_serializing_if = "Option::is_none")]
pub checksums: Option<SpanChecksums>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error_code: Option<ErrorCode>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relationships: Option<Relationships>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_hints: Option<ToolHints>,
#[serde(skip_serializing_if = "Option::is_none")]
pub suggested_action: Option<SuggestedAction>,
}
impl SpanResult {
pub fn from_byte_span(file_path: String, byte_start: usize, byte_end: usize) -> Self {
let span_id = generate_span_id(&file_path, byte_start, byte_end);
Self {
file_path,
symbol: None,
kind: None,
byte_start,
byte_end,
start_line: 0,
end_line: 0,
start_col: 0,
end_col: 0,
span_id,
match_id: None,
before_hash: None,
after_hash: None,
context: None,
semantics: None,
checksums: None,
error_code: None,
relationships: None,
tool_hints: None,
suggested_action: None,
}
}
pub fn with_symbol(mut self, symbol: String, kind: String) -> Self {
self.symbol = Some(symbol);
self.kind = Some(kind);
self
}
pub fn with_hashes(mut self, before: String, after: String) -> Self {
self.before_hash = Some(before);
self.after_hash = Some(after);
self
}
pub fn with_line_col(
mut self,
line_start: usize,
line_end: usize,
col_start: usize,
col_end: usize,
) -> Self {
self.start_line = line_start;
self.end_line = line_end;
self.start_col = col_start;
self.end_col = col_end;
self
}
pub fn with_match_id(mut self, match_id: String) -> Self {
self.match_id = Some(match_id);
self
}
pub fn with_span_checksums(mut self, before: String, after: String) -> Self {
let checksums = self.checksums.get_or_insert_with(SpanChecksums::default);
checksums.checksum_before = Some(normalize_checksum(before));
checksums.checksum_after = Some(normalize_checksum(after));
self
}
pub fn with_context(mut self, context: SpanContext) -> Self {
self.context = Some(context);
self
}
pub fn with_semantic_kind(mut self, kind: impl Into<String>) -> Self {
let kind = kind.into();
let semantics = self.semantics.get_or_insert_with(|| SpanSemantics {
kind: "unknown".to_string(),
language: "unknown".to_string(),
});
semantics.kind = kind;
self
}
pub fn with_language(mut self, language: impl Into<String>) -> Self {
let language = language.into();
let semantics = self.semantics.get_or_insert_with(|| SpanSemantics {
kind: "unknown".to_string(),
language: "unknown".to_string(),
});
semantics.language = language;
self
}
pub fn with_checksum_before(mut self, checksum: impl Into<String>) -> Self {
let checksums = self.checksums.get_or_insert_with(SpanChecksums::default);
checksums.checksum_before = Some(normalize_checksum(checksum.into()));
self
}
pub fn with_file_checksum_before(mut self, checksum: impl Into<String>) -> Self {
let checksums = self.checksums.get_or_insert_with(SpanChecksums::default);
checksums.file_checksum_before = Some(normalize_checksum(checksum.into()));
self
}
pub fn with_error_code(mut self, error_code: ErrorCode) -> Self {
self.error_code = Some(error_code);
self
}
pub fn with_semantic_info(
mut self,
kind: impl Into<String>,
language: impl Into<String>,
) -> Self {
self.semantics = Some(SpanSemantics {
kind: kind.into(),
language: language.into(),
});
self
}
pub fn with_both_checksums(
mut self,
checksum_before: impl Into<String>,
file_checksum_before: impl Into<String>,
) -> Self {
let checksum_after = self
.checksums
.as_ref()
.and_then(|c| c.checksum_after.clone());
self.checksums = Some(SpanChecksums {
checksum_before: Some(normalize_checksum(checksum_before.into())),
checksum_after,
file_checksum_before: Some(normalize_checksum(file_checksum_before.into())),
});
self
}
pub fn with_relationships(mut self, relationships: Relationships) -> Self {
self.relationships = Some(relationships);
self
}
pub fn with_tool_hints(mut self, hints: ToolHints) -> Self {
self.tool_hints = Some(hints);
self
}
pub fn with_suggested_action(mut self, action: SuggestedAction) -> Self {
self.suggested_action = Some(action);
self
}
}
impl PartialEq for SpanResult {
fn eq(&self, other: &Self) -> bool {
self.file_path == other.file_path
&& self.byte_start == other.byte_start
&& self.byte_end == other.byte_end
}
}
impl Eq for SpanResult {}
impl PartialOrd for SpanResult {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SpanResult {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.file_path.cmp(&other.file_path) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.byte_start.cmp(&other.byte_start) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
self.byte_end.cmp(&other.byte_end)
}
}
impl PartialEq for FilePatternResult {
fn eq(&self, other: &Self) -> bool {
self.file == other.file
}
}
impl Eq for FilePatternResult {}
impl PartialOrd for FilePatternResult {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for FilePatternResult {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.file.cmp(&other.file)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrorDetails {
pub kind: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<DiagnosticPayload>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DiagnosticPayload {
pub tool: String,
pub level: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub file: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub column: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub code: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_version: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remediation: Option<String>,
}
impl PartialEq for DiagnosticPayload {
fn eq(&self, other: &Self) -> bool {
self.tool == other.tool
&& self.file == other.file
&& self.line == other.line
&& self.column == other.column
&& self.level == other.level
&& self.message == other.message
}
}
impl Eq for DiagnosticPayload {}
impl PartialOrd for DiagnosticPayload {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for DiagnosticPayload {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match self.tool.cmp(&other.tool) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.file.cmp(&other.file) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.line.cmp(&other.line) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.column.cmp(&other.column) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
match self.level.cmp(&other.level) {
std::cmp::Ordering::Equal => {}
ord => return ord,
}
self.message.cmp(&other.message)
}
}
impl From<crate::patch::FilePatchSummary> for SpanResult {
fn from(summary: crate::patch::FilePatchSummary) -> Self {
let file_path = summary.file.to_string_lossy().to_string();
let span_id = generate_span_id(&file_path, 0, 0);
Self {
file_path,
symbol: None,
kind: None,
byte_start: 0,
byte_end: 0,
start_line: 0,
end_line: 0,
start_col: 0,
end_col: 0,
span_id,
match_id: None,
before_hash: Some(summary.before_hash),
after_hash: Some(summary.after_hash),
context: None,
semantics: None,
checksums: None,
error_code: None,
relationships: None,
tool_hints: None,
suggested_action: None,
}
}
}
impl From<crate::resolve::ResolvedSpan> for SpanResult {
fn from(span: crate::resolve::ResolvedSpan) -> Self {
let span_id = generate_span_id(&span.file_path, span.byte_start, span.byte_end);
Self {
file_path: span.file_path,
symbol: Some(span.name),
kind: Some(span.kind),
byte_start: span.byte_start,
byte_end: span.byte_end,
start_line: span.line_start,
end_line: span.line_end,
start_col: span.col_start,
end_col: span.col_end,
span_id,
match_id: Some(span.match_id),
before_hash: None,
after_hash: None,
context: None,
semantics: None,
checksums: None,
error_code: None,
relationships: None,
tool_hints: None,
suggested_action: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusResponse {
pub files: usize,
pub symbols: usize,
pub references: usize,
pub calls: usize,
pub code_chunks: usize,
pub db_path: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FindResponse {
pub symbols: Vec<MagellanSymbol>,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagellanSymbol {
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_format: Option<String>,
pub name: String,
pub kind: String,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RefsResponse {
pub symbol: MagellanSymbol,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub callers: Vec<MagellanCallReference>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub callees: Vec<MagellanCallReference>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagellanCallReference {
pub symbol: MagellanSymbol,
pub call_site: MagellanSpan,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagellanSpan {
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub start_col: usize,
pub end_line: usize,
pub end_col: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FilesResponse {
pub files: Vec<MagellanFileMetadata>,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MagellanFileMetadata {
pub path: String,
pub hash: String,
pub last_indexed_at: i64,
pub last_modified: i64,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_count: Option<usize>,
}
impl From<crate::graph::magellan_integration::DatabaseStats> for StatusResponse {
fn from(stats: crate::graph::magellan_integration::DatabaseStats) -> Self {
Self {
files: stats.files,
symbols: stats.symbols,
references: stats.references,
calls: stats.calls,
code_chunks: stats.code_chunks,
db_path: String::new(), }
}
}
impl From<crate::graph::magellan_integration::SymbolInfo> for MagellanSymbol {
fn from(info: crate::graph::magellan_integration::SymbolInfo) -> Self {
let symbol_id = crate::symbol_id::generate_v2(&info.name, &info.file_path, info.byte_start);
Self {
symbol_id: Some(symbol_id.as_str().to_string()),
id_format: Some("v2".to_string()),
name: info.name,
kind: info.kind,
file_path: info.file_path,
byte_start: info.byte_start,
byte_end: info.byte_end,
start_line: 0,
end_line: 0,
start_col: 0,
end_col: 0,
}
}
}
impl From<crate::graph::magellan_integration::CallReference> for MagellanCallReference {
fn from(cr: crate::graph::magellan_integration::CallReference) -> Self {
Self {
symbol: cr.symbol.into(),
call_site: MagellanSpan {
file_path: cr.call_site.file_path,
byte_start: cr.call_site.byte_start,
byte_end: cr.call_site.byte_end,
start_line: cr.call_site.start_line,
start_col: cr.call_site.start_col,
end_line: cr.call_site.end_line,
end_col: cr.call_site.end_col,
},
}
}
}
impl From<crate::graph::magellan_integration::FileMetadata> for MagellanFileMetadata {
fn from(fm: crate::graph::magellan_integration::FileMetadata) -> Self {
Self {
path: fm.path,
hash: fm.hash,
last_indexed_at: fm.last_indexed_at,
last_modified: fm.last_modified,
symbol_count: fm.symbol_count,
}
}
}
impl From<crate::graph::magellan_integration::CallRelationships> for RefsResponse {
fn from(rels: crate::graph::magellan_integration::CallRelationships) -> Self {
Self {
symbol: rels.symbol.into(),
callers: rels.callers.into_iter().map(Into::into).collect(),
callees: rels.callees.into_iter().map(Into::into).collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReachabilityResult {
pub symbol: SymbolInfo,
pub direction: String, pub max_depth: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub forward: Option<ReachabilityChain>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reverse: Option<ReachabilityChain>,
pub affected_files: Vec<AffectedFile>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReachabilityChain {
pub count: usize,
pub depth: usize,
pub symbols: Vec<ReachableSymbol>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReachableSymbol {
pub symbol: SymbolInfo,
pub depth: usize,
pub path: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AffectedFile {
pub path: String,
pub symbol_count: usize,
pub is_root: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadCodeResult {
pub entry_point: SymbolInfo,
pub total_symbols: usize,
pub reachable_count: usize,
pub dead_count: usize,
pub dead_by_file: Vec<DeadCodeByFile>,
pub excluded_public: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadCodeByFile {
pub path: String,
pub count: usize,
pub symbols: Vec<DeadSymbol>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeadSymbol {
pub symbol: SymbolInfo,
pub reason: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CycleDetectionResult {
pub total_cycles: usize,
pub max_cycles: usize,
pub truncated: bool,
pub cycles: Vec<CycleInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
pub queried_symbol: Option<SymbolInfo>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CycleInfo {
pub id: String,
pub size: usize,
pub members: Vec<SymbolInfo>,
pub representative: SymbolInfo,
pub is_self_loop: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolInfo {
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_format: Option<String>,
pub name: String,
pub kind: String,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CondensationResult {
pub scc_count: usize,
pub cycle_scc_count: usize,
pub singleton_count: usize,
pub sccs: Vec<CondensedScc>,
pub edges: Vec<SccEdge>,
#[serde(skip_serializing_if = "Option::is_none")]
pub levels: Option<Vec<LevelInfo>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CondensedScc {
pub id: String,
pub size: usize,
pub is_cycle: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub members: Option<Vec<SymbolInfo>>,
pub representative: SymbolInfo,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SccEdge {
pub from: String,
pub to: String,
pub weight: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LevelInfo {
pub level: usize,
pub scc_ids: Vec<String>,
pub count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SliceResult {
pub target: SymbolInfo,
pub direction: String, pub max_depth: Option<usize>,
pub symbols: Vec<SlicedSymbol>,
pub affected_files: Vec<AffectedFile>,
pub stats: SliceStats,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SlicedSymbol {
pub symbol: SymbolInfo,
pub distance: usize,
pub is_target: bool,
pub relationship: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SliceStats {
pub total_symbols: usize,
pub max_distance: usize,
pub affected_file_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportResponse {
pub schema_version: String,
pub timestamp: String,
pub db_path: String,
pub data: ExportData,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportData {
pub files: Vec<FileExport>,
pub symbols: Vec<SymbolExport>,
pub references: Vec<ReferenceExport>,
pub calls: Vec<CallExport>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileExport {
pub path: String,
pub hash: String,
pub last_indexed_at: i64,
pub last_modified: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SymbolExport {
pub symbol_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id_format: Option<String>,
pub name: String,
pub kind: String,
pub file_path: String,
pub byte_start: usize,
pub byte_end: usize,
pub start_line: usize,
pub end_line: usize,
pub start_col: usize,
pub end_col: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReferenceExport {
pub from_symbol_id: String,
pub to_symbol_id: String,
pub reference_kind: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CallExport {
pub caller_symbol_id: String,
pub callee_symbol_id: String,
pub call_site_file: String,
pub call_site_line: usize,
}
pub const EXPORT_SCHEMA_VERSION: &str = "1.0.0";
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_span_id_deterministic() {
let span1 = SpanResult::from_byte_span("test.rs".to_string(), 10, 20);
let span2 = SpanResult::from_byte_span("test.rs".to_string(), 10, 20);
assert_eq!(
span1.span_id, span2.span_id,
"Same span inputs should produce the same span_id"
);
}
#[test]
fn test_match_id_preserved() {
let match_id = uuid::Uuid::new_v4().to_string();
let span = SpanResult::from_byte_span("test.rs".to_string(), 10, 20)
.with_match_id(match_id.clone());
assert_eq!(
span.match_id,
Some(match_id),
"match_id should be preserved when set"
);
}
#[test]
fn test_match_id_from_resolved_span() {
let match_id = uuid::Uuid::new_v4().to_string();
let span1 = SpanResult::from_byte_span("test.rs".to_string(), 10, 20)
.with_match_id(match_id.clone());
assert_eq!(span1.match_id, Some(match_id));
assert!(!span1.span_id.is_empty());
}
#[test]
fn test_from_byte_span_generates_distinct_span_ids() {
let span1 = SpanResult::from_byte_span("file.rs".to_string(), 0, 10);
let span2 = SpanResult::from_byte_span("file.rs".to_string(), 0, 10);
let span3 = SpanResult::from_byte_span("file.rs".to_string(), 20, 30);
assert_eq!(span1.span_id, span2.span_id);
assert_ne!(span2.span_id, span3.span_id);
assert_ne!(span1.span_id, span3.span_id);
}
}