main-spex 0.23.0

AI-first development framework that turns specifications into deterministic, verifiable code
use std::cmp::Reverse;
use std::process::ExitCode;

use anstream::println;
use eros::bail;

use crate::error::Result;
use crate::support::text::level_two_sections;

#[cfg(test)]
#[path = "migration_guide_test.rs"]
mod migration_guide_test;

const MIGRATION_GUIDE: &str =
    include_str!("../../../templates/skills/spex-update/references/migration_guide.md");

type Version = (u64, u64, u64);

pub fn print_since(from_version: &str) -> Result<ExitCode> {
    let sections = migration_guide_since(MIGRATION_GUIDE, from_version)?;
    if sections.is_empty() {
        println!("no migration needed");
    } else {
        println!("{}", sections.join("\n\n"));
    }
    Ok(ExitCode::SUCCESS)
}

fn migration_guide_since(guide: &str, from_version: &str) -> Result<Vec<String>> {
    let Some(from) = parse_from_version(from_version) else {
        bail!(
            "invalid version `{from_version}` — expected MAJOR, MAJOR.MINOR or MAJOR.MINOR.PATCH"
        );
    };

    let mut newer: Vec<(Version, String)> = level_two_sections(guide)
        .into_iter()
        .filter_map(|section| {
            let version = parse_heading_version(section.lines().next()?.strip_prefix("## ")?)?;
            (version > from).then(|| (version, section.trim_end().to_string()))
        })
        .collect();
    newer.sort_by_key(|(version, _)| Reverse(*version));
    Ok(newer.into_iter().map(|(_, section)| section).collect())
}

fn parse_numbers(text: &str) -> Option<Vec<u64>> {
    text.split('.').map(|part| part.parse().ok()).collect()
}

fn parse_heading_version(text: &str) -> Option<Version> {
    let [major, minor, patch] = parse_numbers(text)?[..] else {
        return None;
    };
    Some((major, minor, patch))
}

fn parse_from_version(text: &str) -> Option<Version> {
    match parse_numbers(text)?[..] {
        [major] => Some((major, 0, 0)),
        [major, minor] => Some((major, minor, 0)),
        [major, minor, patch] => Some((major, minor, patch)),
        _ => None,
    }
}