use rusqlite::Connection;
use crate::index::AnchorHealth;
use crate::query::tree::DirTree;
const MEMORY_TITLES_SHOWN: usize = 3;
#[derive(Debug)]
pub struct Orientation {
pub tree: DirTree,
pub load_bearing: Vec<(String, u64)>,
pub recent_commits: Vec<String>,
pub hot_files: Vec<String>,
pub active_memory_titles: Vec<String>,
pub active_memory_total: u32,
pub head: String,
pub indexed_head: String,
pub anchor: AnchorHealth,
pub total_files: u32,
pub parser_failures: u64,
}
pub fn orientation(conn: &Connection, root: &std::path::Path) -> anyhow::Result<Orientation> {
let (commit_sha, worktree_id) = crate::index::resolve_git_context(root);
crate::index::install_scope_view(conn, &commit_sha, &worktree_id)?;
let tree = crate::query::tree::dir_tree(conn, &Default::default())?;
let load_bearing = spine_load_bearing(conn, 5)?;
let recent_commits = recent_commit_subjects(conn, 5)?;
let hot_files = recently_changed_source_files(conn, 3)?;
let active_memory_titles = active_non_dir_memory_titles(conn, MEMORY_TITLES_SHOWN)?;
let active_memory_total = active_non_dir_memory_count(conn)?;
let git_status = crate::index::git_history::status(conn, root)?;
let head = git_status.head.unwrap_or_default();
let indexed_head = git_status.indexed_head.unwrap_or_default();
let anchor = crate::query::memory::anchor_health_counts(conn)?;
let total_files = scoped_file_count(conn)?;
let parser_failures = crate::query::impact::parser_failure_count(conn)?;
Ok(Orientation {
tree,
load_bearing,
recent_commits,
hot_files,
active_memory_titles,
active_memory_total,
head,
indexed_head,
anchor,
total_files,
parser_failures,
})
}
fn spine_load_bearing(conn: &Connection, limit: usize) -> anyhow::Result<Vec<(String, u64)>> {
let mut stmt = conn.prepare(
"-- Top files by incoming graph edges (fan_in) within the active scope.
-- Invariant: files resolves to the scoped TEMP VIEW; only non-generated source files.
SELECT files.path,
COUNT(*) AS fan_in
FROM edges
JOIN symbols AS target_symbols ON target_symbols.id = edges.to_symbol_id
JOIN files ON files.id = target_symbols.file_id
WHERE files.generated = 0
GROUP BY files.path
ORDER BY fan_in DESC, files.path ASC
LIMIT ?1",
)?;
let rows = stmt
.query_map([limit as i64], |row| Ok((row.get::<_, String>(0)?, row.get::<_, i64>(1)?)))?;
let mut out = Vec::new();
for row in rows {
let (path, fan_in) = row?;
out.push((path, u64::try_from(fan_in.max(0)).unwrap_or(0)));
}
Ok(out)
}
fn recent_commit_subjects(conn: &Connection, limit: usize) -> anyhow::Result<Vec<String>> {
let mut stmt = conn.prepare(
"-- Most recent indexed commit subjects, newest first.
-- Invariant: git_commits rows are global (not scoped); subjects only.
SELECT subject
FROM git_commits
ORDER BY authored_at_s DESC, committed_at_s DESC
LIMIT ?1",
)?;
let rows = stmt.query_map([limit as i64], |row| row.get::<_, String>(0))?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
}
fn recently_changed_source_files(conn: &Connection, limit: usize) -> anyhow::Result<Vec<String>> {
let mut stmt = conn.prepare(
"-- Most recently changed source paths that are currently indexed in the active scope.
-- Invariant: files resolves to the scoped TEMP VIEW; git_file_changes is global.
SELECT DISTINCT gfc.path
FROM git_file_changes AS gfc
JOIN git_commits AS gc ON gc.hash = gfc.commit_hash
JOIN files ON files.path = gfc.path
WHERE files.generated = 0
ORDER BY gc.authored_at_s DESC, gfc.path ASC
LIMIT ?1",
)?;
let rows = stmt.query_map([limit as i64], |row| row.get::<_, String>(0))?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
}
fn active_non_dir_memory_titles(conn: &Connection, limit: usize) -> anyhow::Result<Vec<String>> {
let mut stmt = conn.prepare(
"-- Active memories not bound to a directory, newest-updated first.
-- Invariant: excludes binding_kind='dir' rows so tree-shown titles are not repeated.
SELECT m.title
FROM repo_memories AS m
WHERE m.status = 'active'
AND m.id NOT IN (
SELECT b.memory_id
FROM repo_memory_bindings AS b
WHERE b.binding_kind = 'dir'
)
ORDER BY m.updated_at_ms DESC
LIMIT ?1",
)?;
let rows = stmt.query_map([limit as i64], |row| row.get::<_, String>(0))?;
let mut out = Vec::new();
for row in rows {
out.push(row?);
}
Ok(out)
}
fn active_non_dir_memory_count(conn: &Connection) -> anyhow::Result<u32> {
let count: i64 = conn.query_row(
"-- Total active memories not bound to a directory (mirrors the titles query).
-- Invariant: excludes binding_kind='dir' rows so the count matches what the
-- titles list draws from; only the LIMIT/ORDER differ.
SELECT COUNT(*)
FROM repo_memories AS m
WHERE m.status = 'active'
AND m.id NOT IN (
SELECT b.memory_id
FROM repo_memory_bindings AS b
WHERE b.binding_kind = 'dir'
)",
[],
|row| row.get(0),
)?;
Ok(u32::try_from(count.max(0)).unwrap_or(u32::MAX))
}
fn scoped_file_count(conn: &Connection) -> anyhow::Result<u32> {
let count: i64 =
conn.query_row("SELECT COUNT(*) FROM files WHERE generated = 0", [], |row| row.get(0))?;
Ok(u32::try_from(count.max(0)).unwrap_or(u32::MAX))
}