huskies: merge 1013

This commit is contained in:
dave
2026-05-13 23:07:32 +00:00
parent 4e007bb770
commit 8754c790b9
2 changed files with 154 additions and 33 deletions
+90 -10
View File
@@ -5,8 +5,9 @@
//! extension (`.rs` → [`RustAdapter`], `.ts`/`.tsx` → [`TypeScriptAdapter`]).
//!
//! The entry point for agent spawn integration is [`update_for_worktree`], which
//! runs `git diff --name-only` to find changed files and updates the source map for
//! those that pass the documentation coverage check.
//! finds changed files and updates the source map for those that pass the documentation
//! coverage check. [`added_line_ranges`] covers all git states — committed, staged,
//! unstaged, and untracked — so doc-gap detection is independent of index state.
mod rust_adapter;
mod ts_adapter;
@@ -141,30 +142,78 @@ fn parse_added_ranges(diff: &str) -> Vec<std::ops::RangeInclusive<usize>> {
ranges
}
/// Returns the 1-based line ranges in `file` that were added since `base` in `worktree`.
/// Returns the 1-based line ranges in `file` that were added relative to `base` in `worktree`.
///
/// Uses `git diff --unified=0 {base}...HEAD -- {file}` and parses the hunk headers.
/// Returns an empty `Vec` on git errors or when there are no added lines.
/// Covers all git states:
/// - Untracked files (not yet `git add`-ed): the entire file is treated as added.
/// - Committed changes since `base`: `git diff --unified=0 {base}...HEAD`
/// - Staged changes: `git diff --unified=0 --cached`
/// - Unstaged changes: `git diff --unified=0`
///
/// Returns an empty `Vec` when there are no additions in any state.
pub fn added_line_ranges(
worktree: &Path,
base: &str,
file: &Path,
) -> Vec<std::ops::RangeInclusive<usize>> {
let rel = file.strip_prefix(worktree).unwrap_or(file);
let output = Command::new("git")
let rel_str = rel.to_string_lossy();
// For untracked files, every line is a new addition.
let tracked = Command::new("git")
.args(["ls-files", "--", &*rel_str])
.current_dir(worktree)
.output();
if let Ok(out) = tracked
&& out.status.success()
&& out.stdout.is_empty()
{
let line_count = std::fs::read_to_string(file)
.map(|s| s.lines().count())
.unwrap_or(0);
return if line_count > 0 {
vec![1..=line_count]
} else {
Vec::new()
};
}
let mut ranges = Vec::new();
// Committed changes since base.
let committed = Command::new("git")
.args([
"diff",
"--unified=0",
&format!("{base}...HEAD"),
"--",
&rel.to_string_lossy(),
&*rel_str,
])
.current_dir(worktree)
.output();
match output {
Ok(o) => parse_added_ranges(&String::from_utf8_lossy(&o.stdout)),
Err(_) => Vec::new(),
if let Ok(o) = committed {
ranges.extend(parse_added_ranges(&String::from_utf8_lossy(&o.stdout)));
}
// Staged changes not yet committed.
let staged = Command::new("git")
.args(["diff", "--unified=0", "--cached", "--", &*rel_str])
.current_dir(worktree)
.output();
if let Ok(o) = staged {
ranges.extend(parse_added_ranges(&String::from_utf8_lossy(&o.stdout)));
}
// Unstaged changes to tracked files.
let unstaged = Command::new("git")
.args(["diff", "--unified=0", "--", &*rel_str])
.current_dir(worktree)
.output();
if let Ok(o) = unstaged {
ranges.extend(parse_added_ranges(&String::from_utf8_lossy(&o.stdout)));
}
ranges
}
/// Check documentation coverage, reporting only violations in lines added since `base`.
@@ -814,6 +863,37 @@ mod tests {
);
}
/// AC2: an untracked Rust file lacking a doc comment is caught by `check_files_ratcheted`.
///
/// The file is never `git add`-ed, so it is invisible to `git diff {base}...HEAD`.
/// The ratchet must still surface the missing-doc failure.
#[test]
fn untracked_file_with_missing_doc_fails() {
let tmp = TempDir::new().unwrap();
init_git_repo(tmp.path());
// Base commit so there is a HEAD to diff against.
Command::new("git")
.args(["commit", "--allow-empty", "-m", "base"])
.current_dir(tmp.path())
.output()
.unwrap();
// Write a new Rust file with a missing doc comment but do NOT `git add` it.
write_rs(
tmp.path(),
"untracked.rs",
"//! Module doc.\n\npub fn no_doc_here() {}\n",
);
let file = tmp.path().join("untracked.rs");
let result = check_files_ratcheted(&[file.as_path()], tmp.path(), "HEAD");
assert!(
matches!(&result, CheckResult::Failures(v) if v.iter().any(|f| f.item_name == "no_doc_here")),
"expected failure for undocumented fn in untracked file, got {result:?}"
);
}
/// `relative_key` strips the root prefix from an absolute path.
#[test]
fn relative_key_strips_root_prefix() {