use super::*;
pub(in super::super) fn resolve_source_relative(current_file: &Path, target: &str) -> PathBuf {
let candidate = PathBuf::from(target);
if candidate.is_absolute() {
candidate
} else {
current_file
.parent()
.unwrap_or(Path::new("."))
.join(candidate)
}
}
pub(in super::super) fn resolve_preflight_target(
current_file: &Path,
target: &str,
config: &CheckConfig,
) -> Vec<PathBuf> {
if let Some(asset_ref) = harn_modules::asset_paths::parse(target) {
let anchor = current_file.parent().unwrap_or(Path::new("."));
let candidates = match harn_modules::asset_paths::resolve(&asset_ref, anchor) {
Ok(path) => vec![path],
Err(_) => vec![PathBuf::from(target)],
};
super::super::result_cache::record_resolve_target(current_file, target, &candidates);
return candidates;
}
let mut candidates = vec![resolve_source_relative(current_file, target)];
if let Some(bundle_root) = config.bundle_root.as_deref() {
let bundle_base = PathBuf::from(bundle_root);
candidates.push(if PathBuf::from(target).is_absolute() {
PathBuf::from(target)
} else {
bundle_base.join(target)
});
}
candidates.dedup();
super::super::result_cache::record_resolve_target(current_file, target, &candidates);
candidates
}
pub(super) fn render_candidate_paths(candidates: &[PathBuf]) -> String {
candidates
.iter()
.map(|path| crate::format::slash_path(path))
.collect::<Vec<_>>()
.join(" or ")
}
pub(super) fn scan_stdlib_prompt_target(
template_path: &str,
target_label: &str,
span: harn_lexer::Span,
file_path: &Path,
source: &str,
diagnostics: &mut Vec<PreflightDiagnostic>,
) -> bool {
if harn_modules::asset_paths::stdlib_prompt_asset_path(template_path).is_none() {
return false;
}
let Some(body) = harn_vm::stdlib_modules::get_stdlib_prompt_asset(template_path) else {
diagnostics.push(PreflightDiagnostic {
code: Code::PromptTargetMissing,
path: file_path.display().to_string(),
source: source.to_string(),
span,
message: format!(
"preflight: {target_label} target '{template_path}' is not an embedded stdlib prompt asset"
),
help: Some(
"verify the `std/...harn.prompt` asset path against the stdlib prompt asset catalog"
.to_string(),
),
tags: None,
});
return true;
};
if let Err(err) = harn_vm::stdlib::template::validate_template_syntax(body) {
diagnostics.push(PreflightDiagnostic {
code: Code::PromptTemplateParse,
path: file_path.display().to_string(),
source: source.to_string(),
span,
message: format!("preflight: template '{template_path}' has a syntax error: {err}"),
help: Some("fix the embedded stdlib prompt asset template syntax".to_string()),
tags: None,
});
}
true
}
pub(in super::super) fn literal_string(node: &SNode) -> Option<String> {
match &node.node {
Node::StringLiteral(value) => Some(value.clone()),
_ => None,
}
}
pub(super) fn literal_template_path(node: &SNode) -> Option<String> {
match &node.node {
Node::StringLiteral(value) | Node::RawStringLiteral(value) => Some(value.clone()),
_ => None,
}
}
pub(super) fn render_target_miss_help(file_path: &Path, template_path: &str) -> String {
const GENERIC: &str = "keep template paths relative to the pipeline source file, or set [check].bundle_root / --bundle-root for bundled layouts. Use `@/...` for project-root paths";
let Some(basename) = Path::new(template_path)
.file_name()
.map(|n| n.to_string_lossy().into_owned())
else {
return GENERIC.to_string();
};
let anchor = file_path.parent().unwrap_or(Path::new("."));
let project_root = harn_modules::asset_paths::find_project_root(anchor)
.unwrap_or_else(|| anchor.to_path_buf());
let Some(near) = find_unique_basename(&project_root, &basename) else {
return GENERIC.to_string();
};
let caller_dir = file_path.parent();
if near.parent() == caller_dir {
return GENERIC.to_string();
}
let display = near
.strip_prefix(&project_root)
.map(crate::format::slash_path)
.unwrap_or_else(|_| crate::format::slash_path(&near));
format!(
"did you mean '{display}'? (found at {}). Otherwise: {GENERIC}",
crate::format::slash_path(&near)
)
}
pub(in super::super) fn find_unique_basename(root: &Path, basename: &str) -> Option<PathBuf> {
let mut matches: Vec<PathBuf> = Vec::with_capacity(2);
walk_for_basename(root, basename, 0, 8, &mut matches);
let result = (matches.len() == 1).then(|| matches.into_iter().next().expect("len == 1"));
super::super::result_cache::record_walk_unique(root, basename, result.as_deref());
result
}
pub(super) fn walk_for_basename(
dir: &Path,
basename: &str,
depth: usize,
max_depth: usize,
out: &mut Vec<PathBuf>,
) {
if depth > max_depth || out.len() > 1 {
return;
}
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let name_str = file_name.to_string_lossy();
if name_str.starts_with('.')
|| matches!(
name_str.as_ref(),
"target" | "node_modules" | "dist" | "build" | "out"
)
{
continue;
}
let Ok(file_type) = entry.file_type() else {
continue;
};
let path = entry.path();
if file_type.is_file() {
if name_str == basename {
out.push(path);
if out.len() > 1 {
return;
}
}
} else if file_type.is_dir() {
walk_for_basename(&path, basename, depth + 1, max_depth, out);
if out.len() > 1 {
return;
}
}
}
}