Skip to content

Commit a2e9fb6

Browse files
Merge branch 'simplify/v2-cleanup' into codex/tracedecay-total-redesign-plan
2 parents 7aa2fc0 + 5fed296 commit a2e9fb6

17 files changed

Lines changed: 296 additions & 261 deletions

File tree

crates/tracedecay-capture/src/claude/mod.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
mod canonical;
22

3-
use sha2::{Digest, Sha256};
3+
use tracedecay_domain::canonical_text::canonical_framed_sha256;
44

55
pub use canonical::{normalize, stable_record_id};
66

@@ -26,14 +26,3 @@ pub fn observation_source_id(native_transcript_id: &[u8]) -> String {
2626
canonical_framed_sha256(OBSERVATION_SOURCE_ID_DOMAIN, &[native_transcript_id])
2727
)
2828
}
29-
30-
fn canonical_framed_sha256(domain: &[u8], parts: &[&[u8]]) -> String {
31-
let mut hasher = Sha256::new();
32-
hasher.update((domain.len() as u64).to_be_bytes());
33-
hasher.update(domain);
34-
for part in parts {
35-
hasher.update((part.len() as u64).to_be_bytes());
36-
hasher.update(part);
37-
}
38-
hex::encode(hasher.finalize())
39-
}

crates/tracedecay-capture/src/kiro.rs

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use serde_json::{Map, Value};
2-
use sha2::{Digest, Sha256};
2+
use tracedecay_domain::canonical_text::canonical_framed_sha256;
33

44
const PROVIDER: &str = "kiro";
55
const DELIMITED_NATIVE_MESSAGE_ID_DOMAIN: &[u8] = b"tracedecay.kiro-delimited-native-message.v2";
@@ -87,14 +87,3 @@ pub fn stable_message_id(
8787
);
8888
format!("{DERIVED_MESSAGE_ID_PREFIX}{digest}")
8989
}
90-
91-
fn canonical_framed_sha256(domain: &[u8], parts: &[&[u8]]) -> String {
92-
let mut hasher = Sha256::new();
93-
hasher.update((domain.len() as u64).to_be_bytes());
94-
hasher.update(domain);
95-
for part in parts {
96-
hasher.update((part.len() as u64).to_be_bytes());
97-
hasher.update(part);
98-
}
99-
hex::encode(hasher.finalize())
100-
}

crates/tracedecay-code-extraction/src/traversal.rs

Lines changed: 4 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,16 @@ use tree_sitter::Node as TsNode;
66
/// This helper is intentionally language-agnostic: callers provide the raw
77
/// `kind()` string they expect, and the traversal checks direct children in
88
/// source order without filtering to named nodes.
9-
#[allow(dead_code)]
109
pub(crate) fn has_direct_child_kind(node: TsNode<'_>, kind: &str) -> bool {
1110
find_direct_child_by_kind(node, kind).is_some()
1211
}
1312

