orchestral-runtime 0.1.0

Thread Runtime with concurrency, interruption, scheduling, LLM planners, actions, and API for Orchestral
Documentation
use std::path::Path;

use serde::Deserialize;
use serde_json::{json, Value};

#[derive(Debug, Clone, Deserialize)]
pub(super) struct DocumentUpdate {
    pub(super) path: String,
    pub(super) content: String,
}

pub(super) fn parse_document_updates(patch_spec: &Value) -> Result<Vec<DocumentUpdate>, String> {
    let updates = patch_spec
        .get("updates")
        .cloned()
        .ok_or_else(|| "patch_spec missing updates".to_string())?;
    serde_json::from_value::<Vec<DocumentUpdate>>(updates)
        .map_err(|err| format!("parse document patch updates failed: {}", err))
}

#[derive(Debug)]
pub(super) struct DocumentInspectionSummary {
    pub(super) file_name: String,
    pub(super) stem: String,
    pub(super) suggested_title: Option<String>,
    pub(super) title: Option<String>,
    pub(super) missing_title: bool,
    pub(super) todo_count: usize,
    pub(super) todo_lines: Vec<Value>,
    pub(super) heading_count: usize,
    pub(super) headings: Vec<Value>,
    pub(super) line_count: usize,
}

pub(super) fn inspect_document_content(path: &Path, content: &str) -> DocumentInspectionSummary {
    let mut todo_lines = Vec::new();
    let mut headings = Vec::new();
    let mut title = None;
    let mut first_nonempty = None;

    for (index, line) in content.lines().enumerate() {
        let trimmed = line.trim();
        if first_nonempty.is_none() && !trimmed.is_empty() {
            first_nonempty = Some(trimmed.to_string());
        }
        if trimmed.contains("TODO") {
            todo_lines.push(json!({
                "line": index + 1,
                "text": trimmed,
            }));
        }
        if trimmed.starts_with('#') {
            let hashes = trimmed.chars().take_while(|ch| *ch == '#').count();
            let text = trimmed[hashes..].trim().to_string();
            headings.push(json!({
                "line": index + 1,
                "level": hashes,
                "text": text,
            }));
            if title.is_none() && hashes == 1 && !text.is_empty() {
                title = Some(text);
            }
        }
    }

    let missing_title = first_nonempty
        .as_deref()
        .map(|line| !line.starts_with("# "))
        .unwrap_or(true);

    DocumentInspectionSummary {
        file_name: path
            .file_name()
            .and_then(|value| value.to_str())
            .unwrap_or_default()
            .to_string(),
        stem: path
            .file_stem()
            .and_then(|value| value.to_str())
            .unwrap_or_default()
            .to_string(),
        suggested_title: suggested_title_from_stem(
            path.file_stem()
                .and_then(|value| value.to_str())
                .unwrap_or_default(),
        ),
        title,
        missing_title,
        todo_count: todo_lines.len(),
        todo_lines,
        heading_count: headings.len(),
        headings,
        line_count: content.lines().count(),
    }
}

pub(super) fn suggested_title_from_stem(stem: &str) -> Option<String> {
    let words = stem
        .split(|ch: char| matches!(ch, '-' | '_' | '.' | '/' | '\\') || ch.is_whitespace())
        .filter(|part| !part.is_empty())
        .map(title_case_word)
        .collect::<Vec<_>>();
    if words.is_empty() {
        None
    } else {
        Some(words.join(" "))
    }
}

fn title_case_word(word: &str) -> String {
    let mut chars = word.chars();
    let Some(first) = chars.next() else {
        return String::new();
    };
    let mut out = String::new();
    out.extend(first.to_uppercase());
    out.push_str(&chars.as_str().to_ascii_lowercase());
    out
}