Skip to main content

relay_knowledge/watcher/task_seed/
mod.rs

1use std::path::{Path, PathBuf};
2
3use super::WatchedRepository;
4
5pub(super) struct ChangedPathSnapshot {
6    pub path: PathBuf,
7    pub content_hash: u64,
8}
9
10pub fn build_incremental_task_seed(
11    repository: &WatchedRepository,
12    changed_paths: &[PathBuf],
13    ref_selector: &str,
14    resolved_commit_sha: &str,
15    tree_hash: &str,
16    content_fingerprint: u64,
17    now_ms: u64,
18) -> Option<crate::storage::CodeIndexTaskSeed> {
19    if changed_paths.is_empty() {
20        return None;
21    }
22    let relative_paths = changed_path_labels(repository, changed_paths);
23    if relative_paths.is_empty() {
24        return None;
25    }
26    let path_hash = stable_path_fingerprint(&relative_paths);
27    let effective_ref = if ref_selector.trim().is_empty() {
28        "HEAD"
29    } else {
30        ref_selector
31    };
32    let task_resolved_commit = if resolved_commit_sha.trim().is_empty() {
33        effective_ref.to_owned()
34    } else {
35        resolved_commit_sha.to_owned()
36    };
37    let task_tree_hash = if tree_hash.trim().is_empty() {
38        format!("worktree:pending:{content_fingerprint:016x}")
39    } else {
40        tree_hash.to_owned()
41    };
42
43    let input_fingerprint = format!(
44        "worktree_overlay:{}:{}:{}:{path_hash:016x}:{content_fingerprint:016x}",
45        repository.repository_id, task_tree_hash, repository.source_scope,
46    );
47
48    let request = crate::domain::CodeIndexRequest {
49        repository: crate::domain::CodeRepositorySelector {
50            repository: repository.alias.clone(),
51            ref_selector: effective_ref.to_owned(),
52            path_filters: Vec::new(),
53            language_filters: Vec::new(),
54        },
55        mode: crate::domain::CodeIndexMode::WorktreeOverlay,
56        workspace_detection: Default::default(),
57        freshness_policy: crate::domain::FreshnessPolicy::WaitUntilFresh,
58    };
59    let mut payload = serde_json::to_value(&request).ok()?;
60    if let Some(object) = payload.as_object_mut() {
61        object.insert(
62            "watcher".to_owned(),
63            serde_json::json!({
64                "repository_id": repository.repository_id.clone(),
65                "changed_paths": relative_paths,
66                "content_fingerprint": format!("{content_fingerprint:016x}"),
67            }),
68        );
69    }
70
71    Some(crate::storage::CodeIndexTaskSeed {
72        repository_id: repository.repository_id.clone(),
73        alias: repository.alias.clone(),
74        ref_selector: effective_ref.to_owned(),
75        resolved_commit_sha: task_resolved_commit,
76        tree_hash: task_tree_hash,
77        source_scope: repository.source_scope.clone(),
78        path_filters: repository.path_filters.clone(),
79        language_filters: repository.language_filters.clone(),
80        mode: crate::domain::CodeIndexMode::WorktreeOverlay,
81        input_fingerprint,
82        resource_budget: crate::domain::CodeIndexResourceBudget::default(),
83        payload_json: serde_json::to_string(&payload).ok()?,
84        now_ms,
85    })
86}
87
88pub(super) fn changed_content_fingerprint(
89    repository: &WatchedRepository,
90    changes: &[&ChangedPathSnapshot],
91) -> u64 {
92    let mut entries = changes
93        .iter()
94        .filter_map(|change| {
95            let relative = change.path.strip_prefix(&repository.root).ok()?;
96            let label = path_label(relative)?;
97            Some((label, change.content_hash))
98        })
99        .collect::<Vec<_>>();
100    entries.sort();
101    entries.dedup();
102    stable_content_fingerprint(&entries)
103}
104
105pub(super) fn unreadable_path_fingerprint(path: &Path) -> u64 {
106    let label = path_label(path).unwrap_or_else(|| "<unreadable>".to_owned());
107    stable_content_fingerprint(&[(label, 0)])
108}
109
110fn changed_path_labels(repository: &WatchedRepository, changed_paths: &[PathBuf]) -> Vec<String> {
111    let mut labels = changed_paths
112        .iter()
113        .filter_map(|path| path.strip_prefix(&repository.root).ok())
114        .filter_map(path_label)
115        .collect::<Vec<_>>();
116    labels.sort();
117    labels.dedup();
118    labels
119}
120
121fn path_label(path: &Path) -> Option<String> {
122    let value = path
123        .to_string_lossy()
124        .replace(std::path::MAIN_SEPARATOR, "/");
125    (!value.is_empty()).then_some(value)
126}
127
128fn stable_path_fingerprint(paths: &[String]) -> u64 {
129    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
130    const FNV_PRIME: u64 = 0x100000001b3;
131
132    let mut hash = FNV_OFFSET_BASIS;
133    for path in paths {
134        for byte in path.as_bytes().iter().copied().chain([0]) {
135            hash ^= u64::from(byte);
136            hash = hash.wrapping_mul(FNV_PRIME);
137        }
138    }
139    hash
140}
141
142fn stable_content_fingerprint(entries: &[(String, u64)]) -> u64 {
143    const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
144    const FNV_PRIME: u64 = 0x100000001b3;
145
146    let mut hash = FNV_OFFSET_BASIS;
147    for (path, content_hash) in entries {
148        for byte in path
149            .as_bytes()
150            .iter()
151            .copied()
152            .chain([0])
153            .chain(content_hash.to_le_bytes())
154            .chain([0])
155        {
156            hash ^= u64::from(byte);
157            hash = hash.wrapping_mul(FNV_PRIME);
158        }
159    }
160    hash
161}
162
163#[cfg(test)]
164#[path = "mod_tests.rs"]
165mod tests;