Skip to main content

agentstatedeveloper_cli/commands/
recipe.rs

1//! `asd recipe <recipe-name> <query>` — structured change-intent plans
2//! (Plan C t-004). First concrete recipe: classify-test-migration. Sets
3//! the pattern for future recipes (Plan C+).
4
5use anyhow::Result;
6use clap::{Args, Subcommand};
7
8use agentstatedeveloper_core::{
9    AsgIndexStore, Engine, FtsFilters, SearchFtsDb,
10    recipes::{classify_test_migration, migrate_stale_tests},
11};
12
13use crate::config::Config;
14
15#[derive(Debug, Subcommand)]
16pub enum RecipeCmd {
17    /// Classify test-tier symbols matching a query into migration actions
18    /// (Delete / Gate / Run / KeepAsCovered / Review) based on their
19    /// role-tagged ledger entries.
20    #[command(name = "classify-test-migration")]
21    ClassifyTestMigration(ClassifyArgs),
22
23    /// Plan F t-002: build a migration plan for stale test files. Adds
24    /// the Move action when a Mapping ledger entry carries a `move_to`
25    /// path in its body; otherwise falls back to the classify
26    /// decision tree.
27    #[command(name = "migrate-stale-tests")]
28    MigrateStaleTests(ClassifyArgs),
29}
30
31#[derive(Debug, Args)]
32pub struct ClassifyArgs {
33    /// Search query — finds candidate test symbols.
34    #[arg(required = true, num_args = 1..)]
35    pub query: Vec<String>,
36
37    /// Number of top candidate symbols to classify (default: 50).
38    #[arg(long, default_value = "50")]
39    pub limit: usize,
40}
41
42pub fn run(cfg: &Config, cmd: RecipeCmd) -> Result<()> {
43    match cmd {
44        RecipeCmd::ClassifyTestMigration(args) => run_recipe(cfg, args, |e, i, q, query| {
45            classify_test_migration(e, i, q, query)
46        }),
47        RecipeCmd::MigrateStaleTests(args) => run_recipe(cfg, args, |e, i, q, query| {
48            migrate_stale_tests(e, i, q, query)
49        }),
50    }
51}
52
53/// Shared candidate-resolution + dispatch shell for every test-recipe.
54/// New recipes just pass a different builder closure.
55fn run_recipe<F>(cfg: &Config, args: ClassifyArgs, build: F) -> Result<()>
56where
57    F: FnOnce(
58        &Engine,
59        &AsgIndexStore,
60        &[String],
61        &str,
62    ) -> agentstatedeveloper_core::recipes::Recipe,
63{
64    let query = args.query.join(" ");
65    let engine = Engine::open_sqlite(&cfg.db_path)?;
66    let index = AsgIndexStore::from_engine(&engine);
67
68    let fts = SearchFtsDb::open(&cfg.db_path).ok();
69    let candidate_qnames: Vec<String> = if let Some(fts) = fts {
70        let filters = FtsFilters {
71            kind: None,
72            language: None,
73            include_tests: true,
74            tests_only: true,
75            exclude_terms: vec![],
76            paths_filter: vec![],
77            exclude_paths: vec![],
78            exclude_languages: vec![],
79        };
80        fts.search(&query, &filters, args.limit)
81            .unwrap_or_default()
82            .into_iter()
83            .map(|h| h.qname)
84            .collect()
85    } else {
86        Vec::new()
87    };
88
89    let recipe = build(&engine, &index, &candidate_qnames, &query);
90    println!("{}", serde_json::to_string_pretty(&recipe)?);
91    Ok(())
92}