use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, VecDeque}; use uuid::Uuid; use chrono::{DateTime, Utc}; /// Record (L0 evidence). #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Record { pub role: String, pub text: String, pub timestamp: String, pub source_position: u32, } /// Git context enrichment. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitContext { pub file: Option, pub commit_sha: Option, pub author: Option, } /// Ingest request with full payload. #[derive(Debug, Deserialize, Clone)] pub struct IngestRequest { pub project: String, pub source: String, pub ingest_id: String, #[serde(default)] pub records: Vec, #[serde(default)] pub git_repo_path: Option, #[serde(default)] pub git_head: Option, } /// Job status. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct JobStatus { pub job_id: String, pub ingest_id: String, pub project: String, pub status: String, pub chunks_seen: u32, pub chunks_used: u32, pub error: Option, pub created_at: DateTime, pub completed_at: Option>, } /// In-memory ingest queue — per-project FIFO + global dedup. pub struct IngestQueue { /// All jobs (for lookup by job_id or ingest_id) jobs: BTreeMap, /// Per-project queues (ingest_id order) project_queues: BTreeMap>, } impl IngestQueue { /// Create new queue. pub fn new() -> Self { Self { jobs: BTreeMap::new(), project_queues: BTreeMap::new(), } } /// Submit job (idempotent by ingest_id). pub fn submit(&mut self, project: &str, ingest_id: &str) -> (String, bool) { if let Some(existing) = self.jobs.get(ingest_id) { return (existing.job_id.clone(), false); } let job_id = format!("ingest-{}", Uuid::new_v4()); let status = JobStatus { job_id: job_id.clone(), ingest_id: ingest_id.to_string(), project: project.to_string(), status: "running".to_string(), chunks_seen: 0, chunks_used: 0, error: None, created_at: Utc::now(), completed_at: None, }; // Insert into job map self.jobs.insert(ingest_id.to_string(), status); // Enqueue to project-specific queue self.project_queues .entry(project.to_string()) .or_insert_with(VecDeque::new) .push_back(ingest_id.to_string()); (job_id, true) } /// Get job status by job_id. pub fn get_status(&self, job_id: &str) -> Option { self.jobs.values().find(|j| j.job_id == job_id).cloned() } /// Update job status (used by background task during async processing). pub fn update_status( &mut self, ingest_id: &str, status: &str, chunks_seen: u32, chunks_used: u32, error: Option, ) { if let Some(job) = self.jobs.get_mut(ingest_id) { job.status = status.to_string(); job.chunks_seen = chunks_seen; job.chunks_used = chunks_used; job.error = error; if status == "completed" || status == "failed" { job.completed_at = Some(Utc::now()); } } } /// Dequeue next job for a project (FIFO). pub fn dequeue(&mut self, project: &str) -> Option { self.project_queues .get_mut(project) .and_then(|q| q.pop_front()) } /// Get queue depth for a project. pub fn queue_depth(&self, project: &str) -> usize { self.project_queues .get(project) .map(|q| q.len()) .unwrap_or(0) } }