#[must_use]
pub fn normalize_cmd(command: &str) -> String {
command
.split_whitespace()
.collect::<Vec<_>>()
.join(" ")
.to_ascii_lowercase()
}
#[must_use]
pub fn is_verification_like_command(command: &str) -> bool {
if super::nudge::VERIFICATION_RE.is_match(command) {
return true;
}
let norm = normalize_cmd(command);
norm.contains("npm test")
|| norm.contains("jest")
|| norm.contains("cargo test")
|| norm.contains("go test")
|| norm.contains("tsc ")
|| norm.contains("npm run build")
}
#[must_use]
pub fn manifest_command_trusted(cmd: &str, recent_execs: &[String]) -> bool {
if cmd.trim().is_empty() {
return false;
}
if verification_satisfied(cmd, recent_execs) {
return true;
}
let norm = normalize_cmd(cmd);
let npm_jest = norm.contains("npm test") || norm.contains("jest");
let cargo = norm.contains("cargo test");
let go = norm.contains("go test");
let tsc = norm.contains("tsc ");
if !(npm_jest || cargo || go || tsc) {
return false;
}
recent_execs.iter().any(|ran| {
let ran = ran.as_str();
if npm_jest {
return ran.contains("npm test") || ran.contains("jest");
}
if cargo {
return ran.contains("cargo test");
}
if go {
return ran.contains("go test");
}
if tsc {
return ran.contains("tsc");
}
false
})
}
#[must_use]
pub fn verification_satisfied(expected: &str, recent_execs: &[String]) -> bool {
if expected.trim().is_empty() {
return true;
}
let equivalents: Vec<String> = super::verify_platform::verify_command_equivalents(expected);
equivalents.iter().any(|expected_norm| {
recent_execs
.iter()
.any(|ran| commands_match(expected_norm, ran))
})
}
fn commands_match(expected_norm: &str, ran_norm: &str) -> bool {
if expected_norm == ran_norm {
return true;
}
ran_norm.contains(expected_norm) || expected_norm.contains(ran_norm)
}
#[must_use]
pub fn verify_mismatch_suffix(expected: &str, lang: &str) -> String {
if lang.starts_with("zh") {
format!(
"\n\n[LHT] 警告:已将此项标为 completed,但近期未见匹配的验证命令 `{expected}`。请先运行该命令或撤销 completed。"
)
} else {
format!(
"\n\n[LHT] Warning: marked completed but no recent run matched verify command `{expected}`. Run it first or revert the status."
)
}
}
#[must_use]
pub fn strip_verify_prefix(content: &str) -> String {
let trimmed = content.trim();
let Some(rest) = trimmed.strip_prefix("[verify:") else {
return trimmed.to_string();
};
rest.split_once(']')
.map(|(_, after)| after.trim().to_string())
.unwrap_or_else(|| rest.trim().to_string())
}
#[must_use]
pub fn parse_verify_command(content: &str) -> Option<String> {
let trimmed = content.trim();
let rest = trimmed.strip_prefix("[verify:")?;
let (cmd, _) = rest.split_once(']')?;
let cmd = cmd.trim();
if cmd.is_empty() {
None
} else {
Some(cmd.to_string())
}
}
#[must_use]
pub fn parse_req_tag(content: &str) -> Option<String> {
parse_all_req_tags(content).into_iter().next()
}
#[must_use]
pub fn parse_all_req_tags(content: &str) -> Vec<String> {
let mut results = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut rest = content;
while let Some(start) = rest.find("[req:") {
let after = &rest[start + 5..];
if let Some(end) = after.find(']') {
let id = after[..end].trim().to_string();
if !id.is_empty() && seen.insert(id.clone()) {
results.push(id);
}
rest = &after[end + 1..];
} else {
break;
}
}
results
}
#[must_use]
pub fn strip_req_tags(content: &str) -> String {
let mut result = content.to_string();
while let Some(start) = result.find("[req:") {
if let Some(end) = result[start..].find(']') {
result = format!(
"{}{}",
&result[..start],
result[start + end + 1..].trim_start()
);
} else {
break;
}
}
result.trim().to_string()
}
const EN_ACCEPTANCE_HINTS: &[&str] = &[
"tests pass",
"test passes",
"all tests",
"build passes",
"builds clean",
"compiles",
"lint clean",
"lints clean",
"run the example",
"run examples",
"examples run",
"runs green",
"go build",
"go test",
"go vet",
"cargo test",
"cargo build",
"cargo check",
"cargo clippy",
"npm test",
"pnpm test",
"yarn test",
"pytest",
];
const ZH_ACCEPTANCE_HINTS: &[&str] = &[
"跑通",
"全绿",
"编译通过",
"构建通过",
"测试通过",
"全部通过",
"验证通过",
"运行示例",
"示例脚本",
];
#[must_use]
pub fn unverified_acceptance_suffix(content: &str, lang: &str) -> Option<String> {
if parse_verify_command(content).is_some() {
return None;
}
let lc = content.to_ascii_lowercase();
let looks_runnable = EN_ACCEPTANCE_HINTS.iter().any(|k| lc.contains(k))
|| ZH_ACCEPTANCE_HINTS.iter().any(|k| content.contains(k));
if !looks_runnable {
return None;
}
Some(if lang.starts_with("zh") {
"\n\n[LHT] 提示:此项看起来是“可运行的验收”(构建 / 测试 / 跑示例),但没有 `[verify: <命令>]` 前缀,无法据此核验你是否真的运行过。请改写为 `[verify: <命令>] <描述>` 并**实际运行该命令、看到通过输出后**再标 completed——创建文件不等于验证通过。".to_string()
} else {
"\n\n[LHT] Note: this looks like a runnable acceptance (build / tests pass / run examples) but has no `[verify: <command>]` prefix, so there's no way to confirm you actually ran it. Rewrite it as `[verify: <command>] <label>` and **run that command and see it pass** before marking it completed — creating a file is not the same as verifying it runs.".to_string()
})
}
#[must_use]
pub fn verify_gate_verdict(
content: &str,
recent_execs: &[String],
lang: &str,
) -> (&'static str, Option<String>) {
match parse_verify_command(content) {
Some(expected) => {
if verification_satisfied(&expected, recent_execs) {
("verified", None)
} else {
("mismatch", Some(verify_mismatch_suffix(&expected, lang)))
}
}
None => match unverified_acceptance_suffix(content, lang) {
Some(s) => ("unverified_acceptance", Some(s)),
None => ("untagged_ok", None),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_and_parse_verify() {
let s = "[verify: cargo test -p auth] Run tests";
assert_eq!(
parse_verify_command(s).as_deref(),
Some("cargo test -p auth")
);
assert_eq!(strip_verify_prefix(s), "Run tests");
}
#[test]
fn manifest_command_trusted_after_npm_test_family() {
let recent = vec![normalize_cmd("npx jest --passWithNoTests")];
assert!(manifest_command_trusted("npm test --silent", &recent));
assert!(!manifest_command_trusted("cargo clippy", &recent));
}
#[test]
fn is_verification_like_includes_jest() {
assert!(is_verification_like_command("npx jest"));
assert!(!is_verification_like_command("echo hi"));
}
#[test]
fn verification_match_substring() {
let recent = vec![normalize_cmd("cargo test -p auth --no-run")];
assert!(verification_satisfied("cargo test -p auth", &recent));
assert!(!verification_satisfied("cargo clippy", &recent));
}
#[test]
fn unverified_acceptance_flags_runnable_items() {
assert!(unverified_acceptance_suffix("Run examples and confirm they pass", "en").is_some());
assert!(unverified_acceptance_suffix("go test ./... all green", "en").is_some());
assert!(unverified_acceptance_suffix("跑通全部示例脚本", "zh").is_some());
assert!(unverified_acceptance_suffix("编译通过且测试通过", "zh").is_some());
}
#[test]
fn unverified_acceptance_ignores_plain_and_tagged_items() {
assert!(unverified_acceptance_suffix("Create token/token.go", "en").is_none());
assert!(unverified_acceptance_suffix("创建 lexer 包", "zh").is_none());
assert!(unverified_acceptance_suffix("[verify: go test ./...] tests pass", "en").is_none());
}
#[test]
fn req_tag_parses_single() {
assert_eq!(
parse_req_tag("[verify: cargo test] [req: R2] auth tests"),
Some("R2".to_string())
);
assert_eq!(
parse_req_tag("implement login [req: AUTH-1]"),
Some("AUTH-1".to_string())
);
assert_eq!(parse_req_tag("refactor module"), None);
}
#[test]
fn req_tag_parses_multiple_deduped() {
let tags = parse_all_req_tags("[req: R1] implement X [req: R2] also [req: R1] again");
assert_eq!(tags, vec!["R1", "R2"]);
}
#[test]
fn req_tag_strip() {
assert_eq!(
strip_req_tags("[verify: go test ./...] [req: R1] tests pass"),
"[verify: go test ./...] tests pass"
);
assert_eq!(strip_req_tags("plain item"), "plain item");
}
#[test]
fn verdict_covers_all_four_cases() {
let recent = vec![normalize_cmd("go test ./...")];
let (v, s) = verify_gate_verdict("[verify: go test ./...] all green", &recent, "en");
assert_eq!(v, "verified");
assert!(s.is_none());
let (v, s) = verify_gate_verdict("[verify: go vet ./...] no warnings", &recent, "en");
assert_eq!(v, "mismatch");
assert!(s.is_some());
let (v, s) = verify_gate_verdict("go build passes cleanly", &[], "en");
assert_eq!(v, "unverified_acceptance");
assert!(s.is_some());
let (v, s) = verify_gate_verdict("Create token/token.go", &[], "en");
assert_eq!(v, "untagged_ok");
assert!(s.is_none());
}
}