use postgres::Row;
use serde::{Deserialize, Serialize};
use std::fmt;
use uuid::Uuid;
use crate::utils::i64_to_usize;
pub const CODE_INDEX_UUID_NAMESPACE: Uuid = Uuid::from_bytes([
0xc0, 0xde, 0x1d, 0xe0, 0x00, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
]);
pub const SOURCE_SYSTEM_GCODE: &str = "gcode";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
pub enum ProjectionProvenance {
#[default]
Extracted,
Inferred,
Ambiguous,
}
impl ProjectionProvenance {
pub fn as_str(self) -> &'static str {
match self {
Self::Extracted => "EXTRACTED",
Self::Inferred => "INFERRED",
Self::Ambiguous => "AMBIGUOUS",
}
}
pub fn from_wire_value(value: &str) -> Option<Self> {
match value {
"EXTRACTED" | "extracted" => Some(Self::Extracted),
"INFERRED" | "inferred" => Some(Self::Inferred),
"AMBIGUOUS" | "ambiguous" => Some(Self::Ambiguous),
_ => None,
}
}
}
impl fmt::Display for ProjectionProvenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ProjectionMetadata {
pub provenance: ProjectionProvenance,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<f64>,
pub source_system: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_file_path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_line: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub source_symbol_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub matching_method: Option<String>,
}
impl ProjectionMetadata {
pub fn new(provenance: ProjectionProvenance, source_system: impl Into<String>) -> Self {
Self {
provenance,
confidence: None,
source_system: source_system.into(),
source_file_path: None,
source_line: None,
source_symbol_id: None,
matching_method: None,
}
}
pub fn gcode_extracted() -> Self {
Self::new(ProjectionProvenance::Extracted, SOURCE_SYSTEM_GCODE).with_confidence(Some(1.0))
}
pub fn inferred(source_system: impl Into<String>, confidence: Option<f64>) -> Self {
Self::new(ProjectionProvenance::Inferred, source_system).with_confidence(confidence)
}
pub fn ambiguous(source_system: impl Into<String>, confidence: Option<f64>) -> Self {
Self::new(ProjectionProvenance::Ambiguous, source_system).with_confidence(confidence)
}
pub fn with_confidence(mut self, confidence: Option<f64>) -> Self {
self.confidence = confidence;
self
}
pub fn with_source_file_path(mut self, file_path: impl Into<String>) -> Self {
self.source_file_path = Some(file_path.into());
self
}
pub fn with_source_line(mut self, line: usize) -> Self {
self.source_line = Some(line);
self
}
pub fn with_source_symbol_id(mut self, symbol_id: impl Into<String>) -> Self {
self.source_symbol_id = Some(symbol_id.into());
self
}
pub fn with_matching_method(mut self, matching_method: impl Into<String>) -> Self {
self.matching_method = Some(matching_method.into());
self
}
pub fn is_hypothesis(&self) -> bool {
matches!(
self.provenance,
ProjectionProvenance::Inferred | ProjectionProvenance::Ambiguous
)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbol {
pub id: String,
pub project_id: String,
pub file_path: String,
pub name: String,
pub qualified_name: String,
pub kind: String,
pub language: String,
pub byte_start: usize,
pub byte_end: usize,
pub line_start: usize,
pub line_end: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub docstring: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_symbol_id: Option<String>,
#[serde(default)]
pub content_hash: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(default)]
pub created_at: String,
#[serde(default)]
pub updated_at: String,
}
impl Symbol {
pub fn make_id(
project_id: &str,
file_path: &str,
name: &str,
kind: &str,
byte_start: usize,
) -> String {
let key = format!("{project_id}:{file_path}:{name}:{kind}:{byte_start}");
Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
}
pub fn from_row(row: &Row) -> anyhow::Result<Self> {
Ok(Self {
id: row.try_get("id")?,
project_id: row.try_get("project_id")?,
file_path: row.try_get("file_path")?,
name: row.try_get("name")?,
qualified_name: row.try_get("qualified_name")?,
kind: row.try_get("kind")?,
language: row.try_get("language")?,
byte_start: i64_to_usize(row.try_get("byte_start")?, "byte_start")?,
byte_end: i64_to_usize(row.try_get("byte_end")?, "byte_end")?,
line_start: i64_to_usize(row.try_get("line_start")?, "line_start")?,
line_end: i64_to_usize(row.try_get("line_end")?, "line_end")?,
signature: row.try_get("signature")?,
docstring: row.try_get("docstring")?,
parent_symbol_id: row.try_get("parent_symbol_id")?,
content_hash: row
.try_get::<_, Option<String>>("content_hash")?
.unwrap_or_default(),
summary: row.try_get("summary")?,
created_at: row
.try_get::<_, Option<String>>("created_at")?
.unwrap_or_default(),
updated_at: row
.try_get::<_, Option<String>>("updated_at")?
.unwrap_or_default(),
})
}
pub fn to_outline(&self) -> OutlineSymbol {
OutlineSymbol {
id: self.id.clone(),
name: self.name.clone(),
kind: self.kind.clone(),
line_start: self.line_start,
line_end: self.line_end,
signature: self.signature.clone(),
}
}
pub fn to_brief(&self) -> SearchResult {
SearchResult {
id: self.id.clone(),
name: self.name.clone(),
qualified_name: self.qualified_name.clone(),
kind: self.kind.clone(),
language: self.language.clone(),
file_path: self.file_path.clone(),
line_start: self.line_start,
line_end: self.line_end,
score: 0.0,
rrf_score: None,
summary: self.summary.clone(),
signature: self.signature.clone(),
sources: None,
}
}
}
pub fn make_unresolved_callee_id(project_id: &str, callee_name: &str) -> String {
let key = format!("unresolved:{project_id}:{callee_name}");
Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
}
pub fn make_external_symbol_id(
project_id: &str,
callee_name: &str,
module: Option<&str>,
) -> String {
let module_key = module.unwrap_or_default();
let key = format!("external:{project_id}:{module_key}:{callee_name}");
Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedFile {
pub id: String,
pub project_id: String,
pub file_path: String,
pub language: String,
pub content_hash: String,
pub symbol_count: usize,
pub byte_size: usize,
pub indexed_at: String,
}
impl IndexedFile {
pub fn make_id(project_id: &str, file_path: &str) -> String {
let key = format!("{project_id}:{file_path}");
Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentChunk {
pub id: String,
pub project_id: String,
pub file_path: String,
pub chunk_index: usize,
pub line_start: usize,
pub line_end: usize,
pub content: String,
pub language: String,
pub created_at: String,
}
impl ContentChunk {
pub fn make_id(project_id: &str, file_path: &str, chunk_index: usize) -> String {
let key = format!("{project_id}:{file_path}:chunk:{chunk_index}");
Uuid::new_v5(&CODE_INDEX_UUID_NAMESPACE, key.as_bytes()).to_string()
}
}
#[derive(Debug, Clone)]
pub struct ImportRelation {
pub file_path: String,
pub module_name: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallTargetKind {
Symbol,
Unresolved,
External,
LocalImport,
}
impl CallTargetKind {
pub fn as_str(self) -> &'static str {
match self {
Self::Symbol => "symbol",
Self::Unresolved => "unresolved",
Self::External => "external",
Self::LocalImport => "local_import",
}
}
}
#[derive(Debug, Clone)]
pub struct CallRelation {
pub caller_symbol_id: String,
pub callee_symbol_id: Option<String>,
pub callee_name: String,
pub callee_target_kind: CallTargetKind,
pub callee_external_module: Option<String>,
pub file_path: String,
pub line: usize,
}
impl CallRelation {
pub fn new(
caller_symbol_id: String,
callee_name: String,
file_path: String,
line: usize,
) -> Self {
Self {
caller_symbol_id,
callee_symbol_id: None,
callee_name,
callee_target_kind: CallTargetKind::Unresolved,
callee_external_module: None,
file_path,
line,
}
}
pub fn with_symbol_target(mut self, callee_symbol_id: String) -> Self {
self.callee_symbol_id = Some(callee_symbol_id);
self.callee_target_kind = CallTargetKind::Symbol;
self
}
pub fn with_external_target(
mut self,
callee_name: String,
callee_external_module: String,
) -> Self {
self.callee_name = callee_name;
self.callee_target_kind = CallTargetKind::External;
self.callee_external_module = Some(callee_external_module);
self
}
pub fn with_local_import_target(
mut self,
callee_name: String,
candidate_files: Vec<String>,
) -> Self {
self.callee_name = callee_name;
self.callee_target_kind = CallTargetKind::LocalImport;
self.callee_symbol_id = None;
self.callee_external_module = Some(candidate_files.join(LOCAL_IMPORT_CANDIDATE_SEP));
self
}
pub fn with_local_default_import_target(
mut self,
callee_name: String,
candidate_files: Vec<String>,
) -> Self {
self.callee_name = callee_name;
self.callee_target_kind = CallTargetKind::LocalImport;
self.callee_symbol_id = None;
let encoded = std::iter::once(LOCAL_IMPORT_DEFAULT_EXPORT_MARKER.to_string())
.chain(candidate_files)
.collect::<Vec<_>>()
.join(LOCAL_IMPORT_CANDIDATE_SEP);
self.callee_external_module = Some(encoded);
self
}
pub fn local_import_uses_default_export_fallback(&self) -> bool {
self.callee_target_kind == CallTargetKind::LocalImport
&& self
.callee_external_module
.as_deref()
.and_then(|joined| joined.split(LOCAL_IMPORT_CANDIDATE_SEP).next())
== Some(LOCAL_IMPORT_DEFAULT_EXPORT_MARKER)
}
pub fn local_import_candidate_files(&self) -> Vec<String> {
if self.callee_target_kind != CallTargetKind::LocalImport {
return Vec::new();
}
self.callee_external_module
.as_deref()
.map(|joined| {
joined
.split(LOCAL_IMPORT_CANDIDATE_SEP)
.filter(|part| !part.is_empty() && *part != LOCAL_IMPORT_DEFAULT_EXPORT_MARKER)
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default()
}
}
pub const LOCAL_IMPORT_CANDIDATE_SEP: &str = "\n";
const LOCAL_IMPORT_DEFAULT_EXPORT_MARKER: &str = "__gcode_local_import_default_export__";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexedProject {
pub id: String,
pub root_path: String,
pub total_files: usize,
pub total_symbols: usize,
pub last_indexed_at: String,
pub index_duration_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub total_eligible_files: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SearchResult {
pub id: String,
pub name: String,
pub qualified_name: String,
pub kind: String,
pub language: String,
pub file_path: String,
pub line_start: usize,
pub line_end: usize,
pub score: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub rrf_score: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub summary: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sources: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GraphResult {
pub id: String,
pub name: String,
pub file_path: String,
pub line: usize,
#[serde(default)]
pub confidence: ProjectionProvenance,
#[serde(skip_serializing_if = "Option::is_none")]
pub relation: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub distance: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<ProjectionMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphPathStep {
pub position: usize,
pub id: String,
pub name: String,
pub file_path: String,
pub line: usize,
}
pub struct ParseResult {
pub symbols: Vec<Symbol>,
pub imports: Vec<ImportRelation>,
pub calls: Vec<CallRelation>,
pub source: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IndexResult {
pub project_id: String,
pub files_indexed: usize,
pub files_skipped: usize,
pub symbols_found: usize,
pub errors: Vec<String>,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize)]
pub struct PagedResponse<T: Serialize> {
pub project_id: String,
pub total: usize,
pub offset: usize,
pub limit: usize,
pub results: Vec<T>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hint: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct OutlineSymbol {
pub id: String,
pub name: String,
pub kind: String,
pub line_start: usize,
pub line_end: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub signature: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ContentSearchHit {
pub file_path: String,
pub line_start: usize,
pub line_end: usize,
pub snippet: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn symbol_make_id_matches_python_uuid5_golden_vectors() {
assert_eq!(
CODE_INDEX_UUID_NAMESPACE.to_string(),
"c0de1de0-0000-4000-8000-000000000000"
);
let cases = [
(
"proj1",
"src/main.py",
"foo",
"function",
42,
"403e2117-92e7-5390-ad83-226629486481",
),
(
"3bf57fe7-2a0c-4074-8912-a83d9cd4df01",
"crates/gcode/src/models.rs",
"Symbol",
"struct",
111,
"d28e80d3-a95e-5c2a-91c3-92551f75a2b1",
),
(
"proj-with-dashes",
"src/lib.rs",
"Widget::render",
"method",
0,
"44da4f31-7218-5b3b-97c4-5a5eca9f0451",
),
(
"overlay:child",
"nested/path/file.ts",
"HTTPClient.new",
"class",
987654321,
"f9531553-f2a7-5425-b487-6fb5b31d57bb",
),
];
for (project_id, file_path, name, kind, byte_start, expected) in cases {
assert_eq!(
Symbol::make_id(project_id, file_path, name, kind, byte_start),
expected,
"Python UUID5 parity failed for {project_id}:{file_path}:{name}:{kind}:{byte_start}"
);
}
}
#[test]
fn unresolved_and_external_ids_match_python_uuid5_golden_vectors() {
assert_eq!(
make_unresolved_callee_id("proj1", "missing_func"),
"42693df1-99e6-5daa-be29-3535096cd2b5"
);
assert_eq!(
make_external_symbol_id("proj1", "get", Some("requests")),
"7c7e6ebe-47c6-5a3d-a83d-d5160f10cb74"
);
assert_eq!(
make_external_symbol_id("proj1", "println", None),
"c6b97498-448e-5ef1-9cb5-ab1cf37b6596"
);
}
#[test]
fn test_call_relation_promotes_symbol_targets() {
let call = CallRelation::new(
"caller-id".to_string(),
"foo".to_string(),
"src/main.py".to_string(),
12,
)
.with_symbol_target("callee-id".to_string());
assert_eq!(call.callee_symbol_id.as_deref(), Some("callee-id"));
assert_eq!(call.callee_target_kind, CallTargetKind::Symbol);
}
#[test]
fn graph_result_metadata_remains_optional_in_json_contract() {
let json = serde_json::json!({
"id": "sym-1",
"name": "foo",
"file_path": "src/main.rs",
"line": 10
});
let parsed: GraphResult =
serde_json::from_value(json).expect("graph result JSON parses without metadata");
assert_eq!(parsed.confidence, ProjectionProvenance::Extracted);
assert!(parsed.metadata.is_none());
let serialized = serde_json::to_value(&parsed).expect("graph result serializes");
assert_eq!(serialized["confidence"], "EXTRACTED");
assert!(serialized.get("metadata").is_none());
}
#[test]
fn graph_result_without_metadata_omits_metadata_when_serialized() {
let strategy = (
proptest::string::string_regex("[ -~]{0,32}").expect("valid id regex"),
proptest::string::string_regex("[ -~]{0,32}").expect("valid name regex"),
proptest::string::string_regex("[ -~]{0,64}").expect("valid path regex"),
0usize..1_000_000,
proptest::option::of(
proptest::string::string_regex("[ -~]{0,32}").expect("valid relation regex"),
),
proptest::option::of(0usize..1_000),
);
proptest::test_runner::TestRunner::default()
.run(
&strategy,
|(id, name, file_path, line, relation, distance)| {
let result = GraphResult {
id,
name,
file_path,
line,
confidence: ProjectionProvenance::Extracted,
relation,
distance,
metadata: None,
};
let serialized =
serde_json::to_value(&result).expect("graph result serializes");
assert_eq!(serialized["confidence"], "EXTRACTED");
assert_eq!(serialized.get("metadata"), None);
Ok(())
},
)
.expect("metadata omission property holds");
}
}