use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::process::Command;
use anyhow::{anyhow, Context, Result};
use chrono::Utc;
use serde::{Deserialize, Serialize};
use serde_json::json;
use uuid::Uuid;
use khive_runtime::{secret_gate, KhiveRuntime, NamespaceToken, VerbRegistry};
use khive_storage::types::{SqlStatement, SqlValue};
use crate::hook;
use crate::refs;
#[derive(Debug, Clone, Copy)]
pub struct IngestInclude {
pub commits: bool,
pub issues: bool,
pub pull_requests: bool,
}
impl Default for IngestInclude {
fn default() -> Self {
Self {
commits: true,
issues: true,
pull_requests: true,
}
}
}
#[derive(Debug, Clone)]
pub struct IngestOptions {
pub repo: PathBuf,
pub project: String,
pub max_items: Option<u64>,
pub include: IngestInclude,
}
impl IngestOptions {
pub fn unbounded(repo: PathBuf, project: String) -> Self {
Self {
repo,
project,
max_items: None,
include: IngestInclude::default(),
}
}
}
struct Budget {
remaining: Option<u64>,
}
impl Budget {
fn try_consume(&mut self) -> bool {
match &mut self.remaining {
None => true,
Some(0) => false,
Some(n) => {
*n -= 1;
true
}
}
}
fn exhausted(&self) -> bool {
matches!(self.remaining, Some(0))
}
}
struct NewRecordForRef {
id: Uuid,
text: String,
}
#[derive(Debug, Default, Serialize)]
pub struct IngestReport {
pub commits_ingested: u64,
pub commits_skipped_existing: u64,
pub issues_ingested: u64,
pub issues_skipped_existing: u64,
pub prs_ingested: u64,
pub prs_skipped_existing: u64,
pub gh_available: bool,
pub warnings: Vec<String>,
pub done: bool,
pub project_id: Option<String>,
pub project_created: bool,
pub reference_edges_created: u64,
pub reference_edges_unresolved: u64,
pub parent_edges_created: u64,
pub commit_embeddings_truncated: u64,
}
pub async fn run_ingest(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
opts: IngestOptions,
) -> Result<IngestReport> {
run_ingest_with_commit_recovery(runtime, token, registry, opts, |_repo, _err| Ok(None)).await
}
pub(crate) async fn run_ingest_with_commit_recovery(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
opts: IngestOptions,
mut recover: impl FnMut(&Path, &GitLogError) -> Result<Option<RecoveredRepo>> + Send,
) -> Result<IngestReport> {
let mut report = IngestReport {
done: true,
..IngestReport::default()
};
let project_id = resolve_id(runtime, token, &opts.project)
.await?
.ok_or_else(|| anyhow!("--project {:?} did not resolve to an entity", opts.project))?;
report.project_id = Some(project_id.to_string());
let mut merge_sha_to_pr: HashMap<String, Uuid> = HashMap::new();
let mut number_to_pr: HashMap<u64, Uuid> = HashMap::new();
let mut budget = Budget {
remaining: opts.max_items,
};
let mut new_records: Vec<NewRecordForRef> = Vec::new();
if opts.include.issues || opts.include.pull_requests {
if gh_available(&opts.repo) {
report.gh_available = true;
if opts.include.pull_requests && !budget.exhausted() {
match ingest_prs(
runtime,
token,
registry,
&opts.repo,
project_id,
&mut report,
&mut merge_sha_to_pr,
&mut number_to_pr,
&mut budget,
&mut new_records,
)
.await
{
Ok(()) => {}
Err(e) => report
.warnings
.push(format!("gh pr list failed, skipping pull requests: {e}")),
}
}
if opts.include.issues && !budget.exhausted() {
if let Err(e) = ingest_issues(
runtime,
token,
registry,
&opts.repo,
project_id,
&mut report,
&mut budget,
&mut new_records,
)
.await
{
report
.warnings
.push(format!("gh issue list failed, skipping issues: {e}"));
}
}
} else {
report.gh_available = false;
report.warnings.push(
"gh CLI not found on PATH; skipped issues and pull requests — commits still ingest"
.to_string(),
);
}
}
if opts.include.commits && !budget.exhausted() {
ingest_commits(
runtime,
token,
registry,
&opts.repo,
project_id,
&merge_sha_to_pr,
&number_to_pr,
&mut report,
&mut budget,
&mut new_records,
&mut recover,
)
.await?;
}
if budget.exhausted() {
report.done = false;
}
link_references(
runtime,
token,
registry,
project_id,
&new_records,
&mut report,
)
.await;
Ok(report)
}
async fn resolve_id(
runtime: &KhiveRuntime,
_token: &NamespaceToken,
raw: &str,
) -> Result<Option<Uuid>> {
if let Ok(u) = Uuid::parse_str(raw) {
return Ok(Some(u));
}
runtime
.resolve_prefix_unfiltered(raw)
.await
.map_err(|e| anyhow!("{e}"))
}
pub async fn resolve_project_id(runtime: &KhiveRuntime, raw: &str) -> Result<Option<Uuid>> {
if let Ok(u) = Uuid::parse_str(raw) {
return Ok(Some(u));
}
runtime
.resolve_prefix_unfiltered(raw)
.await
.map_err(|e| anyhow!("{e}"))
}
async fn find_issue_or_pr_by_number(
runtime: &KhiveRuntime,
token: &NamespaceToken,
project_id: Uuid,
number: u64,
) -> Result<Option<Uuid>> {
if let Some(id) = find_by_number(runtime, token, "issue", project_id, number).await? {
return Ok(Some(id));
}
find_by_number(runtime, token, "pull_request", project_id, number).await
}
async fn link_references(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
project_id: Uuid,
new_records: &[NewRecordForRef],
report: &mut IngestReport,
) {
for record in new_records {
let mentions = refs::dedupe_prefer_closes(refs::extract_references(&record.text));
for mention in mentions {
let target = match find_issue_or_pr_by_number(
runtime,
token,
project_id,
mention.number,
)
.await
{
Ok(Some(id)) => id,
Ok(None) => {
report.reference_edges_unresolved += 1;
continue;
}
Err(e) => {
report
.warnings
.push(format!("resolving reference #{}: {e}", mention.number));
continue;
}
};
if target == record.id {
continue;
}
match registry
.dispatch(
"link",
json!({
"source_id": record.id.to_string(),
"target_id": target.to_string(),
"relation": "annotates",
"metadata": { "ref_kind": mention.kind.as_str() },
}),
)
.await
{
Ok(_) => report.reference_edges_created += 1,
Err(e) => report.warnings.push(format!(
"linking reference #{} from {}: {e}",
mention.number, record.id
)),
}
}
}
}
fn gh_available(repo: &Path) -> bool {
Command::new("gh")
.arg("--version")
.current_dir(repo)
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
async fn find_commit_by_sha(
runtime: &KhiveRuntime,
token: &NamespaceToken,
sha: &str,
) -> Result<Option<Uuid>> {
let sql = runtime.sql();
let mut r = sql.reader().await.map_err(|e| anyhow!("{e}"))?;
let row = r
.query_row(SqlStatement {
sql: "SELECT id FROM notes WHERE kind='commit' AND namespace=?1 \
AND deleted_at IS NULL AND json_extract(properties,'$.sha')=?2 LIMIT 1"
.into(),
params: vec![
SqlValue::Text(token.namespace().as_str().to_string()),
SqlValue::Text(sha.to_string()),
],
label: Some("git_ingest_find_commit_by_sha".into()),
})
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(row.and_then(|r| row_uuid(&r)))
}
async fn find_by_number(
runtime: &KhiveRuntime,
token: &NamespaceToken,
kind: &str,
project_id: Uuid,
number: u64,
) -> Result<Option<Uuid>> {
let sql = runtime.sql();
let mut r = sql.reader().await.map_err(|e| anyhow!("{e}"))?;
let row = r
.query_row(SqlStatement {
sql: "SELECT id FROM notes WHERE kind=?1 AND namespace=?2 \
AND deleted_at IS NULL AND json_extract(properties,'$.number')=?3 \
AND json_extract(properties,'$.project_id')=?4 LIMIT 1"
.into(),
params: vec![
SqlValue::Text(kind.to_string()),
SqlValue::Text(token.namespace().as_str().to_string()),
SqlValue::Integer(number as i64),
SqlValue::Text(project_id.to_string()),
],
label: Some("git_ingest_find_by_number".into()),
})
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(row.and_then(|r| row_uuid(&r)))
}
fn row_uuid(row: &khive_storage::types::SqlRow) -> Option<Uuid> {
match row.get("id") {
Some(SqlValue::Uuid(u)) => Some(*u),
Some(SqlValue::Text(s)) => Uuid::parse_str(s).ok(),
_ => None,
}
}
fn escape_like(input: &str) -> String {
let mut out = String::with_capacity(input.len());
for c in input.chars() {
if matches!(c, '\\' | '%' | '_') {
out.push('\\');
}
out.push(c);
}
out
}
async fn find_document_for_path(
runtime: &KhiveRuntime,
token: &NamespaceToken,
path: &str,
) -> Result<Option<Uuid>> {
let file_name = Path::new(path)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(path);
let sql = runtime.sql();
let namespace = token.namespace().as_str().to_string();
let like_pattern = format!("%{}", escape_like(path));
let mut r = sql.reader().await.map_err(|e| anyhow!("{e}"))?;
let row = r
.query_row(SqlStatement {
sql: "SELECT id FROM entities WHERE kind='document' AND namespace=?1 \
AND deleted_at IS NULL \
AND (json_extract(properties,'$.source_uri')=?2 OR name=?3 \
OR json_extract(properties,'$.source_uri') LIKE ?4 ESCAPE '\\') \
ORDER BY CASE WHEN json_extract(properties,'$.source_uri')=?2 OR name=?3 \
THEN 0 ELSE 1 END, id \
LIMIT 1"
.into(),
params: vec![
SqlValue::Text(namespace),
SqlValue::Text(path.to_string()),
SqlValue::Text(file_name.to_string()),
SqlValue::Text(like_pattern),
],
label: Some("git_ingest_find_document_for_path".into()),
})
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(row.and_then(|r| row_uuid(&r)))
}
async fn read_cursor(
runtime: &KhiveRuntime,
project_id: Uuid,
kind: &str,
) -> Result<Option<String>> {
let sql = runtime.sql();
let mut r = sql.reader().await.map_err(|e| anyhow!("{e}"))?;
let row = r
.query_row(SqlStatement {
sql: "SELECT cursor_value FROM git_mirror_cursor WHERE project_id=?1 AND kind=?2"
.into(),
params: vec![
SqlValue::Text(project_id.to_string()),
SqlValue::Text(kind.to_string()),
],
label: Some("git_ingest_read_cursor".into()),
})
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(row.and_then(|r| match r.get("cursor_value") {
Some(SqlValue::Text(s)) => Some(s.clone()),
_ => None,
}))
}
async fn write_cursor(
runtime: &KhiveRuntime,
project_id: Uuid,
kind: &str,
value: &str,
) -> Result<()> {
let sql = runtime.sql();
let mut w = sql.writer().await.map_err(|e| anyhow!("{e}"))?;
w.execute(SqlStatement {
sql: "INSERT INTO git_mirror_cursor(project_id, kind, cursor_value, updated_at) \
VALUES(?1, ?2, ?3, ?4) \
ON CONFLICT(project_id, kind) DO UPDATE SET \
cursor_value=excluded.cursor_value, \
updated_at=excluded.updated_at"
.into(),
params: vec![
SqlValue::Text(project_id.to_string()),
SqlValue::Text(kind.to_string()),
SqlValue::Text(value.to_string()),
SqlValue::Integer(Utc::now().timestamp_micros()),
],
label: Some("git_ingest_write_cursor".into()),
})
.await
.map_err(|e| anyhow!("{e}"))?;
Ok(())
}
const RECORD_SEP: char = '\u{1e}';
const FIELD_SEP: char = '\u{1f}';
struct RawCommit {
sha: String,
short_sha: String,
author: String,
author_email: String,
committed_at: String,
parents: Vec<String>,
subject: String,
body: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum GitLogPhase {
Metadata,
TouchedFiles,
}
#[derive(Debug)]
pub(crate) struct GitLogError {
phase: GitLogPhase,
stderr: String,
}
impl std::fmt::Display for GitLogError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let cmd = match self.phase {
GitLogPhase::Metadata => "git log",
GitLogPhase::TouchedFiles => "git log --name-only",
};
write!(f, "{cmd} failed: {}", self.stderr)
}
}
impl std::error::Error for GitLogError {}
impl GitLogError {
pub(crate) fn is_missing_promisor_object(&self) -> bool {
let lower = self.stderr.to_ascii_lowercase();
lower.contains("promisor")
&& (lower.contains("not in the object database") || lower.contains("missing object"))
}
}
fn walk_commits(repo: &Path, since_sha: Option<&str>) -> Result<Vec<RawCommit>> {
let format = format!("%H{FIELD_SEP}%h{FIELD_SEP}%an{FIELD_SEP}%ae{FIELD_SEP}%cI{FIELD_SEP}%P{FIELD_SEP}%s{FIELD_SEP}%b{RECORD_SEP}");
let mut args = vec![
"log".to_string(),
"--reverse".to_string(),
format!("--pretty=format:{format}"),
];
if let Some(sha) = since_sha {
args.push(format!("{sha}..HEAD"));
}
let output = Command::new("git")
.arg("-C")
.arg(repo)
.args(&args)
.output()
.context("spawning git log")?;
if !output.status.success() {
return Err(anyhow::Error::new(GitLogError {
phase: GitLogPhase::Metadata,
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}));
}
let text = String::from_utf8_lossy(&output.stdout);
let mut commits = Vec::new();
for record in text.split(RECORD_SEP) {
let record = record.trim_matches('\n');
if record.is_empty() {
continue;
}
let fields: Vec<&str> = record.splitn(8, FIELD_SEP).collect();
if fields.len() < 8 {
continue;
}
let sha = fields[0].to_string();
let short_sha = fields[1].to_string();
let author = fields[2].to_string();
let author_email = fields[3].to_string();
let committed_at = fields[4].to_string();
let parents = fields[5]
.split_whitespace()
.map(str::to_string)
.collect::<Vec<_>>();
let subject = fields[6].to_string();
let body = fields[7].trim_end_matches('\n').to_string();
commits.push(RawCommit {
sha,
short_sha,
author,
author_email,
committed_at,
parents,
subject,
body,
});
}
Ok(commits)
}
fn touched_files(repo: &Path) -> Result<HashMap<String, Vec<String>>> {
let output = Command::new("git")
.arg("-C")
.arg(repo)
.arg("log")
.arg("--name-only")
.arg(format!("--pretty=format:{RECORD_SEP}%H"))
.output()
.context("spawning git log --name-only")?;
if !output.status.success() {
return Err(anyhow::Error::new(GitLogError {
phase: GitLogPhase::TouchedFiles,
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}));
}
let text = String::from_utf8_lossy(&output.stdout);
let mut map: HashMap<String, Vec<String>> = HashMap::new();
for block in text.split(RECORD_SEP) {
let mut lines = block.lines().filter(|l| !l.trim().is_empty());
let Some(sha) = lines.next() else { continue };
let files: Vec<String> = lines.map(str::to_string).collect();
map.insert(sha.trim().to_string(), files);
}
Ok(map)
}
struct CommitSnapshot {
commits: Vec<RawCommit>,
files_by_sha: HashMap<String, Vec<String>>,
}
fn load_commit_snapshot(repo: &Path, since_sha: Option<&str>) -> Result<CommitSnapshot> {
let commits = walk_commits(repo, since_sha)?;
if commits.is_empty() {
return Ok(CommitSnapshot {
commits,
files_by_sha: HashMap::new(),
});
}
let files_by_sha = touched_files(repo)?;
Ok(CommitSnapshot {
commits,
files_by_sha,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum CacheRepairStrategy {
Refetch,
Reclone,
}
pub(crate) struct RecoveredRepo {
pub(crate) repo: PathBuf,
pub(crate) strategy: CacheRepairStrategy,
}
fn cache_repair_warning(strategy: CacheRepairStrategy) -> String {
match strategy {
CacheRepairStrategy::Refetch => {
"repaired corrupt remote git cache by refetching missing promisor objects".to_string()
}
CacheRepairStrategy::Reclone => {
"repaired corrupt remote git cache by replacing the owned clone".to_string()
}
}
}
fn recover_commit_snapshot(
repo: &Path,
since_sha: Option<&str>,
mut recover: impl FnMut(&Path, &GitLogError) -> Result<Option<RecoveredRepo>>,
) -> Result<(CommitSnapshot, Option<String>)> {
let mut repo_path = repo.to_path_buf();
let mut recovery_warning: Option<String> = None;
loop {
match load_commit_snapshot(&repo_path, since_sha) {
Ok(snapshot) => return Ok((snapshot, recovery_warning)),
Err(e) => {
let classified = e
.downcast_ref::<GitLogError>()
.filter(|g| g.is_missing_promisor_object());
let Some(git_log_err) = classified else {
return Err(e);
};
match recover(&repo_path, git_log_err)? {
Some(recovered) => {
repo_path = recovered.repo;
recovery_warning = Some(cache_repair_warning(recovered.strategy));
}
None => return Err(e),
}
}
}
}
}
fn squash_merge_pr_number(subject: &str) -> Option<u64> {
let trimmed = subject.trim_end();
let close = trimmed.strip_suffix(')')?;
let open = close.rfind("(#")?;
close[open + 2..].parse::<u64>().ok()
}
const NAME_MAX_CHARS: usize = 120;
const MAX_COMMIT_EMBED_BYTES: usize = 32_768;
fn truncated_embedding_head(content: &str) -> Option<&str> {
if content.len() <= MAX_COMMIT_EMBED_BYTES {
return None;
}
let mut end = MAX_COMMIT_EMBED_BYTES;
while !content.is_char_boundary(end) {
end -= 1;
}
Some(&content[..end])
}
struct MaskedCommitFields {
sha: String,
short_sha: String,
author: String,
author_email: String,
committed_at: String,
parents: Vec<String>,
subject: String,
body: String,
}
impl MaskedCommitFields {
fn new(commit: &RawCommit) -> Self {
let RawCommit {
sha,
short_sha,
author,
author_email,
committed_at,
parents,
subject,
body,
} = commit;
Self {
sha: sha.clone(),
short_sha: short_sha.clone(),
author: secret_gate::mask_secrets(author).into_owned(),
author_email: secret_gate::mask_secrets(author_email).into_owned(),
committed_at: committed_at.clone(),
parents: parents.clone(),
subject: secret_gate::mask_secrets(subject).into_owned(),
body: secret_gate::mask_secrets(body).into_owned(),
}
}
}
#[allow(clippy::too_many_arguments)]
async fn ingest_commits(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
repo: &Path,
project_id: Uuid,
merge_sha_to_pr: &HashMap<String, Uuid>,
number_to_pr: &HashMap<u64, Uuid>,
report: &mut IngestReport,
budget: &mut Budget,
new_records: &mut Vec<NewRecordForRef>,
recover: &mut (dyn FnMut(&Path, &GitLogError) -> Result<Option<RecoveredRepo>> + Send),
) -> Result<()> {
let since = read_cursor(runtime, project_id, "commits").await?;
let (snapshot, recovery_warning) = recover_commit_snapshot(repo, since.as_deref(), recover)?;
let CommitSnapshot {
commits,
files_by_sha,
} = snapshot;
if commits.is_empty() {
if let Some(warning) = recovery_warning {
report.warnings.push(warning);
}
return Ok(());
}
let mut last_sha: Option<String> = since;
let mut cursor_stalled = false;
let mut local_sha_to_id: HashMap<String, Uuid> = HashMap::new();
for c in &commits {
if let Some(existing) = find_commit_by_sha(runtime, token, &c.sha).await? {
local_sha_to_id.insert(c.sha.clone(), existing);
report.commits_skipped_existing += 1;
if !cursor_stalled {
last_sha = Some(c.sha.clone());
}
continue;
}
if budget.exhausted() {
break;
}
let masked = MaskedCommitFields::new(c);
let content = if masked.body.trim().is_empty() {
masked.subject.clone()
} else {
format!("{}\n\n{}", masked.subject, masked.body)
};
let mut annotates = vec![project_id.to_string()];
if let Some(paths) = files_by_sha.get(&c.sha) {
for p in paths {
if !p.starts_with("docs/adr/") {
continue;
}
if let Some(doc_id) = find_document_for_path(runtime, token, p).await? {
annotates.push(doc_id.to_string());
}
}
}
let pr_id = match merge_sha_to_pr.get(&c.sha).copied() {
Some(id) => Some(id),
None => match squash_merge_pr_number(&c.subject) {
Some(n) => match number_to_pr.get(&n).copied() {
Some(id) => Some(id),
None => find_by_number(runtime, token, "pull_request", project_id, n).await?,
},
None => None,
},
};
if let Some(pr_id) = pr_id {
annotates.push(pr_id.to_string());
}
let properties = json!({
"sha": masked.sha,
"short_sha": masked.short_sha,
"author": masked.author,
"author_email": masked.author_email,
"committed_at": masked.committed_at,
"parents": masked.parents,
});
let name = refs::truncate_chars(
&format!("{} {}", masked.short_sha, masked.subject),
NAME_MAX_CHARS,
);
let embedding_head = truncated_embedding_head(&content);
let mut create_request = json!({
"kind": "commit",
"name": name,
"content": content,
"properties": properties,
"annotates": annotates,
});
if let Some(head) = embedding_head {
create_request["embedding_content"] = json!(head);
}
budget.try_consume();
match registry.dispatch("create", create_request).await {
Ok(v) => {
report.commits_ingested += 1;
if embedding_head.is_some() {
report.commit_embeddings_truncated += 1;
}
if !cursor_stalled {
last_sha = Some(c.sha.clone());
}
if let Some(id) = v
.get("id")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
{
local_sha_to_id.insert(c.sha.clone(), id);
new_records.push(NewRecordForRef {
id,
text: content.clone(),
});
for parent_sha in &c.parents {
let parent_id = match local_sha_to_id.get(parent_sha).copied() {
Some(pid) => Some(pid),
None => find_commit_by_sha(runtime, token, parent_sha).await?,
};
let Some(parent_id) = parent_id else {
continue;
};
if parent_id == id {
continue;
}
match registry
.dispatch(
"link",
json!({
"source_id": parent_id.to_string(),
"target_id": id.to_string(),
"relation": "precedes",
}),
)
.await
{
Ok(_) => report.parent_edges_created += 1,
Err(e) => report.warnings.push(format!(
"linking parent {parent_sha} -> {} precedes: {e}",
c.sha
)),
}
}
}
}
Err(e) => {
report
.warnings
.push(format!("create commit {}: {e}", c.sha));
cursor_stalled = true;
}
}
}
if let Some(sha) = last_sha {
write_cursor(runtime, project_id, "commits", &sha).await?;
}
if let Some(warning) = recovery_warning {
report.warnings.push(warning);
}
Ok(())
}
#[derive(Debug, Deserialize)]
struct GhAuthor {
login: Option<String>,
}
#[derive(Debug, Deserialize)]
struct GhLabel {
name: String,
}
#[derive(Debug, Deserialize)]
struct GhIssue {
number: u64,
title: String,
author: Option<GhAuthor>,
#[serde(rename = "createdAt")]
created_at: Option<String>,
#[serde(rename = "closedAt")]
closed_at: Option<String>,
#[serde(rename = "updatedAt")]
updated_at: Option<String>,
labels: Option<Vec<GhLabel>>,
#[serde(rename = "stateReason")]
state_reason: Option<String>,
body: Option<String>,
}
#[derive(Debug, Deserialize)]
struct GhMergeCommit {
oid: Option<String>,
}
#[derive(Debug, Deserialize)]
struct GhPr {
number: u64,
title: String,
author: Option<GhAuthor>,
#[serde(rename = "createdAt")]
created_at: Option<String>,
#[serde(rename = "mergedAt")]
merged_at: Option<String>,
#[serde(rename = "closedAt")]
closed_at: Option<String>,
#[serde(rename = "updatedAt")]
updated_at: Option<String>,
#[serde(rename = "baseRefName")]
base_ref_name: Option<String>,
#[serde(rename = "headRefName")]
head_ref_name: Option<String>,
#[serde(rename = "mergeCommit")]
merge_commit: Option<GhMergeCommit>,
body: Option<String>,
}
struct MaskedIssueFields {
number: u64,
title: String,
body: String,
author_login: Option<String>,
labels: Vec<String>,
created_at: Option<String>,
closed_at: Option<String>,
updated_at: Option<String>,
state_reason: StateReasonField,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum StateReasonField {
Absent,
Valid(String),
Rejected,
}
impl MaskedIssueFields {
fn new(issue: GhIssue, warnings: &mut Vec<String>) -> Self {
let GhIssue {
number,
title,
author,
created_at,
closed_at,
updated_at,
labels,
state_reason,
body,
} = issue;
Self {
number,
title: secret_gate::mask_secrets(&title).into_owned(),
body: secret_gate::mask_secrets(&body.unwrap_or_default()).into_owned(),
author_login: author
.and_then(|a| a.login)
.map(|login| secret_gate::mask_secrets(&login).into_owned()),
labels: labels
.unwrap_or_default()
.into_iter()
.map(|l| secret_gate::mask_secrets(&l.name).into_owned())
.collect(),
created_at: canonical_issue_timestamp("createdAt", number, created_at, warnings),
closed_at: canonical_issue_timestamp("closedAt", number, closed_at, warnings),
updated_at: canonical_issue_timestamp("updatedAt", number, updated_at, warnings),
state_reason: canonical_issue_state_reason(state_reason),
}
}
}
fn canonical_issue_state_reason(raw: Option<String>) -> StateReasonField {
let Some(raw) = raw.filter(|r| !r.is_empty()) else {
return StateReasonField::Absent;
};
let lowered = raw.to_ascii_lowercase();
if hook::ISSUE_STATE_REASONS.contains(&lowered.as_str()) {
StateReasonField::Valid(lowered)
} else {
StateReasonField::Rejected
}
}
fn canonical_issue_timestamp(
field: &'static str,
number: u64,
raw: Option<String>,
warnings: &mut Vec<String>,
) -> Option<String> {
let raw = raw?;
match chrono::DateTime::parse_from_rfc3339(&raw) {
Ok(dt) => Some(
dt.with_timezone(&Utc)
.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
),
Err(_) => {
warnings.push(format!(
"issue #{number}: {field} is not a valid RFC3339 timestamp, field dropped"
));
None
}
}
}
fn gh_json(repo: &Path, args: &[&str]) -> Result<String> {
let output = Command::new("gh")
.current_dir(repo)
.args(args)
.output()
.context("spawning gh")?;
if !output.status.success() {
return Err(anyhow!(
"gh {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
));
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
const PAGE_LIMIT: usize = 1000;
#[derive(Debug, Clone, PartialEq, Eq)]
enum PageOutcome {
WindowComplete,
StopBudgetExhausted,
StopFloorStalled,
Continue(String),
}
fn decide_page_outcome(
page_len: usize,
current_floor: Option<&str>,
last_updated_at: Option<&str>,
budget_exhausted: bool,
) -> PageOutcome {
if page_len < PAGE_LIMIT {
return PageOutcome::WindowComplete;
}
if budget_exhausted {
return PageOutcome::StopBudgetExhausted;
}
match last_updated_at {
Some(next) if Some(next) != current_floor => PageOutcome::Continue(next.to_string()),
_ => PageOutcome::StopFloorStalled,
}
}
#[cfg(test)]
fn page_outcome_proves_window_complete(outcome: PageOutcome) -> bool {
matches!(outcome, PageOutcome::WindowComplete)
}
fn search_query(floor: Option<&str>) -> String {
match floor {
Some(f) => format!("sort:updated-asc updated:>={f}"),
None => "sort:updated-asc".to_string(),
}
}
const PR_FIELDS: &str = "number,title,author,createdAt,mergedAt,closedAt,updatedAt,baseRefName,headRefName,mergeCommit,body";
const ISSUE_FIELDS: &str =
"number,title,author,createdAt,closedAt,updatedAt,labels,stateReason,body";
fn fetch_pr_page(repo: &Path, floor: Option<&str>) -> Result<Vec<GhPr>> {
let search = search_query(floor);
let raw = gh_json(
repo,
&[
"pr",
"list",
"--state",
"all",
"--search",
search.as_str(),
"--limit",
"1000",
"--json",
PR_FIELDS,
],
)?;
serde_json::from_str(&raw).context("parsing gh pr list --json")
}
fn fetch_issue_page(repo: &Path, floor: Option<&str>) -> Result<Vec<GhIssue>> {
let search = search_query(floor);
let raw = gh_json(
repo,
&[
"issue",
"list",
"--state",
"all",
"--search",
search.as_str(),
"--limit",
"1000",
"--json",
ISSUE_FIELDS,
],
)?;
serde_json::from_str(&raw).context("parsing gh issue list --json")
}
struct MaskedPrFields {
number: u64,
title: String,
body: String,
author_login: Option<String>,
created_at: Option<String>,
merged_at: Option<String>,
closed_at: Option<String>,
updated_at: Option<String>,
base_ref_name: Option<String>,
head_ref_name: Option<String>,
merge_commit_oid: Option<String>,
}
impl MaskedPrFields {
fn new(pr: GhPr) -> Self {
let GhPr {
number,
title,
author,
created_at,
merged_at,
closed_at,
updated_at,
base_ref_name,
head_ref_name,
merge_commit,
body,
} = pr;
Self {
number,
title: secret_gate::mask_secrets(&title).into_owned(),
body: secret_gate::mask_secrets(&body.unwrap_or_default()).into_owned(),
author_login: author
.and_then(|a| a.login)
.map(|login| secret_gate::mask_secrets(&login).into_owned()),
created_at,
merged_at,
closed_at,
updated_at,
base_ref_name: base_ref_name.map(|r| secret_gate::mask_secrets(&r).into_owned()),
head_ref_name: head_ref_name.map(|r| secret_gate::mask_secrets(&r).into_owned()),
merge_commit_oid: merge_commit.and_then(|m| m.oid),
}
}
}
#[allow(clippy::too_many_arguments)]
async fn ingest_prs(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
repo: &Path,
project_id: Uuid,
report: &mut IngestReport,
merge_sha_to_pr: &mut HashMap<String, Uuid>,
number_to_pr: &mut HashMap<u64, Uuid>,
budget: &mut Budget,
new_records: &mut Vec<NewRecordForRef>,
) -> Result<()> {
let since = read_cursor(runtime, project_id, "prs").await?;
let mut max_updated: Option<String> = since.clone();
let mut cursor_stalled = false;
let mut floor = since.clone();
let mut window_complete = true;
'paging: loop {
let mut page = fetch_pr_page(repo, floor.as_deref())?;
let page_len = page.len();
page.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
let last_updated_at = page.last().and_then(|pr| pr.updated_at.clone());
for pr in page {
let is_new = since
.as_deref()
.zip(pr.updated_at.as_deref())
.map(|(cursor, updated)| updated >= cursor)
.unwrap_or(true);
if let Some(existing) =
find_by_number(runtime, token, "pull_request", project_id, pr.number).await?
{
number_to_pr.insert(pr.number, existing);
if let Some(oid) = pr.merge_commit.as_ref().and_then(|m| m.oid.clone()) {
merge_sha_to_pr.insert(oid, existing);
}
report.prs_skipped_existing += 1;
if !cursor_stalled {
if let Some(u) = &pr.updated_at {
if max_updated
.as_deref()
.map(|m| u.as_str() > m)
.unwrap_or(true)
{
max_updated = Some(u.clone());
}
}
}
continue;
}
if !is_new {
continue;
}
if budget.exhausted() {
break;
}
let masked = MaskedPrFields::new(pr);
let content = masked.body;
let properties = json!({
"number": masked.number,
"title": masked.title,
"author": masked.author_login,
"created_at": masked.created_at,
"merged_at": masked.merged_at,
"closed_at": masked.closed_at,
"base_ref": masked.base_ref_name,
"head_ref": masked.head_ref_name,
"project_id": project_id.to_string(),
});
let name = refs::truncate_chars(
&format!("#{} {}", masked.number, masked.title),
NAME_MAX_CHARS,
);
budget.try_consume();
let result = match registry
.dispatch(
"create",
json!({
"kind": "pull_request",
"name": name,
"content": content,
"properties": properties,
"annotates": [project_id.to_string()],
}),
)
.await
{
Ok(v) => v,
Err(e) => {
report
.warnings
.push(format!("create pull_request #{}: {e}", masked.number));
cursor_stalled = true;
continue;
}
};
if let Some(id) = result
.get("id")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
{
number_to_pr.insert(masked.number, id);
if let Some(oid) = masked.merge_commit_oid {
merge_sha_to_pr.insert(oid, id);
}
new_records.push(NewRecordForRef {
id,
text: content.clone(),
});
}
report.prs_ingested += 1;
if !cursor_stalled {
if let Some(u) = &masked.updated_at {
if max_updated
.as_deref()
.map(|m| u.as_str() > m)
.unwrap_or(true)
{
max_updated = Some(u.clone());
}
}
}
}
match decide_page_outcome(
page_len,
floor.as_deref(),
last_updated_at.as_deref(),
budget.exhausted(),
) {
PageOutcome::WindowComplete => break 'paging,
PageOutcome::StopBudgetExhausted | PageOutcome::StopFloorStalled => {
window_complete = false;
break 'paging;
}
PageOutcome::Continue(next_floor) => floor = Some(next_floor),
}
}
if !window_complete {
report.done = false;
}
if let Some(cursor) = max_updated {
write_cursor(runtime, project_id, "prs", &cursor).await?;
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
async fn ingest_issues(
runtime: &KhiveRuntime,
token: &NamespaceToken,
registry: &VerbRegistry,
repo: &Path,
project_id: Uuid,
report: &mut IngestReport,
budget: &mut Budget,
new_records: &mut Vec<NewRecordForRef>,
) -> Result<()> {
let since = read_cursor(runtime, project_id, "issues").await?;
let mut max_updated: Option<String> = since.clone();
let mut cursor_stalled = false;
let mut floor = since.clone();
let mut window_complete = true;
'paging: loop {
let page = fetch_issue_page(repo, floor.as_deref())?;
let page_len = page.len();
let mut masked_page: Vec<MaskedIssueFields> = page
.into_iter()
.map(|issue| MaskedIssueFields::new(issue, &mut report.warnings))
.collect();
masked_page.sort_by(|a, b| a.updated_at.cmp(&b.updated_at));
let last_updated_at = masked_page.last().and_then(|i| i.updated_at.clone());
for masked in masked_page {
let is_new = since
.as_deref()
.zip(masked.updated_at.as_deref())
.map(|(cursor, updated)| updated >= cursor)
.unwrap_or(true);
if find_by_number(runtime, token, "issue", project_id, masked.number)
.await?
.is_some()
{
report.issues_skipped_existing += 1;
if !cursor_stalled {
if let Some(u) = &masked.updated_at {
if max_updated
.as_deref()
.map(|m| u.as_str() > m)
.unwrap_or(true)
{
max_updated = Some(u.clone());
}
}
}
continue;
}
if !is_new {
continue;
}
if budget.exhausted() {
break;
}
let number = masked.number;
let updated_at = masked.updated_at.clone();
if masked.state_reason == StateReasonField::Rejected {
report.warnings.push(format!(
"issue #{number}: stateReason is not one of the governed values, record skipped"
));
cursor_stalled = true;
continue;
}
let content = masked.body;
let safe_title = masked.title;
let mut properties = json!({
"number": number,
"title": safe_title,
"author": masked.author_login,
"created_at": masked.created_at,
"closed_at": masked.closed_at,
"labels": masked.labels,
"project_id": project_id.to_string(),
});
if let StateReasonField::Valid(reason) = masked.state_reason {
properties["state_reason"] = json!(reason);
}
let name = refs::truncate_chars(&format!("#{number} {safe_title}"), NAME_MAX_CHARS);
budget.try_consume();
let result = match registry
.dispatch(
"create",
json!({
"kind": "issue",
"name": name,
"content": content,
"properties": properties,
"annotates": [project_id.to_string()],
}),
)
.await
{
Ok(v) => v,
Err(e) => {
report.warnings.push(format!("create issue #{number}: {e}"));
cursor_stalled = true;
continue;
}
};
if let Some(id) = result
.get("id")
.and_then(|v| v.as_str())
.and_then(|s| Uuid::parse_str(s).ok())
{
new_records.push(NewRecordForRef {
id,
text: content.clone(),
});
}
report.issues_ingested += 1;
if !cursor_stalled {
if let Some(u) = &updated_at {
if max_updated
.as_deref()
.map(|m| u.as_str() > m)
.unwrap_or(true)
{
max_updated = Some(u.clone());
}
}
}
}
match decide_page_outcome(
page_len,
floor.as_deref(),
last_updated_at.as_deref(),
budget.exhausted(),
) {
PageOutcome::WindowComplete => break 'paging,
PageOutcome::StopBudgetExhausted | PageOutcome::StopFloorStalled => {
window_complete = false;
break 'paging;
}
PageOutcome::Continue(next_floor) => floor = Some(next_floor),
}
}
if !window_complete {
report.done = false;
}
if let Some(cursor) = max_updated {
write_cursor(runtime, project_id, "issues", &cursor).await?;
}
Ok(())
}
#[cfg(test)]
mod paging_tests {
use super::*;
#[test]
fn search_query_omits_updated_qualifier_with_no_floor() {
assert_eq!(search_query(None), "sort:updated-asc");
}
#[test]
fn search_query_includes_inclusive_updated_floor() {
assert_eq!(
search_query(Some("2024-01-01T00:00:00Z")),
"sort:updated-asc updated:>=2024-01-01T00:00:00Z"
);
}
#[test]
fn short_page_proves_window_complete_regardless_of_budget() {
let outcome = decide_page_outcome(42, None, Some("2024-01-01T00:00:00Z"), false);
assert_eq!(outcome, PageOutcome::WindowComplete);
assert!(page_outcome_proves_window_complete(outcome));
let outcome = decide_page_outcome(0, None, None, true);
assert_eq!(outcome, PageOutcome::WindowComplete);
}
#[test]
fn full_page_with_stalled_floor_is_not_window_complete_even_with_budget_left() {
let outcome = decide_page_outcome(PAGE_LIMIT, Some("X"), Some("X"), false);
assert_eq!(outcome, PageOutcome::StopFloorStalled);
assert!(!page_outcome_proves_window_complete(outcome));
}
#[test]
fn full_page_with_advancing_floor_and_budget_left_continues() {
let outcome = decide_page_outcome(PAGE_LIMIT, Some("A"), Some("B"), false);
assert_eq!(outcome, PageOutcome::Continue("B".to_string()));
assert!(!page_outcome_proves_window_complete(outcome));
}
#[test]
fn full_page_with_exhausted_budget_stops_without_proving_completeness() {
let outcome = decide_page_outcome(PAGE_LIMIT, Some("A"), Some("B"), true);
assert_eq!(outcome, PageOutcome::StopBudgetExhausted);
assert!(!page_outcome_proves_window_complete(outcome));
}
#[test]
fn full_page_with_no_updated_at_stalls_rather_than_looping_forever() {
let outcome = decide_page_outcome(PAGE_LIMIT, Some("A"), None, false);
assert_eq!(outcome, PageOutcome::StopFloorStalled);
}
}
#[cfg(test)]
mod recovery_classifier_tests {
use super::*;
fn err(phase: GitLogPhase, stderr: &str) -> GitLogError {
GitLogError {
phase,
stderr: stderr.to_string(),
}
}
const REAL_WORLD_MESSAGE: &str = "fatal: deadbeefdeadbeefdeadbeefdeadbeefdeadbeef is in \
the commit graph file, but not in the object database\nfatal: unable to parse commit: \
deadbeefdeadbeefdeadbeefdeadbeefdeadbeef\nfatal: could not fetch from promisor remote";
#[test]
fn classifies_real_world_missing_promisor_object_message_on_either_phase() {
assert!(err(GitLogPhase::TouchedFiles, REAL_WORLD_MESSAGE).is_missing_promisor_object());
assert!(err(GitLogPhase::Metadata, REAL_WORLD_MESSAGE).is_missing_promisor_object());
}
#[test]
fn classifies_missing_object_wording_case_insensitively() {
assert!(err(
GitLogPhase::TouchedFiles,
"FATAL: MISSING OBJECT abc123; PROMISOR remote unavailable"
)
.is_missing_promisor_object());
}
#[test]
fn does_not_classify_bad_object_without_promisor() {
assert!(!err(GitLogPhase::Metadata, "fatal: bad object HEAD").is_missing_promisor_object());
}
#[test]
fn does_not_classify_auth_or_network_failures() {
assert!(!err(
GitLogPhase::Metadata,
"fatal: Authentication failed for 'https://example.com/org/repo.git/'"
)
.is_missing_promisor_object());
assert!(!err(
GitLogPhase::TouchedFiles,
"fatal: unable to access 'https://example.com/org/repo.git/': Could not resolve host"
)
.is_missing_promisor_object());
}
#[test]
fn does_not_classify_promisor_mention_without_missing_object_wording() {
assert!(!err(
GitLogPhase::Metadata,
"fatal: promisor remote configured but unreachable"
)
.is_missing_promisor_object());
}
#[test]
fn recover_commit_snapshot_returns_no_warning_when_healthy() {
let _env = crate::cache::ENV_MUTEX.blocking_lock();
let dir = tempfile::tempdir().expect("tempdir");
init_repo_with_commit(dir.path());
let mut recover_calls = 0;
let (snapshot, warning) = recover_commit_snapshot(dir.path(), None, |_repo, _err| {
recover_calls += 1;
Ok(None)
})
.expect("healthy repo loads");
assert_eq!(snapshot.commits.len(), 1);
assert_eq!(warning, None);
assert_eq!(recover_calls, 0);
}
#[test]
fn recover_commit_snapshot_never_calls_recover_for_unclassified_failures() {
let _env = crate::cache::ENV_MUTEX.blocking_lock();
let dir = tempfile::tempdir().expect("tempdir");
let mut recover_calls = 0;
let result = recover_commit_snapshot(dir.path(), None, |_repo, _err| {
recover_calls += 1;
Ok(Some(RecoveredRepo {
repo: dir.path().to_path_buf(),
strategy: CacheRepairStrategy::Refetch,
}))
});
assert!(result.is_err(), "a non-repo path must fail to load");
assert_eq!(
recover_calls, 0,
"an unclassified failure must never invoke recover"
);
}
fn init_repo_with_commit(repo: &Path) {
let run = |args: &[&str]| {
let out = std::process::Command::new("git")
.arg("-C")
.arg(repo)
.args(args)
.output()
.expect("spawn git");
assert!(
out.status.success(),
"git {args:?} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
};
run(&["init", "-q", "-b", "main"]);
run(&["config", "user.email", "test@example.com"]);
run(&["config", "user.name", "Test User"]);
std::fs::write(repo.join("a.txt"), b"hello").unwrap();
run(&["add", "a.txt"]);
run(&["commit", "-q", "-m", "initial"]);
}
}
#[cfg(test)]
mod truncation_tests {
use super::*;
#[test]
fn under_cap_content_is_not_truncated() {
let content = "a".repeat(MAX_COMMIT_EMBED_BYTES - 1);
assert_eq!(truncated_embedding_head(&content), None);
}
#[test]
fn exactly_at_cap_content_is_not_truncated() {
let content = "a".repeat(MAX_COMMIT_EMBED_BYTES);
assert_eq!(truncated_embedding_head(&content), None);
}
#[test]
fn over_cap_content_is_truncated_to_exactly_the_cap() {
let content = "a".repeat(MAX_COMMIT_EMBED_BYTES + 1);
let head = truncated_embedding_head(&content).expect("over cap must truncate");
assert_eq!(head.len(), MAX_COMMIT_EMBED_BYTES);
assert!(content.starts_with(head));
}
#[test]
fn multibyte_scalar_straddling_cap_rolls_back_to_char_boundary() {
let mut content = "a".repeat(MAX_COMMIT_EMBED_BYTES - 1);
content.push('€'); content.push_str("tail-sentinel");
let head = truncated_embedding_head(&content).expect("over cap must truncate");
assert!(head.len() <= MAX_COMMIT_EMBED_BYTES);
assert!(content.is_char_boundary(head.len()));
assert!(std::str::from_utf8(head.as_bytes()).is_ok());
assert!(content.starts_with(head));
assert!(
!head.contains("tail-sentinel"),
"head must not include text past the cap"
);
}
}
#[cfg(test)]
mod compact_prefix_resolver_tests {
use super::*;
use khive_runtime::Namespace;
#[tokio::test]
async fn resolve_project_id_rejects_like_wildcard_input() {
let rt = KhiveRuntime::memory().unwrap();
let token = rt.authorize(Namespace::local()).unwrap();
let project = rt
.create_entity(
&token,
"project",
None,
"WildcardIngestTest",
None,
None,
vec![],
)
.await
.unwrap();
let compact = project.id.simple().to_string();
let wildcard_input = format!("{}%", &compact[..8]);
let resolved = resolve_project_id(&rt, &wildcard_input).await.unwrap();
assert_eq!(
resolved, None,
"a %-bearing project argument must not resolve via a wildcard LIKE scan"
);
}
#[tokio::test]
async fn resolve_id_resolves_compact_prefix_over_8_chars() {
let rt = KhiveRuntime::memory().unwrap();
let token = rt.authorize(Namespace::local()).unwrap();
let project = rt
.create_entity(
&token,
"project",
None,
"CompactIngestTest",
None,
None,
vec![],
)
.await
.unwrap();
let compact = project.id.simple().to_string();
let resolved = resolve_id(&rt, &token, &compact[..16]).await.unwrap();
assert_eq!(resolved, Some(project.id));
}
}
#[cfg(test)]
mod find_document_for_path_tests {
use super::*;
use khive_runtime::Namespace;
async fn create_document(rt: &KhiveRuntime, token: &NamespaceToken, source_uri: &str) -> Uuid {
rt.create_entity(
token,
"document",
None,
source_uri,
None,
Some(json!({ "source_uri": source_uri })),
vec![],
)
.await
.unwrap()
.id
}
#[tokio::test]
async fn path_with_like_wildcards_resolves_only_itself() {
let rt = KhiveRuntime::memory().unwrap();
let token = rt.authorize(Namespace::local()).unwrap();
let path = "src/100%_done.rs";
let decoy_source_uri = "prefix/src/100Qdone.rs";
create_document(&rt, &token, decoy_source_uri).await;
let resolved = find_document_for_path(&rt, &token, path).await.unwrap();
assert_eq!(
resolved, None,
"a % or _ in the path must be matched literally, not as a LIKE wildcard"
);
}
#[tokio::test]
async fn exact_match_wins_over_wildcard_broadened_candidate() {
let rt = KhiveRuntime::memory().unwrap();
let token = rt.authorize(Namespace::local()).unwrap();
let path = "crates/khive-pack-git/src/ingest.rs";
let broadened_suffix_path = "other/crates/khive-pack-git/src/ingest.rs";
create_document(&rt, &token, broadened_suffix_path).await;
let exact_id = create_document(&rt, &token, path).await;
let resolved = find_document_for_path(&rt, &token, path).await.unwrap();
assert_eq!(
resolved,
Some(exact_id),
"an exact source_uri match must always win over a suffix-LIKE candidate"
);
}
#[tokio::test]
async fn single_query_snapshot_prefers_exact_over_broadened() {
let rt = KhiveRuntime::memory().unwrap();
let token = rt.authorize(Namespace::local()).unwrap();
let path = "crates/khive-pack-git/src/toctou.rs";
let broadened_suffix_path = "other/crates/khive-pack-git/src/toctou.rs";
let exact_id = create_document(&rt, &token, path).await;
create_document(&rt, &token, broadened_suffix_path).await;
let resolved = find_document_for_path(&rt, &token, path).await.unwrap();
assert_eq!(
resolved,
Some(exact_id),
"a single query covering both exact and broadened candidates \
must still rank the exact match first, regardless of insertion order"
);
}
}