Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 3 additions & 30 deletions crates/tracedecay-cli/tests/core_cli_suite/tool_daemon_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -543,21 +543,6 @@ fn configuration_tool_success(
}
}

fn wait_for_daemon_socket(socket_path: &Path) {
let deadline = Instant::now() + Duration::from_secs(10);
common::poll_until(
deadline,
Duration::from_millis(25),
|| UnixStream::connect(socket_path).is_ok().then_some(()),
|| {
format!(
"timed out waiting for daemon socket at {}",
socket_path.display()
)
},
);
}

/// Enroll a project for the native hook capture contract: an enrollment
/// marker binds the project root to a profile shard, and a daemon-issued
/// hook configuration binding is published under that shard's data root so
Expand Down Expand Up @@ -1718,21 +1703,9 @@ fn daemon_socket_is_owner_only() {
let home = TempDir::new().unwrap();
let home_path = canonical_existing_path(home.path());
let socket_path = common::daemon_socket_path(&home_path);
let _ = std::fs::remove_file(&socket_path);
let mut daemon = common::DaemonProcess::new(
tracedecay_command_with_home(&home_path)
.arg("daemon")
.arg("run")
.arg("--socket")
.arg(&socket_path)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("tracedecay daemon should start"),
);

wait_for_daemon_socket(&socket_path);
let mut daemon = common::spawn_tracedecay_daemon_with(&home_path, |command| {
command.arg("--socket").arg(&socket_path);
});
let mode = std::fs::metadata(&socket_path)
.expect("socket metadata")
.permissions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,10 @@ impl PersistedFileEvidenceV1 {
.any(|path| paths.contains(path.as_str()))
}

pub(super) fn target_paths(&self) -> impl Iterator<Item = &str> {
self.target_paths.iter().map(String::as_str)
}

pub(super) fn cross_file_edge_count(&self) -> usize {
self.edges.len()
}
Expand Down
72 changes: 71 additions & 1 deletion crates/tracedecay-code-index/src/production/partitioned_codec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,72 @@ pub(super) struct PartitionedFileEvidenceDescriptorV1 {
/// that carries the file knows to reseal it with identity lineage.
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub(super) explicit_lineage: bool,
/// Negative edge-target filter authenticated against this segment at restore.
/// A missing filter is a format violation, never a reason to omit an edge.
pub(super) target_filter: EvidenceTargetFilterV1,
}

/// Four independent path-hash probes. False positives decode evidence as
/// before; false negatives would suppress required incoming-edge repoints.
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(transparent)]
pub(super) struct EvidenceTargetFilterV1([u64; 4]);

impl EvidenceTargetFilterV1 {
fn path_bits(path: &str) -> [usize; 4] {
let digest = Sha256::digest(path.as_bytes());
std::array::from_fn(|probe| {
usize::from(u16::from_be_bytes([
digest[probe * 2],
digest[probe * 2 + 1],
])) % 256
})
}

fn from_paths<'a>(paths: impl Iterator<Item = &'a str>) -> Self {
let mut bits = [0_u64; 4];
for path in paths {
for bit in Self::path_bits(path) {
bits[bit / 64] |= 1_u64 << (bit % 64);
}
}
Self(bits)
}

fn might_contain(&self, path: &str) -> bool {
Self::path_bits(path)
.into_iter()
.all(|bit| self.0[bit / 64] & (1_u64 << (bit % 64)) != 0)
}
}

impl PartitionedFileEvidenceDescriptorV1 {
pub(super) fn might_target_any<'a>(&self, mut paths: impl Iterator<Item = &'a str>) -> bool {
paths.any(|path| self.target_filter.might_contain(path))
}
}

