use crate::config::{CODE_SYMBOL_COLLECTION_PREFIX, Context};
use crate::db;
use crate::output::{self, Format};
use crate::projection::sync::{ProjectionStatus, ProjectionSyncReport};
use crate::vector::code_symbols::{
self, CodeSymbolVectorLifecycle, CodeSymbolVectorLifecycleAction,
CodeSymbolVectorLifecycleOutput, CodeSymbolVectorLifecycleStatus, VectorLifecycleError,
};
use serde::Serialize;
pub fn lifecycle_status(
ctx: &Context,
action: CodeSymbolVectorLifecycleAction,
) -> CodeSymbolVectorLifecycleStatus {
let prefix = CODE_SYMBOL_COLLECTION_PREFIX;
code_symbols::lifecycle_status(ctx.project_id.clone(), prefix, action)
}
pub(crate) fn lifecycle_from_context(
ctx: &Context,
) -> Result<CodeSymbolVectorLifecycle, VectorLifecycleError> {
let qdrant = ctx
.qdrant
.clone()
.ok_or(VectorLifecycleError::MissingQdrantConfig)?;
let embedding = ctx
.embedding
.clone()
.ok_or(VectorLifecycleError::MissingEmbeddingConfig)?;
CodeSymbolVectorLifecycle::new(
ctx.project_id.clone(),
qdrant,
embedding,
ctx.code_vectors.clone(),
)
}
pub fn sync_file(ctx: &Context, file_path: &str, format: Format) -> anyhow::Result<()> {
let mut lifecycle = lifecycle_from_context(ctx)?;
let mut conn = db::connect_readwrite(&ctx.database_url)?;
if !db::indexed_file_exists(&mut conn, &ctx.project_id, file_path)? {
anyhow::bail!(
"indexed file `{file_path}` was not found for project {}",
ctx.project_id
);
}
let symbols = code_symbols::fetch_symbols_for_file(&mut conn, &ctx.project_id, file_path)?;
let output = lifecycle.sync_file_symbols(file_path, &symbols)?;
if !db::mark_vectors_synced(&mut conn, &ctx.project_id, file_path)? {
anyhow::bail!(
"indexed file `{file_path}` was not found for project {}",
ctx.project_id
);
}
let report = ProjectionSyncReport::ok(1, output.symbols);
print_lifecycle_output(&output, report, format)
}
pub fn clear(ctx: &Context, format: Format) -> anyhow::Result<()> {
let mut lifecycle = lifecycle_from_context(ctx)?;
let mut conn = db::connect_readwrite(&ctx.database_url)?;
db::reset_vectors_sync_for_project(&mut conn, &ctx.project_id)?;
let output = lifecycle.clear_project_vectors()?;
let report = ProjectionSyncReport::ok(0, 0);
print_lifecycle_output(&output, report, format)
}
pub fn rebuild(ctx: &Context, format: Format) -> anyhow::Result<()> {
let mut lifecycle = lifecycle_from_context(ctx)?;
let mut conn = db::connect_readwrite(&ctx.database_url)?;
let file_paths = db::list_indexed_file_paths(&mut conn, &ctx.project_id)?;
db::reset_vectors_sync_for_project(&mut conn, &ctx.project_id)?;
let symbols = code_symbols::fetch_symbols_for_project(&mut conn, &ctx.project_id)?;
let output = lifecycle.rebuild_symbols(&symbols)?;
let files_synced = db::mark_project_vectors_synced(&mut conn, &ctx.project_id)? as usize;
let report = ProjectionSyncReport::ok(files_synced.min(file_paths.len()), output.symbols);
print_lifecycle_output(&output, report, format)
}
fn print_lifecycle_output(
output: &CodeSymbolVectorLifecycleOutput,
report: ProjectionSyncReport,
format: Format,
) -> anyhow::Result<()> {
let payload = lifecycle_json_payload(output, report);
match format {
Format::Json => output::print_json(&payload),
Format::Text => output::print_text(&serde_json::to_string(&payload)?),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct VectorLifecycleJsonPayload {
pub project_id: String,
pub projection: &'static str,
pub action: CodeSymbolVectorLifecycleAction,
pub file_path: Option<String>,
pub collection: String,
pub status: ProjectionStatus,
pub synced_files: usize,
pub synced_symbols: usize,
pub degraded: bool,
pub error: Option<crate::projection::sync::ProjectionSyncError>,
pub symbols: usize,
pub vectors_upserted: usize,
pub vectors_deleted: usize,
pub summary: String,
}
pub(crate) fn lifecycle_json_payload(
output: &CodeSymbolVectorLifecycleOutput,
report: ProjectionSyncReport,
) -> VectorLifecycleJsonPayload {
VectorLifecycleJsonPayload {
project_id: output.project_id.clone(),
projection: "vector",
action: output.action,
file_path: output.file_path.clone(),
collection: output.collection.clone(),
status: report.status,
synced_files: report.synced_files,
synced_symbols: report.synced_symbols,
degraded: report.degraded,
error: report.error,
symbols: output.symbols,
vectors_upserted: output.vectors_upserted,
vectors_deleted: output.vectors_deleted,
summary: output.summary.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::projection::sync::{ProjectionStatus, ProjectionSyncError, ProjectionSyncReport};
use serde_json::json;
use std::path::PathBuf;
fn make_ctx() -> Context {
Context {
database_url: "postgresql://localhost/nonexistent".to_string(),
project_root: PathBuf::from("/nonexistent"),
project_id: "project-1".to_string(),
quiet: true,
falkordb: None,
qdrant: None,
embedding: None,
code_vectors: crate::config::CodeVectorSettings { vector_dim: None },
daemon_url: None,
}
}
#[test]
fn vector_lifecycle_requires_config() {
let err = lifecycle_from_context(&make_ctx()).expect_err("missing config must fail");
assert!(matches!(
err,
code_symbols::VectorLifecycleError::MissingQdrantConfig
));
let ctx = Context {
qdrant: Some(crate::config::QdrantConfig {
url: Some("http://localhost:6333".to_string()),
api_key: None,
}),
..make_ctx()
};
let err = lifecycle_from_context(&ctx).expect_err("missing embedding must fail");
assert!(matches!(
err,
code_symbols::VectorLifecycleError::MissingEmbeddingConfig
));
}
#[test]
fn lifecycle_json_contract() {
let output = CodeSymbolVectorLifecycleOutput {
project_id: "project-1".to_string(),
collection: "gcode_code_symbols_project-1".to_string(),
action: CodeSymbolVectorLifecycleAction::SyncFile,
file_path: Some("src/lib.rs".to_string()),
symbols: 2,
vectors_upserted: 2,
vectors_deleted: 1,
summary: "2 vector(s) upserted, 1 delete operation(s) issued".to_string(),
};
let payload = lifecycle_json_payload(
&output,
ProjectionSyncReport {
status: ProjectionStatus::Ok,
synced_files: 1,
synced_symbols: 2,
degraded: false,
error: None,
},
);
assert_eq!(
serde_json::to_value(&payload).expect("payload serializes"),
json!({
"project_id": "project-1",
"projection": "vector",
"action": "sync_file",
"file_path": "src/lib.rs",
"collection": "gcode_code_symbols_project-1",
"status": "ok",
"synced_files": 1,
"synced_symbols": 2,
"degraded": false,
"error": null,
"symbols": 2,
"vectors_upserted": 2,
"vectors_deleted": 1,
"summary": "2 vector(s) upserted, 1 delete operation(s) issued"
})
);
let degraded = lifecycle_json_payload(
&output,
ProjectionSyncReport {
status: ProjectionStatus::Degraded,
synced_files: 0,
synced_symbols: 0,
degraded: true,
error: Some(ProjectionSyncError {
kind: "missing_qdrant_config".to_string(),
message: "Qdrant config is required".to_string(),
}),
},
);
let degraded = serde_json::to_value(°raded).expect("payload serializes");
assert_eq!(degraded["status"], "degraded");
assert_eq!(degraded["error"]["kind"], "missing_qdrant_config");
}
}