use std::collections::HashMap;
use std::path::Path;
use super::BM25Index;
use crate::core::context_admission::stores::StoreAdmission;
impl BM25Index {
#[must_use]
pub(super) fn carrying_external_from(mut self, prev: &Self) -> Self {
if prev.provider_origins.is_empty() {
return self;
}
let admission = StoreAdmission::current();
let digest = Some(admission.digest());
if self.admission_policy.as_deref() != digest || prev.admission_policy.as_deref() != digest
{
return self;
}
let verbatim = |text: &str, admitted: Option<String>| admitted.as_deref() == Some(text);
let mut positions: HashMap<usize, usize> = HashMap::new();
for (idx, chunk) in prev.chunks.iter().enumerate() {
let source = chunk.file_path.as_str();
let Some(origin) = prev.provider_origins.get(source) else {
continue;
};
if !source.contains("://")
|| !verbatim(
&chunk.content,
admission.admit(
&chunk.content,
Path::new(source),
chunk.input_origin.as_ref(),
),
)
|| !verbatim(
&chunk.symbol_name,
admission.admit_text_as(
&chunk.symbol_name,
&crate::core::context_admission::stores::class_of_path(
Path::new(source),
chunk.input_origin.as_ref(),
),
),
)
{
continue;
}
positions.insert(idx, self.chunks.len());
self.chunks.push(chunk.clone());
self.record_origin(source, origin.clone());
}
self.with_postings_of(prev, &positions)
}
#[must_use]
pub fn with_served_external_from(mut self, stored: &Self, root: &Path) -> Self {
if stored.provider_origins.is_empty() {
return self;
}
let mut gate = super::source_gate::SourceGate::new(root, super::source_gate::SEARCH_TOOL)
.with_origins(&stored.provider_origins);
let mut positions: HashMap<usize, usize> = HashMap::new();
for (idx, chunk) in stored.chunks.iter().enumerate() {
let source = chunk.file_path.as_str();
let Some(origin) = stored.provider_origins.get(source) else {
continue;
};
if !source.contains("://") || !gate.serves(source, &chunk.content) {
continue;
}
positions.insert(idx, self.chunks.len());
self.chunks.push(chunk.clone());
self.record_origin(source, origin.clone());
}
self.with_postings_of(stored, &positions)
}
fn with_postings_of(mut self, prev: &Self, positions: &HashMap<usize, usize>) -> Self {
if positions.is_empty() {
return self;
}
for (term, postings) in &prev.inverted {
let mut carried: Vec<(usize, f64)> = postings
.iter()
.filter_map(|&(idx, weight)| positions.get(&idx).map(|&at| (at, weight)))
.collect();
if carried.is_empty() {
continue;
}
let mut documents = 0usize;
let mut last = None;
for &(at, _) in &carried {
if last != Some(at) {
documents += 1;
last = Some(at);
}
}
*self.doc_freqs.entry(term.clone()).or_insert(0) += documents;
self.inverted
.entry(term.clone())
.or_default()
.append(&mut carried);
}
self.finalize();
self
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::bm25_index::ChunkKind;
use crate::core::content_chunk::ContentChunk;
use crate::core::providers::provenance::{Projection, ProviderOrigin};
use crate::core::providers::provider_trait::ProviderParams;
fn provider_chunk(item: &str, title: &str, body: &str) -> ContentChunk {
let mut chunk = ContentChunk::from_provider(
"tracker",
"issues",
item,
title,
ChunkKind::Issue,
body.to_owned(),
Vec::new(),
None,
);
chunk.origin = Some(ProviderOrigin {
version: 1,
provider: "tracker".into(),
resource: "issues".into(),
binding: "0".repeat(64),
params: ProviderParams::default(),
item_id: item.into(),
item_digest: "1".repeat(64),
projection: Projection::default(),
});
chunk
}
#[test]
fn a_stale_rebuild_carries_admitted_provider_chunks_with_their_origin() {
let _data = crate::core::data_dir::isolated_data_dir();
let _community = crate::core::policy::runtime::TestPolicyOverride::set(None);
let project = tempfile::tempdir().expect("project");
let root = project.path().canonicalize().expect("canonical root");
std::fs::write(
root.join("ledger.rs"),
"pub fn settle() -> u32 {\n 42\n}\n",
)
.expect("source");
let long_body = format!(
"{}\nfinal line names heliotrope",
"filler line\n".repeat(12)
);
let mut written = BM25Index::build_from_directory(&root);
written.ingest_content_chunks([
provider_chunk("1", "Orchidquartz outage", &long_body),
provider_chunk("2", "Plan", "TOP SECRET\nmarlinspike rollout"),
]);
assert_eq!(written.provider_origins.len(), 2);
written.save(&root).expect("persisted");
std::fs::write(
root.join("ledger.rs"),
"pub fn settle() -> u32 {\n let carried = 41;\n carried + 1\n}\n",
)
.expect("changed source");
let rebuilt = BM25Index::load_or_build(&root);
let kept = "tracker://issues/1";
assert!(rebuilt.chunks.iter().any(|c| c.file_path == kept));
assert!(rebuilt.provider_origins.get(kept).is_some());
assert!(
rebuilt
.search("heliotrope", 5)
.iter()
.any(|hit| hit.file_path == kept),
"postings beyond the stored snippet are carried"
);
assert!(
rebuilt
.chunks
.iter()
.all(|c| c.file_path != "tracker://issues/2"),
"a chunk the admission no longer admits is dropped"
);
let reloaded = BM25Index::load(&root).expect("rebuilt index persisted");
assert_eq!(
reloaded.provider_origins.len(),
1,
"origins follow the index"
);
assert!(reloaded.chunks.iter().any(|c| c.file_path == "ledger.rs"));
}
}