capability-example 0.1.0

A framework for managing skill tree growth and configuration using automated and manual strategies, ideal for AI-driven environments.
Documentation
// ---------------- [ File: capability-example/src/manual_grower_flow_cli.rs ]
crate::ix!();

#[derive(Debug, StructOpt, Builder, Getters, Setters, Clone)]
#[builder(setter(into))]
#[getset(get = "pub", set = "pub")]
#[structopt(
    name = "ManualGrowerFlow",
    about = "Runs the step-by-step, human-in-the-loop growth flow"
)]
pub struct ManualGrowerFlowCliArgs {

    /// The directory where we store/load partial model state
    #[structopt(long, help = "Path to partial model JSON directory")]
    growing_patch_bay: PathBuf,

    /// Optional TOML file with all GrowerInputs
    #[structopt(
        long = "toml-inputs-file",
        help = "If provided, override CLI-based GrowerInputs"
    )]
    toml_inputs_file: PathBuf,

    #[structopt(long = "force-override-drift")]
    force_override_drift: bool,

    /// Current query string
    #[structopt(long, default_value = "query.txt")]
    query_file: PathBuf,

    /// Whether to clear the clipboard at startup
    #[structopt(long = "clear-clipboard")]
    clear_clipboard: bool,

    /// An optional directory for storing fuzzy-parse failures from the clipboard
    #[structopt(long = "failed-parse-dir")]
    failed_parse_dir: Option<PathBuf>,
}

/// Now we modify our main logic to:
///  (A) detect which strategy to use (manual vs. automated)
///  (B) for each missing partial field, call the appropriate fill method from the strategy
///  (C) if the fill method returns `Some(...)`, we store that in the partial model
///  (D) if it returns `None`, we exit early (in the manual scenario)
///
impl ManualGrowerFlowCliArgs {

    /// The top-level public method.  
    /// Splits the logic into separate subroutines for clarity.
    #[instrument(level = "trace", skip_all)]
    pub async fn run_with_strategy<S: GrowerFlowStrategy>(
        &self,
        strategy: &S,
    ) -> Result<(), ManualGrowerFlowError> {
        trace!("Starting run_with_strategy() for CLI args: {:?}", self);

        // Step A) Possibly clear the clipboard
        self.maybe_clear_clipboard();

        // Step B) Try reading the clipboard raw text
        let raw_clipboard: String = match self.read_raw_clipboard_text() {
            Ok(txt) => txt,
            Err(e) => {
                warn!("Could not read raw clipboard => ignoring snippet. Error: {:?}", e);
                String::new()
            }
        };

        // Step C) Attempt parsing the snippet (in top-down order)
        let snippet: Option<ClipboardSnippet> =
            self.parse_clipboard_snippet_in_topdown_order(&raw_clipboard);

        // Step D) If we got a snippet, handle it; otherwise fallback to TOML
        if let Some(sn) = snippet {
            self.handle_parsed_snippet(sn, strategy).await
        } else {
            self.fallback_toml_flow(strategy).await
        }
    }

    // ----------------------------------------------------------------------------------
    // Subroutine #1: Possibly clear the clipboard
    // ----------------------------------------------------------------------------------
    fn maybe_clear_clipboard(&self) {
        if *self.clear_clipboard() {
            trace!("User requested --clear-clipboard => clearing the clipboard now.");
            if let Err(e) = system_clear_clipboard() {
                warn!("Could not clear system clipboard: {:?}", e);
            }
        }
    }

    // ----------------------------------------------------------------------------------
    // Subroutine #2: Read raw text from the clipboard
    // ----------------------------------------------------------------------------------
    pub fn read_raw_clipboard_text(&self) -> Result<String, FuzzyClipboardParseError> {
        // A "raw" version that does no JSON parse at all, just string
        let mut ctx = ClipboardContext::new()
            .map_err(|_| FuzzyClipboardParseError::ClipboardContextCreationFailed)?;
        ctx.get_contents()
            .map_err(|_| FuzzyClipboardParseError::ClipboardGetContentsFailed)
    }

