KeiSeiKit-1.0/_primitives/_rust/kei-projects-index/src/index.rs
Parfii-bot 0be354a920 KeiSeiKit-public — clean state
Single-commit clean baseline after security scrub of niche-tells,
project codenames, internal jargon, and contributor-email leaks.

Contents:
- 100 Rust crates (_primitives/_rust/)
- 37 agent manifests (_manifests/) + generated specs (_generated/)
- 67 user-invocable skills (skills/)
- 33 hooks (hooks/)
- Composition blocks (_blocks/)
- Documentation (docs/, README.md)
- TS adapter packages (_ts_packages/)
- Assembler (_assembler/)
- Roles (_roles/)
- Templates (_templates/)
- Forgejo CI (.forgejo/)

Author: Denis Parfionovich <info@greendragon.info>

License: see LICENSE.
2026-05-01 12:09:03 +08:00

134 lines
5.2 KiB
Rust

//! Index orchestrator: walk → git_state → docs → sqlite_scan → upsert.
//!
//! Constructor Pattern: one cube = the "rebuild" pipeline. Pure glue —
//! all data extraction lives in sibling modules. Idempotent: rebuilding
//! against the same filesystem yields the same DB state (rows are
//! upserted by primary key `path`).
use crate::docs::{detect_docs, DocsState};
use crate::git_state::{detect_git_state, GitState};
use crate::schema::init;
use crate::sqlite_scan::count_sqlite_files;
use crate::walk::{walk_projects_root, ProjectEntry};
use anyhow::{Context, Result};
use chrono::Utc;
use rusqlite::{params, Connection};
use std::path::Path;
// Re-export `ProjectRow` at the orchestrator path so existing callers
// (`kei_projects_index::index::ProjectRow`) keep working alongside the
// canonical `kei_projects_index::row::ProjectRow` location.
pub use crate::row::ProjectRow;
/// Build a `ProjectRow` from the four data sources for one project.
fn build_row(entry: &ProjectEntry, now_ts: i64) -> ProjectRow {
let git: Option<GitState> = if entry.has_git {
detect_git_state(&entry.path)
} else {
None
};
let docs: DocsState = detect_docs(&entry.path);
let sqlite_count = count_sqlite_files(&entry.path) as i64;
ProjectRow {
path: entry.path.to_string_lossy().to_string(),
name: entry.name.clone(),
has_git: entry.has_git,
branch: git.as_ref().and_then(|g| g.branch.clone()),
dirty: git.as_ref().map(|g| g.dirty).unwrap_or(false),
ahead: git.as_ref().map(|g| g.ahead as i64).unwrap_or(0),
behind: git.as_ref().map(|g| g.behind as i64).unwrap_or(0),
last_commit_sha: git.as_ref().and_then(|g| g.last_commit_sha.clone()),
last_commit_msg: git.as_ref().and_then(|g| g.last_commit_msg.clone()),
last_commit_ts: git.as_ref().and_then(|g| g.last_commit_ts),
has_claude_md: docs.has_claude_md,
has_decisions_md: docs.has_decisions_md,
has_runbook_md: docs.has_runbook_md,
has_readme: docs.has_readme,
sqlite_count,
last_indexed_ts: now_ts,
}
}
const UPSERT_SQL: &str = "INSERT OR REPLACE INTO projects
(path, name, has_git, branch, dirty, ahead, behind,
last_commit_sha, last_commit_msg, last_commit_ts,
has_claude_md, has_decisions_md, has_runbook_md, has_readme,
sqlite_count, last_indexed_ts)
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10,
?11, ?12, ?13, ?14, ?15, ?16)";
/// Upsert one row keyed on PRIMARY KEY (`path`).
fn upsert_row(conn: &Connection, row: &ProjectRow) -> Result<()> {
conn.execute(
UPSERT_SQL,
params![
row.path, row.name, row.has_git as i64, row.branch,
row.dirty as i64, row.ahead, row.behind,
row.last_commit_sha, row.last_commit_msg, row.last_commit_ts,
row.has_claude_md as i64, row.has_decisions_md as i64,
row.has_runbook_md as i64, row.has_readme as i64,
row.sqlite_count, row.last_indexed_ts,
],
)
.context("upsert into projects")?;
Ok(())
}
/// Open `db_path` (creating parent dir) and apply the schema.
fn open_db(db_path: &Path) -> Result<Connection> {
if let Some(parent) = db_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let conn = Connection::open(db_path).context("open projects-index sqlite")?;
init(&conn).context("apply schema")?;
Ok(conn)
}
/// Rebuild the index from `projects_root` into `db_path`.
///
/// 1. Open / create DB and apply schema.
/// 2. Walk top-level dirs of `projects_root`.
/// 3. Extract git_state + docs + sqlite_count for each.
/// 4. Upsert as one `projects` row.
///
/// Returns the number of rows touched. Idempotent — running twice
/// against the same filesystem yields the same DB state.
pub fn rebuild_index(db_path: &Path, projects_root: &Path) -> Result<usize> {
let conn = open_db(db_path)?;
rebuild_index_with_conn(&conn, projects_root)
}
/// Same as `rebuild_index` but uses a caller-supplied connection. Used by
/// `kei-projects-watcher` to reuse one Connection across many reindex
/// calls without reopening the DB on each event.
pub fn rebuild_index_with_conn(conn: &Connection, projects_root: &Path) -> Result<usize> {
let entries = walk_projects_root(projects_root).context("walk projects root")?;
let now_ts = Utc::now().timestamp();
let mut count = 0usize;
for entry in &entries {
let row = build_row(entry, now_ts);
upsert_row(conn, &row)?;
count += 1;
}
Ok(count)
}
/// Re-index a single project (one row). Used by the fsevents watcher
/// after a debounced file change in `<projects_root>/<project>/`.
/// `project_path` must be the immediate child of the projects root, i.e.
/// the project's own top-level directory.
pub fn reindex_one(conn: &Connection, project_path: &Path) -> Result<()> {
let name = project_path
.file_name()
.and_then(|n| n.to_str())
.map(|s| s.to_string())
.unwrap_or_default();
let entry = ProjectEntry {
path: project_path.to_path_buf(),
name,
has_git: project_path.join(".git").exists(),
};
let now_ts = Utc::now().timestamp();
let row = build_row(&entry, now_ts);
upsert_row(conn, &row)
}