o7 0.1.1

O7 workflow DSL runner
Documentation
pub mod ast;
pub mod errors;
pub mod lexer;
pub mod linker;
pub mod parser;
pub mod tokens;

use ast::ParseResult;
use errors::ParseError;
use linker::FileWorkflows;

/// Parse a single .7 file from source text.
pub fn parse_file(source: &str, filename: &str) -> ParseResult {
    let lex_result = lexer::lex(source, filename);
    if !lex_result.errors.is_empty() {
        return ParseResult::Err {
            errors: lex_result.errors,
        };
    }
    parser::parse(lex_result.tokens, filename)
}

/// Parse a .7 file from a file path.
/// This parses and links the file, performing duplicate-name, unresolved-reference,
/// and circular-reference validation.
pub fn parse_file_path(path: &str) -> Result<ParseResult, std::io::Error> {
    let source = std::fs::read_to_string(path)?;
    let parse_result = parse_file(&source, path);
    // Run the linker on successful parses to catch link-time errors
    // (duplicate names, unresolved references, circular references)
    match parse_result {
        ParseResult::Ok { workflows } => Ok(linker::link(vec![FileWorkflows {
            filename: path.to_string(),
            workflows,
        }])),
        err => Ok(err),
    }
}

/// Discover all .7 files in a directory, parse them, and link.
///
/// Per spec: lists files in project_root matching *.7 (root only, no recursion).
/// Pipeline: discover -> parse each -> merge -> resolve refs -> detect cycles.
pub fn discover_and_parse(project_root: &str) -> Result<ParseResult, std::io::Error> {
    let mut entries: Vec<String> = Vec::new();

    let dir = std::fs::read_dir(project_root)?;
    for entry in dir {
        let entry = entry?;
        let path = entry.path();
        if path.is_file() {
            if let Some(ext) = path.extension() {
                if ext == "7" {
                    entries.push(path.to_string_lossy().to_string());
                }
            }
        }
    }
    entries.sort();

    if entries.is_empty() {
        return Ok(ParseResult::Ok { workflows: vec![] });
    }

    let mut all_errors: Vec<ParseError> = Vec::new();
    let mut file_workflows: Vec<FileWorkflows> = Vec::new();
    let mut has_parse_errors = false;

    for filepath in &entries {
        let source = std::fs::read_to_string(filepath)?;
        let result = parse_file(&source, filepath);
        match result {
            ParseResult::Ok { workflows } => {
                file_workflows.push(FileWorkflows {
                    filename: filepath.clone(),
                    workflows,
                });
            }
            ParseResult::Err { errors } => {
                has_parse_errors = true;
                all_errors.extend(errors);
            }
        }
    }

    if has_parse_errors {
        return Ok(ParseResult::Err { errors: all_errors });
    }

    Ok(linker::link(file_workflows))
}