    // ----------------------------------------------------------------------------------
    // Subroutine #3: Attempt parse in top-down order, ignoring `StrippedStringSkeleton` fuzzy parse
    // ----------------------------------------------------------------------------------
    pub fn parse_clipboard_snippet_in_topdown_order(
        &self,
        raw_clipboard: &str,
    ) -> Option<ClipboardSnippet> {
        // a) JustifiedGrowerTreeConfiguration
        if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedGrowerTreeConfiguration>(
            raw_clipboard,
            "JustifiedGrowerTreeConfiguration",
            self.failed_parse_dir().as_ref(),
        ) {
            return Some(ClipboardSnippet::JustifiedGrowerTreeConfiguration(parsed));
        }

        // b) JustifiedStringSkeleton
        if let Ok(Some(parsed)) = attempt_snippet_parse::<JustifiedStringSkeleton>(
            raw_clipboard,
            "JustifiedStringSkeleton",
            self.failed_parse_dir().as_ref(),
        ) {
            return Some(ClipboardSnippet::JustifiedStringSkeleton(parsed));
        }

        // c) We skip `StrippedStringSkeleton` in fuzzy mode because it lacks FuzzyFromJsonValue.

        // d) CoreStringSkeleton
        if let Ok(Some(parsed)) = attempt_snippet_parse::<CoreStringSkeleton>(
            raw_clipboard,
            "CoreStringSkeleton",
            self.failed_parse_dir().as_ref(),
        ) {
            return Some(ClipboardSnippet::CoreStringSkeleton(parsed));
        }

        // e) AnnotatedLeafHolderExpansions
        if let Ok(Some(parsed)) = attempt_snippet_parse::<AnnotatedLeafHolderExpansions>(
            raw_clipboard,
            "AnnotatedLeafHolderExpansions",
            self.failed_parse_dir().as_ref(),
        ) {
            return Some(ClipboardSnippet::AnnotatedLeafHolderExpansions(parsed));
        }

        None
    }

    // ----------------------------------------------------------------------------------
    // Subroutine #4: We got a snippet => see if we can apply it; else ignore
    // ----------------------------------------------------------------------------------
    pub async fn handle_parsed_snippet<S: GrowerFlowStrategy>(
        &self,
        snippet: ClipboardSnippet,
        strategy: &S,
    ) -> Result<(), ManualGrowerFlowError> {
        match snippet.target_name() {
            Some(tn) if !tn.trim().is_empty() => {
                let file_path = self.growing_patch_bay().join(format!("{}.json", tn));
                if !file_path.exists() {
                    error!("Partial file for target='{tn}' does not exist => ignoring snippet => done.");
                    return Ok(());
                }

                let mut snippet_partial = match PartiallyGrownModel::load_from_file_fuzzy(&file_path).await {
                    Ok(pm) => pm,
                    Err(e) => {
                        error!("Could not parse partial file for target='{tn}': {e:?} => skipping snippet => done.");
                        return Ok(());
                    }
                };

                let missing_step = snippet_partial.validate().err();
                let partial_rank = missing_field_rank(&missing_step);
                let snippet_rank = snippet_field_rank(&snippet);
                trace!("Target='{}': partial missing={:?} => rank={} ; snippet rank={}", tn, missing_step, partial_rank, snippet_rank);

                if partial_rank == 0 {
                    warn!("Partial is already fully valid => ignoring snippet => done.");
                    return Ok(());
                } 
                else if snippet_rank < partial_rank {
                    // Instead of exiting, we proceed with the partial model as-is
                    warn!("Snippet is for an earlier step => ignoring snippet, continuing from partial as-is...");

                    // We reuse the same loop, but we haven't changed snippet_partial, so we just pass it
                    // to post_snippet_partial_loop so it will attempt to fill whatever is missing 
                    // (the snippet is ignored).
                    self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
                }
                else if snippet_rank == partial_rank {
                    // Perfect match => apply snippet
                    snippet_partial.apply_clipboard_snippet(snippet);
                    snippet_partial.save_to_file(&file_path).await?;
                    self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
                }
                else {
                    // snippet_rank > partial_rank => snippet is ahead
                    if snippet_rank == partial_rank + 1 {
                        info!("Snippet is exactly one step ahead => applying anyway...");
                        snippet_partial.apply_clipboard_snippet(snippet);
                        snippet_partial.save_to_file(&file_path).await?;
                        self.post_snippet_partial_loop(snippet_partial, file_path, strategy).await
                    } else {
                        error!("Snippet is more than one step ahead => ignoring snippet => done.");
                        Ok(())
                    }
                }
            }
            _ => {
                warn!("Clipboard snippet has no valid target_name => ignoring => done.");
                Ok(())
            }
        }
    }

