roder-roadmap 0.1.0

Agentic software development tools and SDKs for Roder.
Documentation
use std::collections::HashSet;

use crate::parser::path_diagnostic;
use crate::{Diagnostic, DiagnosticSeverity, Document, ValidationResult};

pub fn validate_document(document: &Document) -> ValidationResult {
    let mut diagnostics = Vec::new();
    if let Some(diagnostic) = path_diagnostic(&document.path) {
        diagnostics.push(diagnostic);
    }
    require(
        &mut diagnostics,
        document,
        !document.title.trim().is_empty(),
        None,
        "missing title heading",
    );
    require(
        &mut diagnostics,
        document,
        !document.goal.trim().is_empty(),
        None,
        "missing **Goal:** field",
    );
    require(
        &mut diagnostics,
        document,
        !document.architecture.trim().is_empty(),
        None,
        "missing **Architecture:** field",
    );
    require(
        &mut diagnostics,
        document,
        !document.owned_paths.is_empty(),
        None,
        "missing owned paths",
    );
    require(
        &mut diagnostics,
        document,
        !document.tasks.is_empty(),
        None,
        "missing task checklist items",
    );
    require(
        &mut diagnostics,
        document,
        document
            .tasks
            .iter()
            .any(|task| !task.run_blocks.is_empty()),
        None,
        "missing Run block",
    );
    require(
        &mut diagnostics,
        document,
        !document.acceptance.is_empty(),
        None,
        "missing acceptance checklist",
    );

    let mut seen = HashSet::new();
    for task in &document.tasks {
        if !seen.insert(task.id.clone()) {
            diagnostics.push(Diagnostic {
                path: document.path.clone(),
                line: Some(task.line),
                severity: DiagnosticSeverity::Error,
                message: format!("duplicate task id: {}", task.id),
            });
        }
    }

    ValidationResult {
        document_id: document.id.clone(),
        diagnostics,
    }
}

fn require(
    diagnostics: &mut Vec<Diagnostic>,
    document: &Document,
    ok: bool,
    line: Option<usize>,
    message: &str,
) {
    if !ok {
        diagnostics.push(Diagnostic {
            path: document.path.clone(),
            line,
            severity: DiagnosticSeverity::Error,
            message: message.to_string(),
        });
    }
}