Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
740052a
fix(sessions): refuse an authoritative zero from a partial generation
ScriptedAlchemy Sep 19, 2026
5e03dfa
fix(daemon): compare version identity per semver build rules
ScriptedAlchemy Sep 19, 2026
cc10394
fix(sessions): retire refused refresh progress instead of retrying
ScriptedAlchemy Sep 19, 2026
8b75523
perf(code-index): scan clone postings once per resumed page
ScriptedAlchemy Sep 19, 2026
e2384db
fix(sessions): retire only refused progress, not a cancelled pass
ScriptedAlchemy Sep 19, 2026
f2ab808
perf(code-index): index clone postings by occurrence for resume replay
ScriptedAlchemy Sep 19, 2026
f77e763
test(global-db): gate analytics append on abandonment, not poll
ScriptedAlchemy Sep 19, 2026
53d7f9d
fix(retention): skip an artifact reclaimed during the scan
ScriptedAlchemy Sep 19, 2026
b130bce
test(daemon): defer the retention plan while the store is busy
ScriptedAlchemy Sep 19, 2026
63c0784
fix(mcp): attribute risky sites by byte span, not line
ScriptedAlchemy Sep 19, 2026
6d8ef97
test(application): keep git auto-maintenance out of the fixture
ScriptedAlchemy Sep 19, 2026
df9f190
fix(extraction): stop bare receivers inventing callers
cursoragent Sep 19, 2026
b05cab2
fix(daemon): mount the published branch worktree's query authority
ScriptedAlchemy Sep 19, 2026
de5f920
fix(mcp): attribute field sites by byte span, not line
ScriptedAlchemy Sep 19, 2026
aab865a
test(runtime): settle the worker before sampling elapsed freshness
ScriptedAlchemy Sep 19, 2026
15f25b3
test(cli): keep the hotpath metrics port out of the quiet-pipeline test
ScriptedAlchemy Sep 19, 2026
b759356
Merge remote-tracking branch 'origin/fix/master-ci-green-3' into curs…
ScriptedAlchemy Sep 19, 2026
3daba38
fix(extraction): keep module scope in self receiver types
ScriptedAlchemy Sep 19, 2026
f060276
fix(code-index): alias module-scoped trait impl methods
ScriptedAlchemy Sep 19, 2026
1ebadc8
chore(code-index): move Rust extraction to extractor.rust.v11
ScriptedAlchemy Sep 19, 2026
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
8 changes: 8 additions & 0 deletions crates/tracedecay-application/src/git_intelligence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1762,6 +1762,14 @@ mod tests {
"user.email=fixture@example.com",
"-c",
"commit.gpgsign=false",
// `git commit` spawns a detached `git maintenance run --auto`
// that holds `.git/objects/maintenance.lock` after the commit
// returns; the byte-identical snapshot must not see it appear
// or vanish between its two walks.
"-c",
"maintenance.auto=false",
"-c",
"gc.auto=0",
])
.args(args)
.current_dir(self.path())
Expand Down
4 changes: 4 additions & 0 deletions crates/tracedecay-cli/tests/core_cli_suite/cli_boundary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ fn shipped_binary_stops_quietly_when_a_pipeline_reader_exits() {
let output = Command::new("sh")
.args(["-c", r#""$TRACEDECAY_BIN" tool | head -n 4"#])
.env("TRACEDECAY_BIN", env!("CARGO_BIN_EXE_tracedecay"))
// A hotpath-enabled binary binds its metrics port on start; when a
// sibling test's daemon already holds it, the bind failure lands on
// stderr and breaks the quiet-pipeline assertion below.
.env("HOTPATH_METRICS_SERVER_OFF", "true")
.output()
.expect("tracedecay tool pipeline should run");

Expand Down
124 changes: 98 additions & 26 deletions crates/tracedecay-code-extraction/src/rust_extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ struct ShadowedCallNames {
}

/// Receiver bindings whose type the function body states outright: typed
/// parameters, typed `let`s, and `let`s initialised by a struct literal
/// (`T { .. }`, possibly behind `?`). A dotted
/// parameters, typed `let`s, `let`s initialised by a struct literal
/// (`T { .. }`, possibly behind `?`), and `self` in a method. A dotted
/// call on such a binding also names the method by its type
/// (`builder.build()` → `ignore::WalkBuilder::build`), which is the only form
/// the resolver can bind across files. Method calls and constructor-like names
Expand Down Expand Up @@ -1579,24 +1579,11 @@ impl RustExtractor {
column: child.start_position().column as u32,
file_path: state.file_path.clone(),
});
// For dot-calls (e.g. `instance.method()`), also emit
// a ref with just the method name so the resolver can
// match it against impl method definitions.
if let Some(method_name) = callee_name.rsplit('.').next()
&& method_name != callee_name
{
state.unresolved_refs.push(UnresolvedRef {
from_node_id: fn_node_id.to_string(),
reference_name: method_name.to_string(),
reference_kind: EdgeKind::Calls,
line: child.start_position().row as u32,
column: child.start_position().column as u32,
file_path: state.file_path.clone(),
});
}
// A dotted call on a binding with a stated type also
// names the method through its type, the only form
// that binds across files.
// The simple name of a dotted call is not itself a call.
// `items.push()` must not bind a same-file `fn push`.
// Only a stated receiver type names the method
// (`Rows::len`), which is also the form that binds
// across files.
if let Some(typed_method) =
Self::typed_receiver_method(state, callee, receivers)
{
Expand Down Expand Up @@ -1664,13 +1651,85 @@ impl RustExtractor {
}
let value = callee.child_by_field_name("value")?;
let field = callee.child_by_field_name("field")?;
if value.kind() != "identifier" || field.kind() != "field_identifier" {
if field.kind() != "field_identifier" {
return None;
}
let type_path = receivers.type_of(state.node_text(value))?;
// `self` is its own token, not an identifier. Both name a binding.
let receiver_name = match value.kind() {
"identifier" | "self" => state.node_text(value),
_ => return None,
};
let type_path = receivers.type_of(receiver_name)?;
Some(format!("{type_path}::{}", state.node_text(field)))
}

/// The type `self` names in the enclosing impl or trait, carrying the
/// enclosing module path.
///
/// Trait impls store `<Type as Trait>` so the method keeps a UFCS name.
/// `self` still names `Type`, the path a call site writes and the alias
/// same-file resolution binds. Same-file resolution keys a definition by
/// its file-relative qualified name, so an impl inside `mod inner` has to
/// name `inner::Type::method` or the call binds nothing.
fn enclosing_receiver_type(state: &ExtractionState<'_>) -> Option<String> {
let owner = state
.node_stack
.iter()
.rposition(|(_, id)| id.starts_with("impl:") || id.starts_with("trait:"))?;
let (name, id) = &state.node_stack[owner];
let type_name = if id.starts_with("impl:") {
Self::impl_owner_type_name(name)
} else {
name.as_str()
};
if type_name.is_empty()
|| type_name == "Self"
|| type_name == "<unknown>"
|| type_name == "<anonymous>"
{
return None;
}
// Frame 0 is the file root, which the qualified name drops.
let mut path = state
.node_stack
.get(1..owner)
.unwrap_or_default()
.iter()
.map(|(segment, _)| segment.as_str())
.collect::<Vec<_>>();
path.push(type_name);
Some(path.join("::"))
}

/// The self type inside a stored impl owner name.
///
/// A trait impl stores `<Type as Trait>`, and `Type` can itself be a
/// projection (`<Foo as Assoc>::Item`), so the delimiter is the ` as ` at
/// depth zero inside the wrapper, not the first one in the string.
fn impl_owner_type_name(owner: &str) -> &str {
let Some(inner) = owner.strip_prefix('<') else {
return owner;
};
let mut depth = 0_i32;
for (index, character) in inner.char_indices() {
match character {
'<' => depth += 1,
'>' => {
if depth == 0 {
break;
}
depth -= 1;
}
_ => {
if depth == 0 && inner[index..].starts_with(" as ") {
return inner[..index].trim();
}
}
}
}
owner
}

/// Records every binding the function introduces with the type it states,
/// or `None` for a binding whose type the syntax does not state (pattern
/// destructuring, `if let`, `match` arms, closure parameters, `for`).
Expand All @@ -1681,6 +1740,11 @@ impl RustExtractor {
receivers: &mut ReceiverTypes,
) {
match node.kind() {
"self_parameter" => {
if let Some(type_path) = Self::enclosing_receiver_type(state) {
receivers.record("self".to_owned(), Some(type_path));
}
}
"parameter" => {
if let Some(pattern) = node.child_by_field_name("pattern") {
let type_path = node
Expand Down Expand Up @@ -1737,15 +1801,15 @@ impl RustExtractor {
}
}

/// A bare identifier pattern takes `type_path`; every identifier inside any
/// other pattern is bound with an unknown type.
/// A bare identifier or `self` pattern takes `type_path`; every identifier
/// inside any other pattern is bound with an unknown type.
fn record_receiver_pattern(
state: &ExtractionState<'_>,
pattern: TsNode<'_>,
type_path: Option<String>,
receivers: &mut ReceiverTypes,
) {
if pattern.kind() == "identifier" {
if pattern.kind() == "identifier" || pattern.kind() == "self" {
receivers.record(state.node_text(pattern).to_owned(), type_path);
return;
}
Expand All @@ -1763,10 +1827,18 @@ impl RustExtractor {
/// The nominal type path a type annotation names, seen through references,
/// generic arguments, and `dyn`/`impl` trait objects; `None` for tuples,
/// slices, function pointers, and anything else without one nominal head.
/// `Self` is the enclosing impl or trait type when one is on the stack.
fn stated_type_path(state: &ExtractionState<'_>, ty: TsNode<'_>) -> Option<String> {
match ty.kind() {
"type_identifier" | "scoped_type_identifier" => {
Some(state.node_text(ty).to_owned()).filter(|path| path != "Self")
let path = state.node_text(ty);
if path == "Self" {
// `Self` in an annotation is the enclosing impl or trait,
// not a type the file declared under that name.
Self::enclosing_receiver_type(state)
} else {
Some(path.to_owned())
}
}
"reference_type" | "generic_type" => ty
.child_by_field_name("type")
Expand Down
165 changes: 162 additions & 3 deletions crates/tracedecay-code-extraction/tests/main/rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1131,10 +1131,169 @@ fn use_foo() {
ref_names.contains(&"Foo::new"),
"expected Foo::new call, got: {ref_names:?}"
);
// f.bar() should also produce "bar" (method-name hint).
assert!(
ref_names.contains(&"bar"),
"expected 'bar' method-name ref from f.bar(), got: {ref_names:?}"
ref_names.contains(&"f.bar"),
"the receiver-dotted form remains: {ref_names:?}"
);
// `Foo::new()` does not state that `f` is Foo, and `bar` is the method of
// an impl in this file. Emitting the simple name would invent that caller.
assert!(
!ref_names.contains(&"bar"),
"untyped f.bar() must not emit a bare method name: {ref_names:?}"
);
assert!(
!ref_names.contains(&"Foo::bar"),
"constructor-like Foo::new() must not fabricate a Foo receiver: {ref_names:?}"
);
}

#[test]
fn bare_receiver_calls_name_self_without_the_method_simple_name() {
let source = r#"
fn prepare(value: i32) {}
fn push(value: i32) {}

struct Rows;
impl Rows {
fn len(&self) -> usize { 0 }
fn measure(&self) -> usize { self.len() }
fn via_explicit(self: &Self) -> usize { self.len() }
}
trait Span {}
impl Span for Rows {
fn wide(&self) -> usize { self.len() }
}

fn caller(items: Vec<i32>, rows: Rows) {
let foreign = make();
foreign.prepare(1);
items.push(1);
prepare(1);
push(1);
rows.len();
}
fn make() -> Vec<i32> { Vec::new() }
"#;
let result = RustExtractor.extract("src/lib.rs", source);
assert!(result.errors.is_empty(), "{:?}", result.errors);

let from = |name: &str| {
let function = result
.nodes
.iter()
.find(|node| {
matches!(node.kind, NodeKind::Function | NodeKind::Method) && node.name == name
})
.unwrap_or_else(|| panic!("{name} is extracted"));
result
.unresolved_refs
.iter()
.filter(|reference| {
reference.reference_kind == EdgeKind::Calls && reference.from_node_id == function.id
})
.map(|reference| reference.reference_name.as_str())
.collect::<Vec<_>>()
};

let measure = from("measure");
assert!(
measure.contains(&"self.len") && measure.contains(&"Rows::len"),
"{measure:?}"
);
assert!(
!measure.contains(&"len"),
"self.len() must not emit the bare method name: {measure:?}"
);

let via_explicit = from("via_explicit");
assert!(
via_explicit.contains(&"Rows::len"),
"self: &Self still names the enclosing type: {via_explicit:?}"
);

let wide = from("wide");
assert!(
wide.contains(&"Rows::len"),
"self inside `impl Span for Rows` names Rows, not Span: {wide:?}"
);
assert!(!wide.contains(&"Span::len"), "{wide:?}");

let caller = from("caller");
assert!(caller.contains(&"prepare"), "{caller:?}");
assert!(caller.contains(&"push"), "{caller:?}");
assert!(caller.contains(&"Rows::len"), "{caller:?}");
assert!(caller.contains(&"Vec::push"), "{caller:?}");
assert!(caller.contains(&"foreign.prepare"), "{caller:?}");
assert!(caller.contains(&"items.push"), "{caller:?}");
assert_eq!(
caller.iter().filter(|name| **name == "prepare").count(),
1,
"foreign.prepare() invented a second prepare call: {caller:?}"
);
assert_eq!(
caller.iter().filter(|name| **name == "push").count(),
1,
"items.push() invented a second push call: {caller:?}"
);
}

#[test]
fn self_receiver_names_carry_module_scope_and_the_outer_as_delimiter() {
let source = r#"
mod inner {
pub struct Rows;
impl Rows {
fn len(&self) -> usize { 0 }
fn measure(&self) -> usize { self.len() }
}
trait Wide { fn wide(&self) -> usize; }
impl Wide for Rows {
fn wide(&self) -> usize { self.len() }
}
}
struct Foo;
trait Assoc { type Item; }
trait Local { fn span(&self) -> usize; }
impl Local for <Foo as Assoc>::Item {
fn span(&self) -> usize { self.len() }
}
"#;
let result = RustExtractor.extract("src/lib.rs", source);
assert!(result.errors.is_empty(), "{:?}", result.errors);

let from = |qualified: &str| {
let function = result
.nodes
.iter()
.find(|node| {
matches!(node.kind, NodeKind::Function | NodeKind::Method)
&& node.qualified_name == qualified
})
.unwrap_or_else(|| panic!("{qualified} is extracted"));
result
.unresolved_refs
.iter()
.filter(|reference| {
reference.reference_kind == EdgeKind::Calls && reference.from_node_id == function.id
})
.map(|reference| reference.reference_name.as_str())
.collect::<Vec<_>>()
};

let measure = from("src/lib.rs::inner::Rows::measure");
assert!(
measure.contains(&"inner::Rows::len"),
"self inside `mod inner` names the module-scoped type: {measure:?}"
);
let wide = from("src/lib.rs::inner::<Rows as Wide>::wide");
assert!(
wide.contains(&"inner::Rows::len"),
"a trait impl in a module keeps the module path: {wide:?}"
);
let span = from("src/lib.rs::<<Foo as Assoc>::Item as Local>::span");
assert!(
span.contains(&"<Foo as Assoc>::Item::len"),
"a projected self type splits at the outer `as`: {span:?}"
);
}

Expand Down
Loading
Loading