agentstatedeveloper-cli 1.4.9

asd CLI — command-line interface for AgentStateDeveloper
Documentation
//! `asd recipe <recipe-name> <query>` — structured change-intent plans
//! (Plan C t-004). First concrete recipe: classify-test-migration. Sets
//! the pattern for future recipes (Plan C+).

use anyhow::Result;
use clap::{Args, Subcommand};

use agentstatedeveloper_core::{
    AsgIndexStore, Engine, FtsFilters, SearchFtsDb,
    recipes::{classify_test_migration, migrate_stale_tests},
};

use crate::config::Config;

#[derive(Debug, Subcommand)]
pub enum RecipeCmd {
    /// Classify test-tier symbols matching a query into migration actions
    /// (Delete / Gate / Run / KeepAsCovered / Review) based on their
    /// role-tagged ledger entries.
    #[command(name = "classify-test-migration")]
    ClassifyTestMigration(ClassifyArgs),

    /// Plan F t-002: build a migration plan for stale test files. Adds
    /// the Move action when a Mapping ledger entry carries a `move_to`
    /// path in its body; otherwise falls back to the classify
    /// decision tree.
    #[command(name = "migrate-stale-tests")]
    MigrateStaleTests(ClassifyArgs),
}

#[derive(Debug, Args)]
pub struct ClassifyArgs {
    /// Search query — finds candidate test symbols.
    #[arg(required = true, num_args = 1..)]
    pub query: Vec<String>,

    /// Number of top candidate symbols to classify (default: 50).
    #[arg(long, default_value = "50")]
    pub limit: usize,
}

pub fn run(cfg: &Config, cmd: RecipeCmd) -> Result<()> {
    match cmd {
        RecipeCmd::ClassifyTestMigration(args) => run_recipe(cfg, args, |e, i, q, query| {
            classify_test_migration(e, i, q, query)
        }),
        RecipeCmd::MigrateStaleTests(args) => run_recipe(cfg, args, |e, i, q, query| {
            migrate_stale_tests(e, i, q, query)
        }),
    }
}

/// Shared candidate-resolution + dispatch shell for every test-recipe.
/// New recipes just pass a different builder closure.
fn run_recipe<F>(cfg: &Config, args: ClassifyArgs, build: F) -> Result<()>
where
    F: FnOnce(
        &Engine,
        &AsgIndexStore,
        &[String],
        &str,
    ) -> agentstatedeveloper_core::recipes::Recipe,
{
    let query = args.query.join(" ");
    let engine = Engine::open_sqlite(&cfg.db_path)?;
    let index = AsgIndexStore::from_engine(&engine);

    let fts = SearchFtsDb::open(&cfg.db_path).ok();
    let candidate_qnames: Vec<String> = if let Some(fts) = fts {
        let filters = FtsFilters {
            kind: None,
            language: None,
            include_tests: true,
            tests_only: true,
            exclude_terms: vec![],
            paths_filter: vec![],
            exclude_paths: vec![],
            exclude_languages: vec![],
        };
        fts.search(&query, &filters, args.limit)
            .unwrap_or_default()
            .into_iter()
            .map(|h| h.qname)
            .collect()
    } else {
        Vec::new()
    };

    let recipe = build(&engine, &index, &candidate_qnames, &query);
    println!("{}", serde_json::to_string_pretty(&recipe)?);
    Ok(())
}