    /// A small subroutine to handle the repeated logic after we've applied a snippet 
    /// but the partial might still not be fully valid.
    async fn post_snippet_partial_loop<S: GrowerFlowStrategy>(
        &self,
        mut partial: PartiallyGrownModel,
        file_path: PathBuf,
        strategy: &S,
    ) -> Result<(), ManualGrowerFlowError> {
        loop {
            match partial.validate() {
                Ok(_) => {
                    info!("Partial is now fully valid => finalize => done.");
                    let fm = GrowerModel::finalize_from_valid_partial(partial.clone())
                        .map_err(ManualGrowerFlowError::GrowerModelGenError)?;
                    fm.save_to_file(&file_path).await?;
                    return Ok(());
                }
                Err(missing) => {
                    let gi = partial.grower_inputs().clone().unwrap();
                    if let Err(e2) = manual_flow_try_to_fix_missing_field(
                        strategy,
                        &mut partial,
                        &gi,
                        &missing,
                        self,
                    )
                    .await
                    {
                        warn!("Could not fix missing field => exiting: {:?}", e2);
                        return Ok(());
                    }
                    partial.save_to_file(&file_path).await?;
                }
            }
        }
    }

    // ----------------------------------------------------------------------------------
    // Subroutine #5: No snippet => fallback to TOML approach
    // ----------------------------------------------------------------------------------
    pub async fn fallback_toml_flow<S: GrowerFlowStrategy>(
        &self,
        strategy: &S,
    ) -> Result<(), ManualGrowerFlowError> {
        trace!("No parseable snippet => continuing with TOML flow.");

        let grower_inputs = self.load_and_validate_toml()?;
        let mut partial_model = self.open_or_create_partial(&grower_inputs).await?;
        partial_model = self.resolve_input_drift(&grower_inputs, partial_model).await?;

        loop {
            match partial_model.validate() {
                Ok(()) => {
                    info!("Partial is fully valid => finalize => done.");
                    let final_model = GrowerModel::finalize_from_valid_partial(partial_model.clone())
                        .map_err(ManualGrowerFlowError::GrowerModelGenError)?;
                    let partial_path = self.calculate_partial_path(&grower_inputs);
                    final_model.save_to_file(&partial_path).await?;
                    return Ok(());
                }
                Err(missing) => {
                    if let Err(e) = manual_flow_try_to_fix_missing_field(
                        strategy,
                        &mut partial_model,
                        &grower_inputs,
                        &missing,
                        self,
                    )
                    .await
                    {
                        warn!("Could not fill missing field => exiting. Error={:?}", e);
                        return Ok(());
                    }
                }
            }
        }
    }


    /// Figures out the correct partial path for a given grower_inputs
    pub fn calculate_partial_path(&self, grower_inputs: &GrowerInputs) -> PathBuf {
        self.growing_patch_bay
            .join(format!("{}.json", grower_inputs.target()))
    }

    #[instrument(level = "trace", skip_all)]
    fn load_and_validate_toml(&self) -> Result<GrowerInputs, ManualGrowerFlowError> {
        debug!("Loading GrowerInputs from: {:?}", self.toml_inputs_file());
        let gi = GrowerInputs::from_toml_file(self.toml_inputs_file())?;
        gi.validate()?;
        Ok(gi)
    }

    #[instrument(level = "trace", skip_all)]
    async fn open_or_create_partial(
        &self,
        grower_inputs: &GrowerInputs,
    ) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
        let partial_path = self.calculate_partial_path(grower_inputs);
        maybe_write_initial_partial_model_file(&partial_path).await?;
        let partial_model = PartiallyGrownModel::load_from_file_fuzzy(&partial_path).await?;
        Ok(partial_model)
    }

    #[instrument(level = "trace", skip_all)]
    async fn resolve_input_drift(
        &self,
        grower_inputs: &GrowerInputs,
        mut partial_model: PartiallyGrownModel,
    ) -> Result<PartiallyGrownModel, ManualGrowerFlowError> {
        let partial_path = self.calculate_partial_path(grower_inputs);

        if let Some(pm_grower_inputs) = partial_model.grower_inputs() {
            if pm_grower_inputs != grower_inputs {
                if !self.force_override_drift() {
                    if partial_model.essentially_empty() {
                        warn!("Inputs drift but partial is basically empty => overwriting grower_inputs");
                        partial_model = PartiallyGrownModel::from(grower_inputs.clone());
                        partial_model.save_to_file(&partial_path).await?;
                    } else {
                        warn!("Inputs drift => exiting early unless forced override is used.");
                        return Ok(partial_model);
                    }
                } else {
                    warn!("Inputs drift => forced override => clobbering partial");
                    partial_model = PartiallyGrownModel::from(grower_inputs.clone());
                    partial_model.save_to_file(&partial_path).await?;
                }
            }
        } else {
            partial_model.set_grower_inputs(Some(grower_inputs.clone()));
            partial_model.save_to_file(&partial_path).await?;
        }

        Ok(partial_model)
    }
}