#[cfg(test)]
mod evidence_target_filter_tests {
use super::EvidenceTargetFilterV1;

#[test]
fn sealed_target_filter_round_trip_has_no_false_negatives() {
let paths = ["src/util.rs", "src/other.rs", "src/sub/leaf.rs"];
let filter = EvidenceTargetFilterV1::from_paths(paths.into_iter());
let bytes = serde_json::to_vec(&filter).expect("encode target filter");
let restored: EvidenceTargetFilterV1 =
serde_json::from_slice(&bytes).expect("decode target filter");
assert_eq!(restored, filter);
for path in paths {
assert!(restored.might_contain(path), "{path} must not be filtered");
}
let empty = EvidenceTargetFilterV1::from_paths(std::iter::empty());
assert!(
paths.into_iter().all(|path| !empty.might_contain(path)),
"empty edge set must safely skip unrelated evidence"
);
}
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -1724,6 +1790,7 @@ pub(super) fn encode_file_evidence_segment_charged(
})?,
decoded_size_bytes,
explicit_lineage: evidence.has_explicit_lineage(),
target_filter: EvidenceTargetFilterV1::from_paths(evidence.target_paths()),
};
Ok((descriptor, bytes))
}
Expand All @@ -1748,7 +1815,10 @@ pub(super) fn decode_file_evidence_segment(
"sealed file evidence decoding failed: {error}"
))
})?;
if evidence.has_explicit_lineage() != descriptor.explicit_lineage || evidence.is_empty() {
if evidence.has_explicit_lineage() != descriptor.explicit_lineage
|| evidence.is_empty()
|| descriptor.target_filter != EvidenceTargetFilterV1::from_paths(evidence.target_paths())
{
return Err(CodeIndexProductionErrorV1::Contract(
"sealed file evidence does not match its descriptor".to_owned(),
));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use super::*;
/// from source rather than migrated. Revisions through eight also predate
/// required clone-body source rows, so the rebuild keeps them from reading as
/// successful empty clone evidence.
pub const SEALED_GENERATION_FORMAT_REVISION_V1: u32 = 21;
pub const SEALED_GENERATION_FORMAT_REVISION_V1: u32 = 22;

/// The typed refusal for a sealed generation this build no longer reads.
pub fn superseded_sealed_generation_revision(revision: u32) -> CodeIndexProductionErrorV1 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,12 @@ fn a_comment_beside_same_named_symbols_decodes_only_what_it_reaches() {
.collect::<String>()
+ UTIL;
let edited = format!("{util}// probe line\n");
let other = "pub struct Other;\nimpl Other { pub fn new() -> Self { Other } }\n";
let other = "pub fn new() -> u32 { 7 }\n";
let callers = (0..4)
.map(|n| {
(
format!("src/caller{n}.rs"),
format!("pub fn caller{n}() {{ let _ = crate::other::Other::new(); }}\n"),
format!("pub fn caller{n}() {{ let _ = crate::other::new(); }}\n"),
)
})
.collect::<Vec<_>>();
Expand All @@ -343,6 +343,8 @@ fn a_comment_beside_same_named_symbols_decodes_only_what_it_reaches() {
callers: &'a [(String, String)],
) -> Vec<(&'a str, &'a str, &'a str)> {
let mut tree = rust_tree(util, MAIN);
tree[0].2 = "pub mod consumer;\npub mod util;\npub mod other;\n";
tree[1].0 = "src/consumer.rs";
tree.push(("src/other.rs", RUST, other));
tree.extend(
callers
Expand Down Expand Up @@ -388,6 +390,28 @@ fn a_comment_beside_same_named_symbols_decodes_only_what_it_reaches() {
.iter()
.map(|(_, digest)| store.segment_reads(digest))
.collect::<Vec<_>>();
let evidence_of = |path: &str| {
let key = snapshot
.iter()
.position(|file| file.logical_path == path)
.expect("parent evidence file");
manifest
.file_evidence
.iter()
.find(|descriptor| descriptor.file_key as usize == key)
.map(|descriptor| descriptor.segment_digest.clone())
.expect("a resolved cross-file call has sealed evidence")
};
let unrelated_evidence = callers
.iter()
.map(|(path, _)| (path.as_str(), evidence_of(path)))
.collect::<Vec<_>>();
let evidence_reads_before = unrelated_evidence
.iter()
.map(|(_, digest)| store.segment_reads(digest))
.collect::<Vec<_>>();
let dependent_evidence = evidence_of("src/consumer.rs");
let dependent_reads_before = store.segment_reads(&dependent_evidence);

let sparse = publish(
&mut incremental,
Expand All @@ -406,6 +430,17 @@ fn a_comment_beside_same_named_symbols_decodes_only_what_it_reaches() {
"{path} names `new` but binds nowhere in the edited file, so its segment stays sealed"
);
}
for ((path, digest), before) in unrelated_evidence.iter().zip(evidence_reads_before) {
assert_eq!(
store.segment_reads(digest),
before,
"{path} has unrelated sealed edges and must not decode its evidence"
);
}
assert!(
store.segment_reads(&dependent_evidence) > dependent_reads_before,
"a caller whose sealed edge lands in the edited file must still be repointed"
);
assert_sparse_matches_cold(
&caller_tree(&util, other, &callers),
&caller_tree(&edited, other, &callers),
Expand Down
13 changes: 10 additions & 3 deletions crates/tracedecay-code-index/src/production/sparse_resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -711,9 +711,16 @@ pub(super) fn resolve_edit(
let parent_key = parent_key_of_path
.get(files[file_index].logical_path())
.ok_or_else(|| contract("a carried file has no parent segment"))?;
let repoints = parent
.file_evidence(*parent_key)?
.is_some_and(|evidence| evidence.targets_any(&edited_paths));
let might_repoint =
parent
.file_evidence_descriptor(*parent_key)
.is_some_and(|descriptor| {
descriptor.might_target_any(edited_paths.iter().copied())
});
let repoints = might_repoint
&& parent
.file_evidence(*parent_key)?
.is_some_and(|evidence| evidence.targets_any(&edited_paths));
if repoints {
files[file_index].binding()?;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ mod retained_parse_canonical_identity;
mod sanitized_intake;
mod sealed_generation_restore;
mod search_chunks;
mod sparse_comment_publication;
mod support;
mod symbol_span_digest;
mod test_attribution;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3171,7 +3171,7 @@ fn partitioned_codec_fixture() -> (
}

const PARTITIONED_FORMAT_STATE_DIGEST: &str =
"sha256:0ba68ca337a4760c0221c1212eec669965f43abdd244f0b21be7f705f7449918";
"sha256:08e91a89b2dd82615897c5df08b74740fc483551f8f8cef1ef57cf96a93c837f";
const PARTITIONED_FORMAT_SEGMENTS: &[(&str, u64)] = &[
(
"sha256:9b175e6aa1ddf6db4138181c0dd1cd522f37381898a58d46e26b1165b613727b",
Expand Down Expand Up @@ -3458,6 +3458,35 @@ fn partitioned_codec_has_stable_bytes_and_round_trips() {
!file_evidence_digests.is_empty(),
"the fixture must seal file evidence"
);
// This revision requires each evidence descriptor's authenticated target
// filter. A missing filter is not a permissive legacy manifest.
let evidence_descriptors = envelope["generation"]["file_evidence"]
.as_array()
.expect("file evidence descriptors");
for descriptor in evidence_descriptors {
assert_eq!(
descriptor["target_filter"].as_array().map(Vec::len),
Some(4),
"each sealed evidence descriptor has four target-filter words"
);
}
let mut filterless = envelope.clone();
filterless["generation"]["file_evidence"]
.as_array_mut()
.expect("file evidence descriptors")
.first_mut()
.expect("fixture has evidence")
.as_object_mut()
.expect("typed evidence descriptor")
.remove("target_filter")
.expect("filter must be present");
let error =
CodeIndexPublishedGenerationV1::partitioned_text_metadata(&reseal_manifest(filterless))
.expect_err("a current-revision manifest without a target filter must be refused");
assert!(
error.to_string().contains("missing field `target_filter`"),
"filterless manifest reached the wrong refusal: {error}"
);
// Buffer address -> its capacity after the last read it served.
let mut file_buffers = BTreeMap::new();
let mut file_evidence_buffers = BTreeMap::new();
Expand Down
Loading
Loading