Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
546abcc
[docs] RFC-027 live semantic resolution (LSP-assisted incremental Pha…
Abhishek5517 Aug 24, 2026
f9d1d62
[docs] RFC-027 review fixes: correct citations, disambiguate schema, …
Abhishek5517 Aug 27, 2026
960c1a2
[travsr-core] Thread edge provenance to the MCP surface (DEBT-75)
Abhishek5517 Aug 27, 2026
250a408
[travsr-daemon] RFC-027 Phase 0: live semantic resolution lane
Abhishek5517 Aug 27, 2026
a1be08d
[travsr-vscode] RFC-027 Phase 0: definition-provider piggyback
Abhishek5517 Aug 27, 2026
211b15d
[travsr-daemon] Run the live overlay on save only, not on commit
Abhishek5517 Aug 27, 2026
956eaea
[travsr-daemon] RFC-027 Phase 1: edit classification, invalidation, p…
Abhishek5517 Aug 27, 2026
e65aa93
[travsr-daemon] RFC-027 Phase 2: commit ratification and convergence
Abhishek5517 Aug 27, 2026
03abb53
[travsr-mcp] RFC-027 Phase 3: make the live overlay legible at the MC…
Abhishek5517 Aug 27, 2026
7d7e66c
[travsr-daemon] RFC-027 section 12: continuous precision meter
Abhishek5517 Aug 28, 2026
165f4c6
[travsr-daemon] RFC-027 Phase 4: Rust in the live lane + per-language…
Abhishek5517 Aug 28, 2026
9fd2a1a
[travsr-daemon] RFC-027 Phase 4: headless spawn decision and per-lang…
Abhishek5517 Aug 28, 2026
4afe3a3
[travsr-analysis] RFC-027 section 8: generic live reference detector,…
Abhishek5517 Aug 29, 2026
bb17d6c
[travsr-analysis] RFC-027 section 8: the remaining 11 non-native lang…
Abhishek5517 Aug 29, 2026
089be92
[travsr-daemon] RFC-027 section 8.7.5: interface-edit closure reaches…
Abhishek5517 Aug 29, 2026
093ca9d
[travsr-daemon] RFC-027 section 8.7.6: strict opt-in gate for the liv…
Abhishek5517 Aug 29, 2026
6439e10
[travsr-daemon] RFC-027 section 11.3: opt Swift and C++ into the live…
Abhishek5517 Aug 29, 2026
4a94fb7
[travsr-daemon] RFC-027 section 11.3: opt Java into the live lane
Abhishek5517 Aug 29, 2026
efa64f3
[travsr-daemon] RFC-027 section 11.3: opt C# into the live lane
Abhishek5517 Aug 29, 2026
f787175
[travsr-daemon] RFC-027: correct the C# oracle note on DOTNET_ROOT
Abhishek5517 Aug 29, 2026
3fb7220
[travsr-plugin-host] resolve DOTNET_ROOT from well-known installs on …
Abhishek5517 Aug 29, 2026
c2b3b21
[travsr-mcp] RFC-027: green the CI failures on the live-lane PR
Abhishek5517 Aug 29, 2026
e3b0b00
[travsr-plugin-host] fix the dotnet-root tests on Windows
Abhishek5517 Aug 29, 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
1 change: 1 addition & 0 deletions crates/travsr-analysis/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
pub mod emit;
pub mod ffi;
pub mod generic;
pub mod live_detect;
pub mod skeleton;
pub mod snippet;
pub mod test_role;
Expand Down
898 changes: 898 additions & 0 deletions crates/travsr-analysis/src/live_detect.rs

Large diffs are not rendered by default.

89 changes: 89 additions & 0 deletions crates/travsr-analysis/src/phase_b_python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,68 @@ pub fn extract_native_phase_b(
Ok((nodes, edges, unresolved))
}

/// RFC-027 live IsImplementation lane: `class Foo(Bar)` bases in `files`, as
/// unresolved references (base name + line).
///
/// Unlike [`extract_native_phase_b`], which emits `IsImplementation` edges under
/// a same-file assumption (`py_vname(corpus, vname_path, ...)` for the base, so a
/// cross-file base dangles), this returns the raw base so the daemon resolves it
/// against the real node table and abstains otherwise. The clause sits on the
/// class's own declaration, so `impl_type` is `None`: the daemon derives the
/// source from the line.
pub fn extract_unresolved_inheritance(
root: &Path,
files: Option<&[(PathBuf, String)]>,
) -> anyhow::Result<Vec<travsr_core::InheritanceRef>> {
let language = tree_sitter::Language::new(tree_sitter_python::LANGUAGE);
let inherit_q = Query::new(&language, INHERIT_QUERY).context("python inherit query")?;

let walked;
let file_pairs: &[(PathBuf, String)] = match files {
Some(f) => f,
None => {
walked = collect_source_files(root, &["py", "pyi"]);
&walked
}
};

let mut out: Vec<travsr_core::InheritanceRef> = Vec::new();
for (abs_path, _vname_path) in file_pairs {
let Ok(source) = std::fs::read(abs_path) else {
continue;
};
let mut parser = Parser::new();
if parser.set_language(&language).is_err() {
continue;
}
let Some(tree) = parser.parse(&source, None) else {
continue;
};
let cap_names: Vec<String> = inherit_q
.capture_names()
.iter()
.map(|s| s.to_string())
.collect();
let mut cursor = QueryCursor::new();
let mut iter = cursor.matches(&inherit_q, tree.root_node(), source.as_slice());
while let Some(m) = iter.next() {
for &cap in m.captures {
if cap_names.get(cap.index as usize).map(String::as_str) != Some("base.name") {
continue;
}
let Ok(text) = cap.node.utf8_text(source.as_slice()) else {
continue;
};
out.push(travsr_core::InheritanceRef {
base_name: text.to_string(),
line: cap.node.start_position().row as u32 + 1,
});
}
}
}
Ok(out)
}

// ── Per-file analysis ─────────────────────────────────────────────────────────

fn extract_file_edges(
Expand Down Expand Up @@ -541,6 +603,33 @@ fn walk(root: &Path, dir: &Path, exts: &[&str], out: &mut Vec<(PathBuf, String)>
mod tests {
use super::*;

/// RFC-027 live IsImplementation lane: `class Dog(Animal, Mixin)` bases come
/// back as unresolved references, one per base, carrying the base name and
/// the line it is written on.
#[test]
fn class_bases_come_back_as_inheritance_refs() {
let dir = tempfile::tempdir().unwrap();
let file = dir.path().join("animals.py");
std::fs::write(
&file,
b"class Animal:\n pass\n\nclass Dog(Animal, Mixin):\n pass\n",
)
.unwrap();
let files = vec![(file.clone(), "animals.py".to_string())];
let refs = extract_unresolved_inheritance(dir.path(), Some(&files)).unwrap();

// Both bases are on line 4 (the `class Dog(...)` line).
assert!(
refs.iter().any(|r| r.base_name == "Animal" && r.line == 4),
"base Animal captured at its line, got {refs:?}",
);
assert!(
refs.iter().any(|r| r.base_name == "Mixin" && r.line == 4),
"base Mixin captured at its line, got {refs:?}",
);
assert_eq!(refs.len(), 2, "exactly the two bases: {refs:?}");
}

/// Bare-call tagging carries both candidate names, so a PascalCase free
/// function is not silently lost (#716 review).
///
Expand Down
17 changes: 17 additions & 0 deletions crates/travsr-analysis/src/phase_b_rust.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,23 @@ pub fn extract_native_phase_b(
Ok((nodes, edges, unresolved, refs))
}

// RFC-027 live IsImplementation lane, Rust — DEFERRED, not implemented here.
//
// Rust's `impl Trait for Type` does not fit the live model TS/Python use, for
// reasons that are structural, not effort:
// 1. The `impl` block is a separate item from the type, so the natural edge
// source (`struct:Foo`) lives in a different file than the clause. Saving
// the impl file would not invalidate that edge, so deleting an impl would
// leave a stale (wrong) edge until the next commit — the exact §8.1 failure.
// 2. Rust's own `impl:Foo` node carries no span and collapses every impl of a
// type into one node, so it cannot serve as a per-impl source either.
// 3. Rust native Phase B emits no `IsImplementation` edge at all, so a live one
// has nothing to ratify against (swept every commit) and no oracle for the
// precision meter.
// Making it correct requires teaching Rust's committed Phase B to emit
// IsImplementation from a properly-spanned per-impl node first — a change to the
// deterministic pipeline, tracked separately.

// ── Cargo.toml dependency graph ───────────────────────────────────────────────

fn extract_cargo_deps(corpus: &str, root: &Path) -> anyhow::Result<(Vec<Node>, Vec<Edge>)> {
Expand Down
117 changes: 117 additions & 0 deletions crates/travsr-analysis/src/phase_b_typescript.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,93 @@ pub fn extract_native_phase_b(
Ok((nodes, edges, unresolved))
}

/// RFC-027 live IsImplementation lane: the `extends`/`implements` clauses in
/// `files`, as unresolved references (base type name + line).
///
/// Unlike [`extract_native_phase_b`], which emits `IsImplementation` edges under
/// a same-file assumption (`ts_vname(corpus, vname_path, ...)` for the base, so a
/// cross-file base dangles), this returns the raw clause so the daemon can
/// resolve the base against the *real* node table — lexically when it is unique
/// repo-wide, or via the editor's definition provider — and abstain otherwise.
/// It never resolves and never mints identity.
pub fn extract_unresolved_inheritance(
root: &Path,
files: Option<&[(PathBuf, String)]>,
) -> anyhow::Result<Vec<travsr_core::InheritanceRef>> {
let language = tree_sitter::Language::new(tree_sitter_typescript::LANGUAGE_TYPESCRIPT);
let extends_q = Query::new(&language, EXTENDS_QUERY).context("ts extends query")?;
let implements_q = Query::new(&language, IMPLEMENTS_QUERY).context("ts implements query")?;

let walked;
let file_pairs: &[(PathBuf, String)] = match files {
Some(f) => f,
None => {
walked = collect_source_files(root, &["ts", "tsx", "mts", "cts"]);
&walked
}
};

let mut out: Vec<travsr_core::InheritanceRef> = Vec::new();
for (abs_path, _vname_path) in file_pairs {
let Ok(source) = std::fs::read(abs_path) else {
continue;
};
let mut parser = Parser::new();
if parser.set_language(&language).is_err() {
continue;
}
let Some(tree) = parser.parse(&source, None) else {
continue;
};
collect_inheritance(
&extends_q,
"extends.base",
tree.root_node(),
&source,
&mut out,
);
collect_inheritance(
&implements_q,
"implements.iface",
tree.root_node(),
&source,
&mut out,
);
}
Ok(out)
}

/// Push one `InheritanceRef` per match of `base_cap` in `q`, carrying the base
/// name's text and its 1-based line.
fn collect_inheritance(
q: &Query,
base_cap: &str,
root: tree_sitter::Node,
source: &[u8],
out: &mut Vec<travsr_core::InheritanceRef>,
) {
let cap_names: Vec<String> = q.capture_names().iter().map(|s| s.to_string()).collect();
let mut cursor = QueryCursor::new();
let mut iter = cursor.matches(q, root, source);
while let Some(m) = iter.next() {
for &cap in m.captures {
let Some(name) = cap_names.get(cap.index as usize) else {
continue;
};
if name != base_cap {
continue;
}
let Ok(text) = cap.node.utf8_text(source) else {
continue;
};
out.push(travsr_core::InheritanceRef {
base_name: text.to_string(),
line: cap.node.start_position().row as u32 + 1,
});
}
}
}

// ── Per-file analysis ─────────────────────────────────────────────────────────

fn extract_file_edges(
Expand Down Expand Up @@ -757,6 +844,36 @@ class App {
assert_eq!(recv_type_for_call(source, "run"), None);
}

/// RFC-027 live IsImplementation lane: `extends`/`implements` clauses come
/// back as unresolved references carrying the base type's name and the line
/// it is written on, so the daemon can resolve them against the real node
/// table rather than the same-file assumption `extract_native_phase_b` makes.
#[test]
fn extends_and_implements_come_back_as_inheritance_refs() {
let dir = std::env::temp_dir().join(format!("rfc027_inherit_{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("order.ts");
std::fs::write(
&file,
b"import { Base } from './base';\nexport class Order extends Base implements Shape {\n area() { return 0; }\n}\n",
)
.unwrap();
let files = vec![(file.clone(), "order.ts".to_string())];
let refs = extract_unresolved_inheritance(&dir, Some(&files)).unwrap();

// Both clauses are on line 2 (the class declaration line).
assert!(
refs.iter().any(|r| r.base_name == "Base" && r.line == 2),
"extends Base captured at its line, got {refs:?}",
);
assert!(
refs.iter().any(|r| r.base_name == "Shape" && r.line == 2),
"implements Shape captured at its line, got {refs:?}",
);
assert_eq!(refs.len(), 2, "exactly the two clauses, no more: {refs:?}");
std::fs::remove_dir_all(&dir).ok();
}

#[test]
fn new_expression_emits_class_unresolved_call() {
let dir = std::env::temp_dir().join(format!("e4_ts_{}", std::process::id()));
Expand Down
97 changes: 97 additions & 0 deletions crates/travsr-cli/src/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,41 @@ use crate::repo::find_git_root;
/// #583: equal markers are not sufficient evidence of freshness. A watcher
/// reindex rewrites a file's Phase A nodes and drops that file's `ref/call`
/// edges without moving HEAD, so both markers still agree while `get_callers`
/// RFC-027 section 12: render the cumulative live-lane precision reading.
///
/// Returns `None` when nothing has been measured, so the line never appears on a
/// repo that has not exercised the lane.
///
/// Coverage is always shown beside precision. A precision figure alone invites
/// the reading "1.00 means it is perfect", when it may mean "two of four hundred
/// claims were checkable and both happened to be right". The gate is on
/// precision; coverage is what tells you whether the gate saw anything.
fn live_precision_line(store: &travsr_store::SqliteStore) -> Option<String> {
let raw = store.get_meta("live_precision").ok().flatten()?;
let parts: Vec<u64> = raw
.split(',')
.filter_map(|p| p.trim().parse::<u64>().ok())
.collect();
let [agree, disagree, unverifiable] = parts.as_slice() else {
return None;
};
let total = agree + disagree + unverifiable;
if total == 0 {
return None;
}
let verified = agree + disagree;
let precision = if verified > 0 {
format!("{:.4}", *agree as f64 / verified as f64)
} else {
// Not "1.0000": nothing was checked, and saying so is the point.
"n/a".to_string()
};
Some(format!(
"live lane: precision {precision} over {verified}/{total} verifiable claims \
({disagree} disagreed with semantic analysis)"
))
}

/// and `get_blast_radius` answer from a graph degraded below the committed
/// snapshot. Reporting `complete` there is the actual harm; the edges
/// themselves return on the next commit's Phase B run.
Expand Down Expand Up @@ -172,6 +207,22 @@ pub fn run() -> anyhow::Result<()> {
payload.nodes, payload.edges, payload.schema, last_commit, phase_b_state, rerank_segment
);

// RFC-027 section 12: the live lane's measured precision, so the per-language
// shipping gate has a number a human can read rather than a log line that
// scrolled away.
//
// Read straight from the store rather than added to `StatusPayload`: this is
// a diagnostic, and threading it through the query payload would mean a
// protocol bump that every mixed CLI/daemon pair then has to survive.
//
// Silent when the lane has never claimed anything, which is every repo that
// has not used it — a counter of zero is not news.
if let Ok(store) = daemon_client::open_read_store(&db_path) {
if let Some(line) = live_precision_line(&store) {
println!("{line}");
}
}

// #645 WS-B: the freshness markers only ever compare against each other,
// never against the repository. Compare the caller's live HEAD (read at cwd,
// above) to the index's last_commit so a checkout at a different revision —
Expand Down Expand Up @@ -399,6 +450,52 @@ mod tests {
/// `Command::output()`. These pin the two answers it must give without
/// hanging for either: a real repo reports a short SHA, a directory that is
/// not a repo reports nothing.
/// The line never appears on a repo that has not used the live lane, and
/// never reports a precision it did not measure.
#[test]
fn live_precision_line_is_silent_until_something_is_measured() {
let mut store = travsr_store::SqliteStore::open_in_memory().unwrap();
assert_eq!(live_precision_line(&store), None, "no reading, no line");

store.set_meta("live_precision", "0,0,0").unwrap();
assert_eq!(
live_precision_line(&store),
None,
"an empty tally is not news"
);

store.set_meta("live_precision", "garbage").unwrap();
assert_eq!(
live_precision_line(&store),
None,
"a corrupt value is not a reading"
);
}

/// Coverage is always shown, and an unverified sample says so instead of
/// rendering a perfect score it did not earn.
#[test]
fn live_precision_line_reports_coverage_beside_precision() {
let mut store = travsr_store::SqliteStore::open_in_memory().unwrap();

store.set_meta("live_precision", "99,1,100").unwrap();
let line = live_precision_line(&store).unwrap();
assert!(line.contains("0.9900"), "precision: {line}");
assert!(line.contains("100/200"), "coverage must be visible: {line}");
assert!(
line.contains("1 disagreed"),
"false positives named: {line}"
);

// Nothing checkable: must not read as perfect.
store.set_meta("live_precision", "0,0,40").unwrap();
let line = live_precision_line(&store).unwrap();
assert!(
line.contains("n/a"),
"forty unchecked claims must not render as 1.0000: {line}"
);
}

#[test]
fn head_at_reports_a_sha_inside_a_repo_and_none_outside() {
let here = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
Expand Down
Loading
Loading