huskies: merge 821
This commit is contained in:
@@ -6,6 +6,10 @@ edition = "2024"
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
|
||||
[[bin]]
|
||||
name = "source-map-check"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
serde_json = { workspace = true }
|
||||
|
||||
|
||||
@@ -363,6 +363,55 @@ mod tests {
|
||||
assert!(content.contains("new.rs"), "new entry should be added");
|
||||
}
|
||||
|
||||
// --- Gate tests: AC3 / AC4 ---
|
||||
|
||||
/// AC3: a worktree with a missing module doc fails gates with a recognisable
|
||||
/// error that references the missing file and line number.
|
||||
#[test]
|
||||
fn gate_missing_module_doc_fails_with_file_and_line_in_direction() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
// File has a pub fn but NO //! module doc comment.
|
||||
let path = write_rs(tmp.path(), "missing_doc.rs", "pub fn no_module_doc() {}\n");
|
||||
let result = check_files(&[&path]);
|
||||
assert!(
|
||||
matches!(&result, CheckResult::Failures(v) if !v.is_empty()),
|
||||
"expected failures for missing module doc, got {result:?}"
|
||||
);
|
||||
if let CheckResult::Failures(failures) = result {
|
||||
let module_failure = failures
|
||||
.iter()
|
||||
.find(|f| f.item_kind == "module")
|
||||
.expect("expected a module-level failure");
|
||||
let direction = module_failure.to_direction();
|
||||
// Direction must name the file so the agent can navigate directly to it.
|
||||
assert!(
|
||||
direction.contains("missing_doc.rs"),
|
||||
"direction must reference the file name: {direction}"
|
||||
);
|
||||
// Direction must contain a colon-separated line number.
|
||||
assert!(
|
||||
direction.contains(':'),
|
||||
"direction must contain a file:line reference: {direction}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// AC4: a worktree where every changed file has full docs passes gates (Ok result).
|
||||
#[test]
|
||||
fn gate_fully_documented_files_pass() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let path = write_rs(
|
||||
tmp.path(),
|
||||
"fully_documented.rs",
|
||||
"//! Module doc.\n\n/// A function.\npub fn greet() {}\n\n/// A struct.\npub struct Hello;\n",
|
||||
);
|
||||
assert_eq!(
|
||||
check_files(&[&path]),
|
||||
CheckResult::Ok,
|
||||
"fully documented file should produce no failures"
|
||||
);
|
||||
}
|
||||
|
||||
// --- Spawn integration: update_for_worktree writes map at expected path ---
|
||||
|
||||
fn init_git_repo(dir: &Path) {
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
//! CLI for checking documentation coverage on files changed since a base branch.
|
||||
//!
|
||||
//! Usage: `source-map-check [--worktree <path>] [--base <branch>]`
|
||||
//!
|
||||
//! Exits with code 1 and prints LLM-friendly directions when public items are
|
||||
//! missing doc comments. Exits 0 (silently) when all changed files are fully
|
||||
//! documented or when there are no relevant changes to check.
|
||||
|
||||
use source_map_gen::{CheckResult, check_files};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
let worktree = parse_arg(&args, "--worktree").unwrap_or_else(|| ".".to_string());
|
||||
let base = parse_arg(&args, "--base").unwrap_or_else(|| "master".to_string());
|
||||
|
||||
let worktree_path = Path::new(&worktree);
|
||||
|
||||
let output = match Command::new("git")
|
||||
.args(["diff", "--name-only", &format!("{base}...HEAD")])
|
||||
.current_dir(worktree_path)
|
||||
.output()
|
||||
{
|
||||
Ok(o) => o,
|
||||
Err(e) => {
|
||||
eprintln!("source-map-check: git diff failed: {e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
if !output.status.success() {
|
||||
// Base branch not found or other git error — skip the check gracefully.
|
||||
return;
|
||||
}
|
||||
|
||||
let changed: Vec<PathBuf> = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.filter(|l| !l.is_empty())
|
||||
.map(|l| worktree_path.join(l))
|
||||
.filter(|p| p.exists())
|
||||
.collect();
|
||||
|
||||
if changed.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let file_refs: Vec<&Path> = changed.iter().map(PathBuf::as_path).collect();
|
||||
|
||||
match check_files(&file_refs) {
|
||||
CheckResult::Ok => {}
|
||||
CheckResult::Failures(failures) => {
|
||||
eprintln!(
|
||||
"Doc coverage check failed. Add doc comments to the following items before committing:\n"
|
||||
);
|
||||
for f in &failures {
|
||||
eprintln!(" {}", f.to_direction());
|
||||
}
|
||||
eprintln!(
|
||||
"\nRe-run: cargo run -p source-map-gen --bin source-map-check -- --worktree . --base master"
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a flag value from an argument list (e.g. `--flag value`).
|
||||
fn parse_arg(args: &[String], flag: &str) -> Option<String> {
|
||||
args.windows(2).find(|w| w[0] == flag).map(|w| w[1].clone())
|
||||
}
|
||||
@@ -177,7 +177,8 @@ fn has_jsdoc_before(lines: &[&str], item_idx: usize) -> bool {
|
||||
i -= 1;
|
||||
let line = lines[i].trim();
|
||||
if line.is_empty() {
|
||||
continue;
|
||||
// A blank line breaks the JSDoc–item adjacency: stop searching.
|
||||
return false;
|
||||
}
|
||||
if line.starts_with('@') {
|
||||
// Decorator — keep scanning upward
|
||||
|
||||
Reference in New Issue
Block a user