use std::sync::Arc;
use crate::{
api::ApiError,
domain::{
CodeIndexCheckpoint, CodeIndexMode, CodeIndexSession, CodeIndexSummary,
code_incremental_clone, code_query_index_repair, code_reference_resolution,
code_reference_resolution_query_index_repair, code_reference_search_query_index_repair,
code_reference_search_rebuild,
},
storage::KnowledgeStore,
};
use super::task::{CodeIndexTaskLeaseContext, finalize_code_index_session_with_task_lease};
pub(super) enum IncrementalSnapshotApply {
Complete(Box<CodeIndexSummary>),
DurablePending {
completed_steps: usize,
max_steps: usize,
},
FinalizationRequired {
checkpoint_state: String,
},
FullFallback,
}
pub(super) fn checkpoint_skips_parser(state: &str) -> bool {
matches!(
state,
"finalizing:software_projection" | "finalizing:partitioned_publish" | "completed"
) || code_query_index_repair(state).is_some()
|| code_reference_resolution(state).is_some()
|| code_reference_resolution_query_index_repair(state).is_some()
|| code_reference_search_query_index_repair(state).is_some()
|| code_reference_search_rebuild(state).is_some()
}
pub(super) fn should_resume_staged_full(
mode: &CodeIndexMode,
has_task_lease: bool,
checkpoint_state: Option<&str>,
) -> bool {
matches!(mode, CodeIndexMode::Incremental { .. })
&& has_task_lease
&& checkpoint_state.is_some_and(|state| code_incremental_clone(state).is_none())
}
pub(super) async fn resume_finalization(
store: &Arc<dyn KnowledgeStore>,
lease: Option<&CodeIndexTaskLeaseContext>,
checkpoint: Option<&CodeIndexCheckpoint>,
) -> Result<Option<CodeIndexSummary>, ApiError> {
let Some((lease, checkpoint, receipt)) =
lease.zip(checkpoint).and_then(|(lease, checkpoint)| {
checkpoint
.incremental_summary
.as_ref()
.map(|receipt| (lease, checkpoint, receipt))
})
else {
return Ok(None);
};
let content_identity_matches = checkpoint.repository_id
== lease.publication_fence.repository_id
&& checkpoint.source_scope == lease.source_scope
&& checkpoint.tree_hash == lease.tree_hash
&& checkpoint.path_filters == lease.path_filters
&& checkpoint.language_filters == lease.language_filters;
if !content_identity_matches {
return Err(ApiError::internal(format!(
"durable incremental finalization receipt for scope '{}' does not match its live task",
checkpoint.source_scope
)));
}
let terminal = matches!(
checkpoint.state.as_str(),
"completed" | "finalizing:partitioned_publish"
);
if receipt.task_id != lease.task_id {
if terminal {
return Ok(None);
}
return Err(ApiError::internal(format!(
"durable incremental finalization receipt for scope '{}' does not match its live task",
checkpoint.source_scope
)));
}
if checkpoint.resolved_commit_sha != lease.resolved_commit_sha
|| checkpoint.resource_budget != lease.resource_budget
{
return Err(ApiError::internal(format!(
"durable incremental finalization receipt for scope '{}' does not match its live task",
checkpoint.source_scope
)));
}
if !(checkpoint.state == "indexing"
|| checkpoint.state.starts_with("finalizing:")
|| checkpoint.state == "completed")
{
return Err(ApiError::internal(format!(
"durable incremental finalization receipt for scope '{}' does not match its live task",
checkpoint.source_scope
)));
}
let session = CodeIndexSession {
repository_id: checkpoint.repository_id.clone(),
source_scope: checkpoint.source_scope.clone(),
base_resolved_commit_sha: Some(receipt.base_resolved_commit_sha.clone()),
resolved_commit_sha: checkpoint.resolved_commit_sha.clone(),
tree_hash: checkpoint.tree_hash.clone(),
path_filters: checkpoint.path_filters.clone(),
language_filters: checkpoint.language_filters.clone(),
full_replace: true,
total_path_count: checkpoint.total_path_count,
changed_path_count: receipt.changed_path_count,
skipped_unchanged_count: receipt.skipped_unchanged_count,
deleted_paths: Vec::new(),
changed_paths: Vec::new(),
tombstones: Vec::new(),
workspaces: Vec::new(),
resource_budget: checkpoint.resource_budget,
};
finalize_code_index_session_with_task_lease(store, lease, session)
.await
.map(Some)
}