use std::path::Path;
use crate::services::agent_context::TDG_SCALE;
const UNMARKED: &str = "unmarked (pre-v3.30.0, 0-10 lower-is-better)";
pub(crate) fn stale_scale_reason(found: Option<&str>) -> Option<String> {
let found = found.unwrap_or("");
if found == TDG_SCALE {
return None;
}
let described = if found.is_empty() { UNMARKED } else { found };
Some(format!(
"index was written under TDG scale {described}, this build reads {TDG_SCALE}; rebuild required"
))
}
pub(crate) fn db_scale(conn: &rusqlite::Connection) -> Option<String> {
conn.query_row(
"SELECT value FROM metadata WHERE key = 'tdg_scale'",
[],
|r| r.get::<_, String>(0),
)
.ok()
}
fn manifest_scale(index_path: &Path) -> Option<String> {
let raw = std::fs::read_to_string(index_path.join("manifest.json")).ok()?;
let json: serde_json::Value = serde_json::from_str(&raw).ok()?;
Some(
json.get("tdg_scale")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
)
}
pub(crate) fn verify_index_scale(index_path: &Path) -> Result<(), String> {
let db_candidate = index_path.with_extension("db");
if db_candidate.exists() {
if let Ok(conn) = open_readonly(&db_candidate) {
return match stale_scale_reason(db_scale(&conn).as_deref()) {
Some(reason) => Err(reason),
None => Ok(()),
};
}
}
match manifest_scale(index_path) {
Some(scale) => match stale_scale_reason(Some(&scale)) {
Some(reason) => Err(reason),
None => Ok(()),
},
None => Ok(()),
}
}
pub fn verify_db_scale(db_path: &Path) -> Result<(), String> {
let conn =
open_readonly(db_path).map_err(|e| format!("Failed to open {}: {e}", db_path.display()))?;
match stale_scale_reason(db_scale(&conn).as_deref()) {
Some(reason) => Err(reason),
None => Ok(()),
}
}
fn open_readonly(db_path: &Path) -> Result<rusqlite::Connection, rusqlite::Error> {
rusqlite::Connection::open_with_flags(
db_path,
rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY | rusqlite::OpenFlags::SQLITE_OPEN_NO_MUTEX,
)
}
pub(crate) fn discard_stale_index(index_path: &Path) {
let _ = std::fs::remove_file(index_path.with_extension("db"));
let _ = std::fs::remove_dir_all(index_path);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_marker_is_readable() {
assert_eq!(stale_scale_reason(Some(TDG_SCALE)), None);
}
#[test]
fn absent_marker_is_stale_not_clean() {
for found in [None, Some("")] {
let reason = stale_scale_reason(found)
.unwrap_or_else(|| panic!("{found:?} must be rejected, not accepted as current"));
assert!(reason.contains(UNMARKED), "got: {reason}");
}
}
#[test]
fn foreign_marker_is_named_verbatim() {
let reason = stale_scale_reason(Some("tdg-0-10-lower-is-better"))
.expect("a different marker must be rejected");
assert!(reason.contains("tdg-0-10-lower-is-better"), "got: {reason}");
assert!(reason.contains(TDG_SCALE), "got: {reason}");
}
#[test]
fn verify_db_scale_rejects_db_without_metadata_table() {
let dir = tempfile::tempdir().unwrap();
let db_path = dir.path().join("legacy.db");
let conn = rusqlite::Connection::open(&db_path).unwrap();
conn.execute_batch("CREATE TABLE functions (id INTEGER PRIMARY KEY, tdg_score REAL);")
.unwrap();
drop(conn);
let err =
verify_db_scale(&db_path).expect_err("a database with no scale marker must be refused");
assert!(err.contains("rebuild required"), "got: {err}");
}
#[test]
fn discard_removes_both_artifacts() {
let dir = tempfile::tempdir().unwrap();
let index_path = dir.path().join("context.idx");
std::fs::create_dir_all(&index_path).unwrap();
std::fs::write(index_path.join("manifest.json"), "{}").unwrap();
let db_path = index_path.with_extension("db");
std::fs::write(&db_path, b"stale").unwrap();
discard_stale_index(&index_path);
assert!(!db_path.exists(), "stale .db must be removed");
assert!(!index_path.exists(), "stale index dir must be removed");
}
}