relay_knowledge/domain/code/repository/
scope_identity.rs1use crate::identity::stable_hash64;
2
3const CODE_SNAPSHOT_FACT_VERSION: &str = "code-facts-js-ts-import-edges-v1-sbom-dependencies-v2-python-type-refs-v1-scope-compat-v1-workspace-imports-v1-generated-files-v1-web-routes-v1-syntax-failure-chunks-v1-bounded-config-chunks-v1-dense-source-windows-v1-c-composite-tags-v1-doc-block-owner-anchor-v2-bounded-type-doc-summary-v1-search-owner-v2-reference-search-groups-v2";
4
5pub fn code_snapshot_scope_id(
7 repository_id: &str,
8 tree_hash: &str,
9 path_filters: &[String],
10 language_filters: &[String],
11) -> String {
12 let mut input = Vec::new();
13 append_hash_part(&mut input, "git_snapshot");
14 append_hash_part(&mut input, repository_id);
15 append_hash_part(&mut input, tree_hash);
16 append_hash_list(&mut input, path_filters);
17 append_hash_list(&mut input, language_filters);
18 append_hash_part(&mut input, CODE_SNAPSHOT_FACT_VERSION);
19
20 format!("git_snapshot:{:016x}", stable_hash64(&input))
21}
22
23pub fn code_snapshot_scope_id_with_workspace_detection(
26 repository_id: &str,
27 tree_hash: &str,
28 path_filters: &[String],
29 language_filters: &[String],
30 config: &super::super::workspace::CodeWorkspaceDetectionConfig,
31) -> String {
32 let base = code_snapshot_scope_id(repository_id, tree_hash, path_filters, language_filters);
33 workspace_detection_mask(config)
34 .map_or(base.clone(), |mask| format!("{base}:workspace-v1:{mask}"))
35}
36
37pub fn code_snapshot_scope_matches_identity(
40 repository_id: &str,
41 tree_hash: &str,
42 path_filters: &[String],
43 language_filters: &[String],
44 source_scope: &str,
45) -> bool {
46 let base = code_snapshot_scope_id(repository_id, tree_hash, path_filters, language_filters);
47 parse_scope_identity(source_scope).is_some_and(|identity| identity.base == base)
48}
49
50pub fn code_snapshot_scope_workspace_semantic(
54 repository_id: &str,
55 tree_hash: &str,
56 path_filters: &[String],
57 language_filters: &[String],
58 source_scope: &str,
59) -> Option<Option<u8>> {
60 let expected_base =
61 code_snapshot_scope_id(repository_id, tree_hash, path_filters, language_filters);
62 let identity = parse_scope_identity(source_scope)?;
63 (identity.base == expected_base).then_some(identity.workspace_mask)
64}
65
66fn workspace_detection_mask(
67 config: &super::super::workspace::CodeWorkspaceDetectionConfig,
68) -> Option<u8> {
69 if !config.enabled {
70 return None;
71 }
72 let formats = [
73 super::super::workspace::CodeMonorepoWorkspaceFormat::Pnpm,
74 super::super::workspace::CodeMonorepoWorkspaceFormat::GoModules,
75 super::super::workspace::CodeMonorepoWorkspaceFormat::CargoWorkspace,
76 ];
77 Some(
78 formats
79 .iter()
80 .enumerate()
81 .fold(0_u8, |mask, (index, format)| {
82 mask | (u8::from(config.supported_formats.contains(format)) << index)
83 }),
84 )
85}
86
87pub fn code_snapshot_scope_is_fact_versioned(source_scope: &str) -> bool {
88 parse_scope_identity(source_scope).is_some()
89}
90
91struct ParsedScopeIdentity {
92 base: String,
93 workspace_mask: Option<u8>,
94}
95
96fn parse_scope_identity(source_scope: &str) -> Option<ParsedScopeIdentity> {
97 let mut parts = source_scope.split(':');
98 if parts.next()? != "git_snapshot" {
99 return None;
100 }
101 let hash = parts.next()?;
102 if hash.len() != 16 || !hash.bytes().all(|byte| byte.is_ascii_hexdigit()) {
103 return None;
104 }
105 let base = format!("git_snapshot:{hash}");
106 let Some(label) = parts.next() else {
107 return Some(ParsedScopeIdentity {
108 base,
109 workspace_mask: None,
110 });
111 };
112 if label != "workspace-v1" {
113 return None;
114 }
115 let encoded = parts.next()?;
116 if parts.next().is_some() {
117 return None;
118 }
119 let mask = encoded.parse::<u8>().ok()?;
120 if mask >= 8 || encoded != mask.to_string() {
121 return None;
122 }
123 Some(ParsedScopeIdentity {
124 base,
125 workspace_mask: Some(mask),
126 })
127}
128
129pub fn clean_git_commit_from_snapshot_identity(identity: &str) -> Option<&str> {
136 if identity.is_empty() || identity.starts_with("filesystem:") {
137 return None;
138 }
139 let Some(rest) = identity.strip_prefix("worktree:") else {
140 return Some(identity);
141 };
142 let (base_commit, overlay_hash) = rest.split_once(':')?;
143 (!base_commit.is_empty() && !overlay_hash.is_empty()).then_some(base_commit)
144}
145
146fn append_hash_list(input: &mut Vec<u8>, values: &[String]) {
147 input.extend_from_slice(&(values.len() as u64).to_le_bytes());
148 for value in values {
149 append_hash_part(input, value);
150 }
151}
152
153fn append_hash_part(input: &mut Vec<u8>, value: &str) {
154 input.extend_from_slice(&(value.len() as u64).to_le_bytes());
155 input.extend_from_slice(value.as_bytes());
156}
157
158#[cfg(test)]
159#[path = "scope_identity_tests.rs"]
160mod tests;