14-
/// Visits each direct child in source order.
15-
///
16-
/// This keeps the raw tree-sitter cursor loop in one place so extractors that
17-
/// only need to recurse into direct children can share the traversal behavior.
18-
#[allow(dead_code)]
19-
pub(crate) fn visit_children<'tree>(node: TsNode<'tree>, mut visit: impl FnMut(TsNode<'tree>)) {
20-
visit_children_while(node, |child| {
21-
visit(child);
22-
true
23-
});
24-
}
25-
2613
/// Returns the first direct child whose tree-sitter kind exactly matches
2714
/// `kind`.
2815
///
2916
/// The match is an exact `Node::kind()` string comparison. Both named and
3017
/// anonymous children participate so extractor migrations preserve existing
3118
/// behavior.
32-
#[allow(dead_code)]
3319
pub(crate) fn find_direct_child_by_kind<'tree>(
3420
node: TsNode<'tree>,
3521
kind: &str,
@@ -51,7 +37,6 @@ pub(crate) fn find_direct_child_by_kind<'tree>(
5137
/// Descendants are visited with a pre-order depth-first traversal over all
5238
/// children so callers can replace the duplicated recursive extractor helpers
5339
/// without changing search order.
54-
#[allow(dead_code)]
5540
pub(crate) fn find_descendant_by_kind<'tree>(
5641
node: TsNode<'tree>,
5742
kind: &str,
@@ -91,7 +76,8 @@ fn visit_children_while<'tree>(node: TsNode<'tree>, mut visit: impl FnMut(TsNode
9176
#[allow(clippy::expect_used)]
9277
mod tests {
9378
use super::{
94-
find_descendant_by_kind, find_direct_child_by_kind, has_direct_child_kind, visit_children,
79+
find_descendant_by_kind, find_direct_child_by_kind, has_direct_child_kind,
80+
visit_children_while,
9581
};
9682
use crate::ts_provider;
9783
use tree_sitter::{Node as TsNode, Parser};
@@ -140,8 +126,9 @@ mod tests {
140126
let function = parse_c_function("int answer(void) { return 42; }");
141127
let mut child_kinds = Vec::new();
142128

143-
visit_children(function, |child| {
129+
visit_children_while(function, |child| {
144130
child_kinds.push(child.kind().to_owned());
131+
true
145132
});
146133

147134
assert_eq!(

crates/tracedecay-domain/src/canonical_text.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
//! Only the predicate is shared — never the error, so no contract's
99
//! accept/reject reporting changes by reusing it.
1010
11+
use sha2::{Digest, Sha256};
12+
1113
use crate::research::DomainError;
1214

1315
/// Byte bound shared by canonical identities and labels across the contracts.
@@ -79,6 +81,26 @@ pub fn encode_tagged_lowercase_hex(tag: &str, bytes: &[u8]) -> String {
7981
encoded
8082
}
8183

84+
/// Length-prefixed SHA-256 over a domain separator and an ordered list of
85+
/// parts, encoded as lowercase hex.
86+
///
87+
/// Every frame — the domain tag included — is preceded by its big-endian
88+
/// `u64` byte length, so no two different splits of the same concatenated
89+
/// bytes can collide. This is an identity primitive: derived ids already
90+
/// written to disk depend on the exact framing, so the byte layout must never
91+
/// change.
92+
#[must_use]
93+
pub fn canonical_framed_sha256(domain: &[u8], parts: &[&[u8]]) -> String {
94+
let mut hasher = Sha256::new();
95+
hasher.update((domain.len() as u64).to_be_bytes());
96+
hasher.update(domain);
97+
for part in parts {
98+
hasher.update((part.len() as u64).to_be_bytes());
99+
hasher.update(part);
100+
}
101+
encode_lowercase_hex(&hasher.finalize())
102+
}
103+
82104
/// Canonical bounded string that reports an empty value distinctly from a
83105
/// non-canonical one.
84106
///

crates/tracedecay-migrate/src/hermes/resolution.rs

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,34 +6,22 @@ use std::path::{Path, PathBuf};
66
use super::copy::{MIGRATION_QUERY_PAGE_ROWS, ensure_materialized_row_room, table_columns};
77
use crate::root_seam::global_db::RegisteredGlobalDb;
88
use tracedecay_runtime_core::db::engine::{QueryExecutor, params};
9+
use tracedecay_runtime_core::path_safety::{
10+
canonicalize_existing_prefix, canonicalize_path_or_existing_parent,
11+
};
912

1013
pub struct ResolvedTargetProject {
1114
pub root: PathBuf,
1215
pub registry_project_id: Option<String>,
1316
pub user_scope: bool,
1417
}
1518

19+
/// Compares two paths after canonicalizing the deepest existing ancestor and
20+
/// reattaching any missing tail. This preserves OS aliases such as macOS
21+
/// `/var` -> `/private/var` even after the final project directory was moved
22+
/// or a symlink alias was removed.
1623
pub fn same_path(left: &Path, right: &Path) -> bool {
17-
canonicalize_with_missing_tail(left).unwrap_or_else(|| left.to_path_buf())
18-
== canonicalize_with_missing_tail(right).unwrap_or_else(|| right.to_path_buf())
19-
}
20-
21-
/// Canonicalizes the deepest existing ancestor and reattaches a missing tail.
22-
/// This preserves OS aliases such as macOS `/var` -> `/private/var` even after
23-
/// the final project directory was moved or a symlink alias was removed.
24-
fn canonicalize_with_missing_tail(path: &Path) -> Option<PathBuf> {
25-
let mut ancestor = path;
26-
let mut tail = Vec::new();
27-
loop {
28-
if let Ok(mut canonical) = ancestor.canonicalize() {
29-
for component in tail.iter().rev() {
30-
canonical.push(component);
31-
}
32-
return Some(canonical);
33-
}
34-
tail.push(ancestor.file_name()?.to_os_string());
35-
ancestor = ancestor.parent()?;
36-
}
24+
canonicalize_path_or_existing_parent(left) == canonicalize_path_or_existing_parent(right)
3725
}
3826

3927
fn real_project_root(
@@ -127,7 +115,7 @@ async fn resolve_project_candidate(
127115
return Ok(None);
128116
}
129117

130-
let canonical_candidate = canonicalize_with_missing_tail(candidate);
118+
let canonical_candidate = canonicalize_existing_prefix(candidate);
131119
let context = if let Some(registry) = registry {
132120
let direct = registry
133121
.project_registry_context_by_alias(candidate)

crates/tracedecay-migrate/src/profile_identity.rs

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::{Path, PathBuf};
33
use serde::{Deserialize, Serialize};
44
use tracedecay_domain::{BrainId, UserProfileId};
55
use tracedecay_runtime_core::errors::{Result, TraceDecayError};
6+
use tracedecay_runtime_core::path_safety::canonicalize_existing_prefix;
67
use tracedecay_runtime_core::storage::PROFILE_IDENTITY_FILENAME;
78

89
const PROFILE_IDENTITY_SCHEMA_VERSION: u32 = 1;
@@ -157,6 +158,15 @@ fn random_identity(prefix: &str) -> Result<String> {
157158
Ok(format!("{prefix}.{}", hex::encode(bytes)))
158159
}
159160

161+
/// Absolutizes `path` and canonicalizes through its deepest existing
162+
/// ancestor.
163+
///
164+
/// Known divergence, deliberately preserved: the daemon's identically-named
165+
/// resolver additionally applies
166+
/// [`tracedecay_runtime_core::path_safety::collapse_relative_components`] to
167+
/// the result. This one must not — the identity strings it produces are
168+
/// already written into migrated profile records, and collapsing `.`/`..`
169+
/// here would rewrite them. Only the canonicalization algorithm is shared.
160170
fn canonical_identity_path(path: &Path) -> Result<PathBuf> {
161171
let absolute = if path.is_absolute() {
162172
path.to_path_buf()
@@ -165,29 +175,9 @@ fn canonical_identity_path(path: &Path) -> Result<PathBuf> {
165175
.map_err(|error| profile_identity_io("resolve", path, &error))?
166176
.join(path)
167177
};
168-
if let Ok(canonical) = absolute.canonicalize() {
169-
return Ok(canonical);
170-
}
171-
let mut suffix = Vec::new();
172-
let mut existing = absolute.as_path();
173-
while !existing.exists() {
174-
let Some(name) = existing.file_name() else {
175-
return Err(TraceDecayError::Config {
176-
message: format!("failed to resolve identity path '{}'", path.display()),
177-
});
178-
};
179-
suffix.push(name.to_os_string());
180-
existing = existing.parent().ok_or_else(|| TraceDecayError::Config {
181-
message: format!("failed to resolve identity path '{}'", path.display()),
182-
})?;
183-
}
184-
let mut canonical = existing
185-
.canonicalize()
186-
.map_err(|error| profile_identity_io("canonicalize", existing, &error))?;
187-
for component in suffix.iter().rev() {
188-
canonical.push(component);
189-
}
190-
Ok(canonical)
178+
canonicalize_existing_prefix(&absolute).ok_or_else(|| TraceDecayError::Config {
179+
message: format!("failed to resolve identity path '{}'", path.display()),
180+
})
191181
}
192182

193183
fn invalid_identity(path: &Path, field: &str, error: impl std::fmt::Display) -> TraceDecayError {

crates/tracedecay-runtime-core/src/config.rs

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -112,28 +112,7 @@ fn canonicalize_data_dir(path: PathBuf) -> PathBuf {
112112
if !path.is_absolute() {
113113
return path;
114114
}
115-
canonicalize_path_or_existing_parent(&path)
116-
}
117-
118-
fn canonicalize_path_or_existing_parent(path: &Path) -> PathBuf {
119-
if let Ok(canonical) = path.canonicalize() {
120-
return canonical;
121-
}
122-
123-
let mut current = path;
124-
let mut missing_suffix = PathBuf::new();
125-
while let Some(name) = current.file_name() {
126-
missing_suffix = Path::new(name).join(missing_suffix);
127-
let Some(parent) = current.parent() else {
128-
break;
129-
};
130-
current = parent;
131-
if let Ok(canonical_parent) = current.canonicalize() {
132-
return canonical_parent.join(missing_suffix);
133-
}
134-
}
135-
136-
path.to_path_buf()
115+
crate::path_safety::canonicalize_path_or_existing_parent(&path)
137116
}
138117

139118
/// Walks up from `start` looking for the nearest ancestor that hosts an

crates/tracedecay-runtime-core/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ pub mod lifecycle_lease;
7171
pub mod memory;
7272
pub mod open_store_holders;
7373
pub mod os_str_bytes;
74+
pub mod path_safety;
7475
pub mod path_scope;
7576
pub mod privacy;
7677
pub mod project_registry;

0 commit comments

Comments
 (0)