Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
7bd29d6
fix(extraction): bind a unique trait bound as the callee
cursoragent Sep 19, 2026
218ae5c
fix(code-index): stamp continuations before the seat looks idle
cursoragent Sep 19, 2026
6961c5d
Merge remote-tracking branch 'origin/master' into cursor/trait-call-c…
ScriptedAlchemy Sep 19, 2026
7b61b4b
Merge remote-tracking branch 'origin/cursor/false-edge-bare-receiver-…
ScriptedAlchemy Sep 19, 2026
8b31596
Merge remote-tracking branch 'origin/master' into cursor/serving-seat…
ScriptedAlchemy Sep 19, 2026
543b6e2
style(session-temporal): keep the test module last and rustfmt master
ScriptedAlchemy Sep 19, 2026
250e318
chore(code-index): move Rust extraction to extractor.rust.v12
ScriptedAlchemy Sep 19, 2026
e3b140e
style(session-temporal): keep the test module last and rustfmt master
ScriptedAlchemy Sep 19, 2026
fea4c18
style: drop the commit-message prose the batch-c fixes inlined
ScriptedAlchemy Sep 19, 2026
643d332
chore: merge PR #1834 (cursor/trait-call-callee-8fe0)
ScriptedAlchemy Sep 19, 2026
0d51ad4
chore: merge PR #1836 (cursor/serving-seat-failure-ceiling-3ef6)
ScriptedAlchemy Sep 19, 2026
c107086
chore: merge fix/batch-c-deslop (fea4c18a39)
ScriptedAlchemy Sep 19, 2026
55b8aa6
chore: merge origin/ci/pr-batch-c (2c27b4963b)
ScriptedAlchemy Sep 19, 2026
fa8eeee
test(code-index): assert the faulted pointer without following symlinks
ScriptedAlchemy Sep 19, 2026
a2021dd
refactor(contracts): build the runtime-mounting refusal in one place
ScriptedAlchemy Sep 19, 2026
70a1f03
style: cut the comment sermons this batch wrote
ScriptedAlchemy Sep 19, 2026
f44c47e
chore: merge origin/ci/pr-batch-c (44eacdb6e4)
ScriptedAlchemy Sep 19, 2026
acd448b
chore: merge origin/ci/pr-batch-c (f0a144a2f5)
ScriptedAlchemy Sep 19, 2026
a724f58
chore: merge origin/master (51990b1749, #1848 #1859)
ScriptedAlchemy Sep 19, 2026
735bfbd
fix(retention): one contract for a missing scope root
ScriptedAlchemy Sep 19, 2026
f7c57ff
chore: merge fix/batch-d-missing-store-contract
ScriptedAlchemy Sep 19, 2026
8050bef
chore: merge origin/master (a31e7ad75c, #1861)
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
11 changes: 0 additions & 11 deletions crates/tracedecay-agent-hosts/src/agents/codex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,17 +92,6 @@ impl AgentIntegration for CodexIntegration {
// Core apply drives `codex plugin add` when the host CLI is present.
// When it is not, stop with the same backtick remediation preflight
// uses so operators (and lifecycle tests) can activate natively.
//
// `Ready` here is a promise that Core apply can complete, so it must
// not be returned when no `codex` resolves. Returning it anyway opens
// a component transaction that can only die in activation with
// `HostCliUnavailable`; the rollback leaves a `RolledBack` journal
// whose registration backup pins `config.toml` and the versioned
// plugin cache as they were *before* the operator runs the printed
// `codex plugin add`. The next lifecycle command starts with
// `recover_host`, replays that stale rollback over the now-remediated
// host, and refuses with `StalePreview` -- making the remediation this
// very error prints impossible to follow.
if plugin_registry::require_codex_plugin_cli().is_err() {
let marketplace_name = codex_exact_personal_marketplace_name(&ctx.home)
.ok()
Expand Down
256 changes: 253 additions & 3 deletions crates/tracedecay-code-extraction/src/rust_extractor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,70 @@ impl ReceiverTypes {
}
}

/// One type parameter's trait bounds, as a method owner.
///
/// `T: Processor` makes `value.process()` the callee `Processor::process`.
/// Two bounds, or a bound the syntax does not name, stay unresolved so the
/// call is not attached to both traits. This does not rename `self`: #1814
/// still records that binding from the enclosing type.
enum ParamBound {
Unbound,
Unique(String),
Ambiguous,
}

#[derive(Default)]
struct TraitBounds {
parameters: BTreeMap<String, ParamBound>,
}

struct BoundClause {
paths: Vec<String>,
ambiguous: bool,
}

impl TraitBounds {
fn declare(&mut self, name: String) {
self.parameters.insert(name, ParamBound::Unbound);
}

fn knows(&self, name: &str) -> bool {
self.parameters.contains_key(name)
}

fn constrain(&mut self, name: &str, clause: BoundClause) {
if clause.paths.is_empty() && !clause.ambiguous {
return;
}
let Some(slot) = self.parameters.get_mut(name) else {
return;
};
if clause.ambiguous || clause.paths.len() != 1 {
*slot = ParamBound::Ambiguous;
return;
}
let Some(path) = clause.paths.into_iter().next() else {
*slot = ParamBound::Ambiguous;
return;
};
match slot {
ParamBound::Unbound => *slot = ParamBound::Unique(path),
ParamBound::Unique(existing) if existing == &path => {}
ParamBound::Unique(_) | ParamBound::Ambiguous => *slot = ParamBound::Ambiguous,
}
}

/// Replace a written type-parameter name with its unique trait. Any other
/// path, including the enclosing type recorded for `self`, is unchanged.
fn resolve(&self, path: String) -> Option<String> {
match self.parameters.get(path.as_str()) {
Some(ParamBound::Unique(bound)) => Some(bound.clone()),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Resolve methods through supertraits before naming the callee

A unique bound does not imply that the bound itself owns every callable method. For valid Rust such as trait Derived: Base {} with fn f<T: Derived>(value: T) { value.process() }, where process is declared by Base, this rewrites the receiver to Derived and emits Derived::process; resolve_file_references only looks for that qualified name, so it cannot bind the actual Base::process symbol. Account for inherited trait methods, or abstain when ownership cannot be established from the bound alone.

Useful? React with 👍 / 👎.

Some(ParamBound::Ambiguous) => None,
Some(ParamBound::Unbound) | None => Some(path),
}
}
}

/// Internal state used during AST traversal.
///
/// Borrows the caller's source for the lifetime of the walk: copying the
Expand Down Expand Up @@ -1738,6 +1802,17 @@ impl RustExtractor {
node: TsNode<'_>,
function: TsNode<'_>,
receivers: &mut ReceiverTypes,
) {
let bounds = Self::trait_bounds_for(state, function);
Self::collect_receiver_bindings(state, node, function, receivers, &bounds);
}

fn collect_receiver_bindings(
state: &ExtractionState<'_>,
node: TsNode<'_>,
function: TsNode<'_>,
receivers: &mut ReceiverTypes,
bounds: &TraitBounds,
) {
match node.kind() {
"self_parameter" => {
Expand All @@ -1749,14 +1824,14 @@ impl RustExtractor {
if let Some(pattern) = node.child_by_field_name("pattern") {
let type_path = node
.child_by_field_name("type")
.and_then(|ty| Self::stated_type_path(state, ty));
.and_then(|ty| Self::receiver_type_path(state, ty, bounds));
Self::record_receiver_pattern(state, pattern, type_path, receivers);
}
}
"let_declaration" => {
if let Some(pattern) = node.child_by_field_name("pattern") {
let type_path = match node.child_by_field_name("type") {
Some(ty) => Self::stated_type_path(state, ty),
Some(ty) => Self::receiver_type_path(state, ty, bounds),
None => node
.child_by_field_name("value")
.and_then(|value| Self::stated_initializer_type_path(state, value)),
Expand Down Expand Up @@ -1793,7 +1868,7 @@ impl RustExtractor {
let mut cursor = node.walk();
if cursor.goto_first_child() {
loop {
Self::collect_receiver_types(state, cursor.node(), function, receivers);
Self::collect_receiver_bindings(state, cursor.node(), function, receivers, bounds);
if !cursor.goto_next_sibling() {
break;
}
Expand Down Expand Up @@ -1846,10 +1921,185 @@ impl RustExtractor {
"dynamic_type" | "abstract_type" => ty
.child_by_field_name("trait")
.and_then(|inner| Self::stated_type_path(state, inner)),
"higher_ranked_trait_bound" => ty
.child_by_field_name("type")
.and_then(|inner| Self::stated_type_path(state, inner)),
// `impl Trait + 'a` still names that trait. Two nominals do not.
"bounded_type" => Self::unique_sum_type_path(state, ty),
_ => None,
}
}

/// A parameter type, with a type parameter replaced by its unique trait
/// bound. `Self` is left as #1814 mapped it: the enclosing type, not the
/// trait the parameter happens to implement.
fn receiver_type_path(
state: &ExtractionState<'_>,
ty: TsNode<'_>,
bounds: &TraitBounds,
) -> Option<String> {
if Self::annotation_is_self(state, ty) {
return Self::enclosing_receiver_type(state);
}
bounds.resolve(Self::stated_type_path(state, ty)?)
}

fn annotation_is_self(state: &ExtractionState<'_>, ty: TsNode<'_>) -> bool {
match ty.kind() {
"type_identifier" => state.node_text(ty) == "Self",
"reference_type" => ty
.child_by_field_name("type")
.is_some_and(|inner| Self::annotation_is_self(state, inner)),
_ => false,
}
}

fn unique_sum_type_path(state: &ExtractionState<'_>, ty: TsNode<'_>) -> Option<String> {
let mut found = None;
let mut cursor = ty.walk();
if !cursor.goto_first_child() {
return None;
}
loop {
let child = cursor.node();
if child.is_named() {
let path = match child.kind() {
"lifetime" | "use_bounds" => None,
"bounded_type" => Self::unique_sum_type_path(state, child),
_ => Self::stated_type_path(state, child),
};
match path {
None if matches!(child.kind(), "lifetime" | "use_bounds") => {}
None => return None,
Some(path) => {
if found.replace(path).is_some() {
return None;
}
}
}
}
if !cursor.goto_next_sibling() {
break;
}
}
found
}

fn trait_bounds_for(state: &ExtractionState<'_>, function: TsNode<'_>) -> TraitBounds {
let mut ancestors = Vec::new();
let mut current = function.parent();
while let Some(node) = current {
if matches!(node.kind(), "function_item" | "function_signature_item") {
break;
}
if matches!(node.kind(), "impl_item" | "trait_item") {
ancestors.push(node);
}
current = node.parent();
}
ancestors.reverse();
let mut bounds = TraitBounds::default();
for item in ancestors {
Self::absorb_generic_bounds(state, item, &mut bounds);
}
Self::absorb_generic_bounds(state, function, &mut bounds);
bounds
}

fn absorb_generic_bounds(
state: &ExtractionState<'_>,
item: TsNode<'_>,
bounds: &mut TraitBounds,
) {
if let Some(parameters) = item.child_by_field_name("type_parameters") {
let mut cursor = parameters.walk();
if cursor.goto_first_child() {
loop {
let child = cursor.node();
if child.kind() == "type_parameter"
&& let Some(name_node) = child.child_by_field_name("name")
{
let name = state.node_text(name_node).to_owned();
bounds.declare(name.clone());
if let Some(clause) = child.child_by_field_name("bounds") {
bounds.constrain(&name, Self::trait_bound_clause(state, clause));
}
}
if !cursor.goto_next_sibling() {
break;
}
}
}
}
let Some(where_clause) = Self::child_of_kind(item, "where_clause") else {
return;
};
let mut cursor = where_clause.walk();
if !cursor.goto_first_child() {
return;
}
loop {
let child = cursor.node();
if child.kind() == "where_predicate"
&& let Some(left) = child.child_by_field_name("left")
&& left.kind() == "type_identifier"
{
let name = state.node_text(left);
if bounds.knows(name)
&& let Some(clause) = child.child_by_field_name("bounds")
{
bounds.constrain(name, Self::trait_bound_clause(state, clause));
}
}
if !cursor.goto_next_sibling() {
break;
}
}
}

fn trait_bound_clause(state: &ExtractionState<'_>, bounds: TsNode<'_>) -> BoundClause {
let mut clause = BoundClause {
paths: Vec::new(),
ambiguous: false,
};
let mut cursor = bounds.walk();
if !cursor.goto_first_child() {
return clause;
}
loop {
let child = cursor.node();
if child.is_named() {
match child.kind() {
"lifetime" | "use_bounds" | "removed_trait_bound" => {}
_ => match Self::stated_type_path(state, child) {
Some(path) => clause.paths.push(path),
None => clause.ambiguous = true,
},
}
}
if !cursor.goto_next_sibling() {
break;
}
}
clause
}

fn child_of_kind<'t>(node: TsNode<'t>, kind: &str) -> Option<TsNode<'t>> {
let mut cursor = node.walk();
if !cursor.goto_first_child() {
return None;
}
loop {
let child = cursor.node();
if child.kind() == kind {
return Some(child);
}
if !cursor.goto_next_sibling() {
return None;
}
}
}

/// The type a `let` initialiser states in syntax: a `T { .. }` literal,
/// optionally behind `?`. Method names are never return-type evidence.
/// Abstain rather than fabricate a receiver type.
Expand Down
Loading
Loading