use serde_json::Value;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
fn shell_single_quote(s: &str) -> String {
format!("'{}'", s.replace('\'', "'\\''"))
}
fn command_in_dir(dir: Option<&Path>, command: &str) -> String {
match dir {
Some(path) => format!(
"cd {} && {}",
shell_single_quote(&path.to_string_lossy()),
command
),
None => command.to_string(),
}
}
fn package_json_scripts(dir: &Path) -> HashMap<String, String> {
json_scripts(&dir.join("package.json"))
}
fn json_scripts(path: &Path) -> HashMap<String, String> {
let Ok(text) = std::fs::read_to_string(path) else {
return HashMap::new();
};
let Ok(value) = serde_json::from_str::<Value>(&text) else {
return HashMap::new();
};
value
.get("scripts")
.and_then(Value::as_object)
.map(|scripts| {
scripts
.iter()
.filter_map(|(name, script)| {
script
.as_str()
.map(|script| (name.to_string(), script.to_string()))
})
.collect()
})
.unwrap_or_default()
}
fn composer_scripts(dir: &Path) -> HashMap<String, String> {
json_scripts(&dir.join("composer.json"))
}
fn npm_runner(dir: &Path) -> &'static str {
if dir.join("pnpm-lock.yaml").exists() {
"pnpm"
} else if dir.join("yarn.lock").exists() {
"yarn"
} else if dir.join("bun.lockb").exists() || dir.join("bun.lock").exists() {
"bun"
} else {
"npm"
}
}
fn package_script_command(runner: &str, script: &str) -> String {
match (runner, script) {
("npm", "test") => "npm test".to_string(),
("yarn", "test") => "yarn test".to_string(),
("pnpm", "test") => "pnpm test".to_string(),
("bun", "test") => "bun test".to_string(),
("npm", other) => format!("npm run {other}"),
("yarn", other) => format!("yarn {other}"),
("pnpm", other) => format!("pnpm {other}"),
("bun", other) => format!("bun run {other}"),
(_, other) => format!("npm run {other}"),
}
}
fn makefile_has_target(dir: &Path, target: &str) -> bool {
let path = dir.join("Makefile");
let Ok(text) = std::fs::read_to_string(&path) else {
return false;
};
text.lines().any(|line| {
let line = line.trim_start();
line.starts_with(&format!("{target}:")) || line.starts_with(&format!(".PHONY: {target}"))
})
}
fn prompt_mentions(prompt: &str, needles: &[&str]) -> bool {
let lower = prompt.to_ascii_lowercase();
needles.iter().any(|needle| lower.contains(needle))
}
fn prompt_file_candidate(prompt: &str) -> Option<String> {
for raw in prompt.split_whitespace() {
let token = raw.trim_matches(|c: char| {
matches!(
c,
'"' | '\'' | '`' | ',' | ';' | ':' | ')' | '(' | '[' | ']' | '{' | '}'
)
});
if token.is_empty() || token.starts_with('-') || token.contains("://") {
continue;
}
let looks_path = token.contains('/')
|| token.ends_with(".md")
|| token.ends_with(".txt")
|| token.ends_with(".json")
|| token.ends_with(".yaml")
|| token.ends_with(".yml")
|| token.ends_with(".toml")
|| token.ends_with(".html")
|| token.ends_with(".css")
|| token.ends_with(".js")
|| token.ends_with(".ts")
|| token.ends_with(".tsx")
|| token.ends_with(".rs")
|| token.ends_with(".py")
|| token.ends_with(".go")
|| token.ends_with(".swift")
|| token.ends_with(".kt")
|| token.ends_with(".kts")
|| token.ends_with(".java")
|| token.ends_with(".c")
|| token.ends_with(".h")
|| token.ends_with(".cc")
|| token.ends_with(".hh")
|| token.ends_with(".cpp")
|| token.ends_with(".hpp")
|| token.ends_with(".cxx")
|| token.ends_with(".hxx")
|| token.ends_with(".cs")
|| token.ends_with(".fs")
|| token.ends_with(".vb")
|| token.ends_with(".php")
|| token.ends_with(".rb")
|| token.ends_with(".ex")
|| token.ends_with(".exs");
if looks_path && !token.starts_with('/') && !token.contains("..") {
return Some(token.to_string());
}
}
None
}
struct ProjectCheck {
command: String,
confidence: &'static str,
rationale: String,
signals: Vec<String>,
warnings: Vec<String>,
}
struct LocatedProjectCheck {
dir: PathBuf,
file_for_check: String,
project: ProjectCheck,
project_dir_signal: Option<String>,
}
fn project_check_for_prompt(dir: &Path, prompt: &str) -> Option<ProjectCheck> {
if dir.join("Cargo.toml").exists() {
return Some(ProjectCheck {
command: "cargo test -q".to_string(),
confidence: "high",
rationale:
"Rust project detected; cargo test is the broadest deterministic completion check."
.to_string(),
signals: vec!["Cargo.toml".to_string()],
warnings: Vec::new(),
});
}
let mut skipped_signals: Vec<String> = Vec::new();
let mut skipped_warnings: Vec<String> = Vec::new();
if dir.join("package.json").exists() {
let scripts = package_json_scripts(dir);
let runner = npm_runner(dir);
let preference: &[&str] = if prompt_mentions(prompt, &["type", "typescript", "tsc"]) {
&["typecheck", "check", "test", "build"]
} else if prompt_mentions(prompt, &["check", "verify", "ci"]) {
&["check", "test", "typecheck", "build", "lint"]
} else if prompt_mentions(prompt, &["build", "compile", "bundle"]) {
&["build", "test", "typecheck", "check"]
} else if prompt_mentions(prompt, &["lint", "format"]) {
&["lint", "test", "typecheck", "build"]
} else {
&["test", "typecheck", "check", "build", "lint"]
};
for script in preference {
if scripts.contains_key(*script) {
return Some(ProjectCheck {
command: package_script_command(runner, script),
confidence: "high",
rationale: format!(
"Node project detected; selected the `{script}` script from package.json."
),
signals: vec![
"package.json".to_string(),
format!("package_script:{script}"),
],
warnings: Vec::new(),
});
}
}
skipped_signals.push("package.json".to_string());
skipped_warnings
.push("package.json has no test/typecheck/check/build/lint script".to_string());
}
if dir.join("pyproject.toml").exists() || dir.join("setup.py").exists() {
let has_tests = dir.join("tests").is_dir();
let mut signals = skipped_signals;
signals.push("python_project".to_string());
return Some(ProjectCheck {
command: if has_tests {
"python -m pytest".to_string()
} else {
"python -m compileall .".to_string()
},
confidence: if has_tests { "high" } else { "medium" },
rationale: if has_tests {
"Python project with tests/ detected; pytest is a deterministic completion check."
.to_string()
} else {
"Python project detected but no tests/ directory was found; compileall is a syntax-level fallback."
.to_string()
},
signals,
warnings: skipped_warnings,
});
}
if dir.join("go.mod").exists() {
let mut signals = skipped_signals;
signals.push("go.mod".to_string());
return Some(ProjectCheck {
command: "go test ./...".to_string(),
confidence: "high",
rationale:
"Go module detected; go test ./... is the standard deterministic completion check."
.to_string(),
signals,
warnings: skipped_warnings,
});
}
if dir.join("Package.swift").exists() {
let mut signals = skipped_signals;
signals.push("Package.swift".to_string());
return Some(ProjectCheck {
command: "swift test".to_string(),
confidence: "high",
rationale:
"Swift package detected; swift test is the standard deterministic completion check."
.to_string(),
signals,
warnings: skipped_warnings,
});
}
if dir.join("CMakeLists.txt").exists() {
let mut signals = skipped_signals;
signals.push("CMakeLists.txt".to_string());
let use_ctest = prompt_mentions(prompt, &["test", "ctest", "unit", "ci", "verify"]);
return Some(ProjectCheck {
command: if use_ctest {
"cmake -S . -B build && cmake --build build && ctest --test-dir build --output-on-failure"
.to_string()
} else {
"cmake -S . -B build && cmake --build build".to_string()
},
confidence: "high",
rationale: if use_ctest {
"CMake project detected; configure, build, and ctest provide a deterministic completion check."
.to_string()
} else {
"CMake project detected; configure and build provide a deterministic completion check."
.to_string()
},
signals,
warnings: skipped_warnings,
});
}
if dir.join("build.gradle.kts").exists() || dir.join("build.gradle").exists() {
let mut signals = skipped_signals;
let build_file = if dir.join("build.gradle.kts").exists() {
"build.gradle.kts"
} else {
"build.gradle"
};
signals.push(build_file.to_string());
let has_wrapper = dir.join("gradlew").exists();
if has_wrapper {
signals.push("gradlew".to_string());
}
return Some(ProjectCheck {
command: if has_wrapper {
"./gradlew test".to_string()
} else {
"gradle test".to_string()
},
confidence: "high",
rationale: if has_wrapper {
"Gradle project detected with wrapper; ./gradlew test is the deterministic completion check."
.to_string()
} else {
"Gradle project detected; gradle test is the standard deterministic completion check."
.to_string()
},
signals,
warnings: skipped_warnings,
});
}
if dir.join("pom.xml").exists() {
let mut signals = skipped_signals;
signals.push("pom.xml".to_string());
let has_wrapper = dir.join("mvnw").exists();
if has_wrapper {
signals.push("mvnw".to_string());
}
return Some(ProjectCheck {
command: if has_wrapper {
"./mvnw test".to_string()
} else {
"mvn test".to_string()
},
confidence: "high",
rationale: if has_wrapper {
"Maven project detected with wrapper; ./mvnw test is the deterministic completion check."
.to_string()
} else {
"Maven project detected; mvn test is the standard deterministic completion check."
.to_string()
},
signals,
warnings: skipped_warnings,
});
}
if let Some(dotnet_file) = dotnet_project_signal(dir) {
let mut signals = skipped_signals;
signals.push(dotnet_file);
return Some(ProjectCheck {
command: "dotnet test".to_string(),
confidence: "high",
rationale:
".NET project detected; dotnet test is the standard deterministic completion check."
.to_string(),
signals,
warnings: skipped_warnings,
});
}
if dir.join("composer.json").exists() {
let mut signals = skipped_signals;
signals.push("composer.json".to_string());
let has_phpunit =
dir.join("vendor/bin/phpunit").exists() || dir.join("phpunit.xml").exists();
if has_phpunit {
signals.push("phpunit".to_string());
}
let scripts = composer_scripts(dir);
let has_test_script = scripts.contains_key("test");
if has_test_script {
signals.push("composer_script:test".to_string());
}
return Some(ProjectCheck {
command: if has_phpunit {
"vendor/bin/phpunit".to_string()
} else if has_test_script {
"composer test".to_string()
} else {
"find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 -n1 php -l"
.to_string()
},
confidence: if has_phpunit || has_test_script {
"high"
} else {
"medium"
},
rationale: if has_phpunit {
"PHP Composer project with phpunit detected; vendor/bin/phpunit is the deterministic completion check."
.to_string()
} else if has_test_script {
"PHP Composer project with a test script detected; composer test is the deterministic completion check."
.to_string()
} else {
"PHP Composer project detected without a test script; PHP linting is a deterministic syntax-level fallback."
.to_string()
},
signals,
warnings: skipped_warnings,
});
}
if dir.join("Gemfile").exists() {
let mut signals = skipped_signals;
signals.push("Gemfile".to_string());
let ruby = ruby_project_check(dir);
signals.extend(ruby.signals);
return Some(ProjectCheck {
command: ruby.command,
confidence: ruby.confidence,
rationale: ruby.rationale,
signals,
warnings: skipped_warnings,
});
}
if dir.join("mix.exs").exists() {
let mut signals = skipped_signals;
signals.push("mix.exs".to_string());
return Some(ProjectCheck {
command: "mix test".to_string(),
confidence: "high",
rationale:
"Elixir Mix project detected; mix test is the standard deterministic completion check."
.to_string(),
signals,
warnings: skipped_warnings,
});
}
if makefile_has_target(dir, "test") {
let mut signals = skipped_signals;
signals.push("Makefile:test".to_string());
return Some(ProjectCheck {
command: "make test".to_string(),
confidence: "medium",
rationale: "Makefile test target detected.".to_string(),
signals,
warnings: skipped_warnings,
});
}
if skipped_warnings.is_empty() {
None
} else {
Some(ProjectCheck {
command: String::new(),
confidence: "none",
rationale: String::new(),
signals: skipped_signals,
warnings: skipped_warnings,
})
}
}
fn path_for_shell(path: &Path) -> String {
path.to_string_lossy().replace('\\', "/")
}
fn project_check_for_path(
dir: &Path,
file: &str,
prompt: &str,
target_may_be_directory: bool,
) -> Option<LocatedProjectCheck> {
let file_path = dir.join(file);
let mut candidate = if target_may_be_directory && file_path.is_dir() {
file_path.clone()
} else {
file_path.parent()?.to_path_buf()
};
loop {
if let Some(project) = project_check_for_prompt(&candidate, prompt) {
let file_for_check = file_path.strip_prefix(&candidate).ok()?;
let project_dir_signal = candidate
.strip_prefix(dir)
.ok()
.filter(|relative| !relative.as_os_str().is_empty())
.map(|relative| format!("project_dir:{}", path_for_shell(relative)));
return Some(LocatedProjectCheck {
dir: candidate,
file_for_check: path_for_shell(file_for_check),
project,
project_dir_signal,
});
}
if candidate == dir || !candidate.pop() {
break;
}
}
None
}
fn dotnet_project_signal(dir: &Path) -> Option<String> {
let entries = std::fs::read_dir(dir).ok()?;
let mut project_file: Option<String> = None;
for entry in entries.flatten() {
let path = entry.path();
let Some(file_name) = path.file_name().and_then(|name| name.to_str()) else {
continue;
};
if file_name.ends_with(".sln") {
return Some(file_name.to_string());
}
if project_file.is_none()
&& (file_name.ends_with(".csproj")
|| file_name.ends_with(".fsproj")
|| file_name.ends_with(".vbproj"))
{
project_file = Some(file_name.to_string());
}
}
project_file
}
struct RubyCheck {
command: String,
confidence: &'static str,
rationale: String,
signals: Vec<String>,
}
fn ruby_project_check(dir: &Path) -> RubyCheck {
if dir.join("Rakefile").exists() {
return RubyCheck {
command: "bundle exec rake test".to_string(),
confidence: "high",
rationale:
"Ruby Bundler project with Rakefile detected; bundle exec rake test is the deterministic completion check."
.to_string(),
signals: vec!["Rakefile".to_string()],
};
}
if dir.join("spec").is_dir() {
return RubyCheck {
command: "bundle exec rspec".to_string(),
confidence: "medium",
rationale:
"Ruby Bundler project with spec/ detected; bundle exec rspec is the deterministic completion check."
.to_string(),
signals: vec!["spec".to_string()],
};
}
RubyCheck {
command: "find . -name '*.rb' -not -path './vendor/*' -print0 | xargs -0 -n1 ruby -c"
.to_string(),
confidence: "medium",
rationale:
"Ruby Bundler project detected without a test runner signal; Ruby syntax checking is a deterministic fallback."
.to_string(),
signals: Vec::new(),
}
}
pub fn synthesize_goal_check(prompt: &str, cwd: Option<&Path>) -> Value {
let dir = cwd.filter(|p| p.is_dir());
let mut signals: Vec<String> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
if prompt_mentions(
prompt,
&["create", "write", "generate", "save", "output", "produce"],
) {
if let Some(file) = prompt_file_candidate(prompt) {
signals.push(format!("prompt_mentions_path:{file}"));
if let Some(dir) = dir {
if let Some(located) = project_check_for_path(dir, &file, prompt, false) {
let LocatedProjectCheck {
dir: project_dir,
file_for_check,
project,
project_dir_signal,
} = located;
signals.extend(project.signals);
if let Some(project_dir_signal) = project_dir_signal {
signals.push(project_dir_signal);
}
warnings.extend(project.warnings);
if !project.command.is_empty() {
let file_check = format!("test -f {}", shell_single_quote(&file_for_check));
let command = format!("{file_check} && {}", project.command);
return serde_json::json!({
"suggested": true,
"goal": {
"check": command_in_dir(Some(&project_dir), &command),
"max_iterations": 8,
},
"confidence": project.confidence,
"rationale": format!(
"The prompt names `{file}` and a project check is available. Completion requires the file to exist and the project verifier to pass. {}",
project.rationale
),
"signals": signals,
"warnings": warnings,
});
}
}
}
return serde_json::json!({
"suggested": true,
"goal": {
"check": command_in_dir(dir, &format!("test -f {}", shell_single_quote(&file))),
"max_iterations": 8,
},
"confidence": "medium",
"rationale": format!("The prompt names `{file}`, so file existence is a concrete completion check."),
"signals": signals,
"warnings": warnings,
});
}
}
if let Some(dir) = dir {
if let Some(file) = prompt_file_candidate(prompt) {
if let Some(located) = project_check_for_path(dir, &file, prompt, true) {
let LocatedProjectCheck {
dir: project_dir,
project,
project_dir_signal,
..
} = located;
signals.push(format!("prompt_mentions_path:{file}"));
signals.extend(project.signals);
if let Some(project_dir_signal) = project_dir_signal {
signals.push(project_dir_signal);
}
warnings.extend(project.warnings);
if !project.command.is_empty() {
return serde_json::json!({
"suggested": true,
"goal": {
"check": command_in_dir(Some(&project_dir), &project.command),
"max_iterations": 8,
},
"confidence": project.confidence,
"rationale": format!(
"The prompt names `{file}` inside a recognized project. Completion requires the nearest project verifier to pass. {}",
project.rationale
),
"signals": signals,
"warnings": warnings,
});
}
}
}
if let Some(project) = project_check_for_prompt(dir, prompt) {
signals.extend(project.signals);
warnings.extend(project.warnings);
if !project.command.is_empty() {
let ProjectCheck {
command,
confidence,
rationale,
..
} = project;
return serde_json::json!({
"suggested": true,
"goal": {
"check": command_in_dir(Some(dir), &command),
"max_iterations": 8,
},
"confidence": confidence,
"rationale": rationale,
"signals": signals,
"warnings": warnings,
});
}
}
if makefile_has_target(dir, "test") {
signals.push("Makefile:test".to_string());
return serde_json::json!({
"suggested": true,
"goal": {
"check": command_in_dir(Some(dir), "make test"),
"max_iterations": 8,
},
"confidence": "medium",
"rationale": "Makefile test target detected.",
"signals": signals,
"warnings": warnings,
});
}
} else if cwd.is_some() {
warnings.push("working_dir/cwd was supplied but is not a directory".to_string());
}
serde_json::json!({
"suggested": false,
"goal": Value::Null,
"confidence": "none",
"rationale": "No deterministic project or file-existence check could be inferred. Ask the user for a concrete success condition before starting a goal loop.",
"signals": signals,
"warnings": warnings,
})
}