systemprompt-loader 0.6.0

File and module discovery infrastructure for systemprompt.io AI governance — manifests, schemas, and extension loading. Separates I/O from shared models in the MCP governance pipeline.
use std::fs;
use std::path::{Path, PathBuf};

use crate::error::{ConfigLoadError, ConfigLoadResult};

use super::merge::{merge_partial, resolve_partial_includes};
use super::types::{IncludeResolveCtx, PartialServicesFile};

pub(super) fn resolve_includes_recursively(
    base_path: &Path,
    include_path: &str,
    referrer: &Path,
    ctx: &mut IncludeResolveCtx<'_>,
) -> ConfigLoadResult<()> {
    let referrer_dir = referrer.parent().unwrap_or(base_path);
    let full_path = referrer_dir.join(include_path);

    if !full_path.exists() {
        return Err(ConfigLoadError::IncludeNotFound {
            include: full_path,
            referrer: referrer.to_path_buf(),
        });
    }

    let canonical = fs::canonicalize(&full_path).map_err(|e| ConfigLoadError::Io {
        path: full_path.clone(),
        source: e,
    })?;

    if ctx.visited.contains(&canonical) {
        let mut chain: Vec<String> = ctx.chain.iter().map(|p| p.display().to_string()).collect();
        chain.push(canonical.display().to_string());
        return Err(ConfigLoadError::IncludeCycle {
            chain: chain.join(" -> "),
        });
    }
    ctx.visited.insert(canonical.clone());

    let content = fs::read_to_string(&canonical).map_err(|e| ConfigLoadError::Io {
        path: canonical.clone(),
        source: e,
    })?;

    let partial_file: PartialServicesFile =
        serde_yaml::from_str(&content).map_err(|e| ConfigLoadError::Yaml {
            path: canonical.clone(),
            source: e,
        })?;

    ctx.chain.push(canonical.clone());
    for nested in &partial_file.includes {
        resolve_includes_recursively(base_path, nested, &canonical, ctx)?;
    }
    ctx.chain.pop();

    let file_dir: PathBuf = canonical.parent().unwrap_or(base_path).to_path_buf();
    let mut partial = partial_file.into_partial_config();
    resolve_partial_includes(&mut partial, &file_dir)?;
    merge_partial(ctx.merged, partial)?;

    Ok(())
}