tix-tracker 0.12.0

A terminal issue tracker: markdown + YAML frontmatter issues with a kanban TUI and CLI
Documentation
#[allow(clippy::wildcard_imports)]
use super::*;

pub(super) fn create_initiative(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    let name = req_string(args, "name")?;
    let outcome = trimmed_optional(opt_string(args, "outcome")?);
    let owner = trimmed_optional(opt_string(args, "owner")?);
    let status = trimmed_optional(opt_string(args, "status")?);
    let priority = initiative_priority(opt_string(args, "priority")?);
    let labels = opt_string(args, "labels")?
        .map(|text| crate::commands::parse_csv_list(&text))
        .unwrap_or_default();
    let target_date = trimmed_optional(opt_string(args, "target_date")?);
    let projects = opt_string(args, "projects")?
        .map(|text| crate::commands::parse_csv_list(&text))
        .unwrap_or_default();
    let epics = opt_string(args, "epics")?
        .map(|text| crate::commands::parse_csv_list(&text))
        .unwrap_or_default();
    let milestones = opt_string(args, "milestones")?
        .map(|text| crate::commands::parse_csv_list(&text))
        .unwrap_or_default();
    let (mut config, path) = match registry_config(store) {
        Ok(loaded) => loaded,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let new = NewInitiative {
        name,
        outcome,
        owner,
        status,
        priority,
        labels,
        target_date,
        projects,
        epics,
        milestones,
        extensions: crate::roadmap::registry::InitiativeExtensions::default(),
    };
    // The registry allocates the id and validates the whole record through its
    // atomic boundary, so a refusal persists nothing (C-008).
    match crate::roadmap::registry::create_initiative(&mut config, &path, &new) {
        Ok(id) => {
            let record = &config.initiatives[id.as_str()];
            Ok(success_result(
                format!("Created {} ({id})", new.name),
                initiative_record_value(id.as_str(), record),
            ))
        }
        Err(e) => Ok(error_result(crate::error::render_error(&e))),
    }
}

pub(super) fn delete_initiative(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    let reference = req_string(args, "initiative")?;
    // The schema declares `confirm` required, but a schema is documentation a
    // client may ignore; checked before anything is resolved, so a forgotten
    // confirmation writes nothing (the convention `delete_project` follows).
    if opt_bool(args, "confirm")? != Some(true) {
        return Ok(error_result(
            "invalid input: `confirm` must be true to delete an initiative — only the registry \
             entry is removed and no project, epic, milestone or issue is deleted"
                .to_string(),
        ));
    }
    let (mut config, path) = match registry_config(store) {
        Ok(loaded) => loaded,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let id = match crate::roadmap::registry::resolve_initiative_id(&config, &reference) {
        Ok(id) => id,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let Some(record) = crate::roadmap::registry::get(&config, &id).cloned() else {
        return Ok(error_result(format!(
            "invalid input: unknown initiative `{reference}`"
        )));
    };
    // Removes the registry entry **only**: the record's own references are
    // reported and nothing cascades, so no source entity is touched and no
    // association is silently repaired (C-005). The CLI's own sentence, through
    // the one `reference_inventory` renderer.
    match crate::roadmap::registry::delete_initiative(&mut config, &path, &id) {
        Ok(()) => Ok(success_result(
            format!(
                "Deleted initiative {} ({id})\nRemoved references (nothing cascades): {}",
                record.name,
                cmd_roadmap::reference_inventory(&record),
            ),
            json!({
                "id": id.as_str(),
                "name": record.name,
                "references": {
                    "projects": record.projects,
                    "epics": record.epics,
                    "milestones": record.milestones,
                },
            }),
        )),
        Err(e) => Ok(error_result(crate::error::render_error(&e))),
    }
}

pub(super) fn get_initiative(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    let reference = req_string(args, "initiative")?;
    let (config, _path) = match registry_config(store) {
        Ok(loaded) => loaded,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let id = match crate::roadmap::registry::resolve_initiative_id(&config, &reference) {
        Ok(id) => id,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let Some(record) = crate::roadmap::registry::get(&config, &id) else {
        return Ok(error_result(format!(
            "invalid input: unknown initiative `{reference}`"
        )));
    };
    // The CLI's own record rendering (references shown as recorded); the
    // structured channel adds the projection's resolution and diagnostics.
    let text = cmd_roadmap::initiative_record_lines(&id, record).join("\n");
    let row = match roadmap_row_by_id(&config, store, id.as_str()) {
        Ok(Some(row)) => row,
        Ok(None) => {
            return Ok(error_result(format!(
                "internal error: the roadmap projection returned no row for `{id}`"
            )));
        }
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let mut structured = initiative_record_value(id.as_str(), record);
    structured["references"] = json!(
        row.references
            .iter()
            .map(row_reference_value)
            .collect::<Vec<Value>>()
    );
    structured["summary"] = initiative_summary_value(&row.summary);
    structured["diagnostics"] = json!(
        row.diagnostics
            .iter()
            .map(roadmap_diagnostic_value)
            .collect::<Vec<Value>>()
    );
    Ok(success_result(text, structured))
}

pub(super) fn initiative_associate(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    initiative_association(store, args, true)
}

pub(super) fn initiative_dereference(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    initiative_association(store, args, false)
}

pub(super) fn list_initiatives(_config: &Config, store: &Store, _args: &Value) -> ToolOutcome {
    let (config, _path) = match registry_config(store) {
        Ok(loaded) => loaded,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    // The CLI's own listing, verbatim; the structured channel carries the same
    // records with their ids (C-002).
    let lines = cmd_roadmap::initiative_listing_lines(&config);
    let initiatives: Vec<Value> = crate::roadmap::registry::ids(&config)
        .map(|id| initiative_record_value(id, &config.initiatives[id]))
        .collect();
    Ok(success_result(
        lines.join("\n"),
        json!({ "initiatives": initiatives }),
    ))
}

pub(super) fn update_initiative(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
    let reference = req_string(args, "initiative")?;
    let name = opt_string(args, "name")?;
    let outcome = opt_string(args, "outcome")?;
    let owner = opt_string(args, "owner")?;
    let status = opt_string(args, "status")?;
    let priority = opt_string(args, "priority")?;
    let labels = opt_string(args, "labels")?;
    let target_date = opt_string(args, "target_date")?;
    let projects = opt_string(args, "projects")?;
    let epics = opt_string(args, "epics")?;
    let milestones = opt_string(args, "milestones")?;
    if name.is_none()
        && outcome.is_none()
        && owner.is_none()
        && status.is_none()
        && priority.is_none()
        && labels.is_none()
        && target_date.is_none()
        && projects.is_none()
        && epics.is_none()
        && milestones.is_none()
    {
        // The CLI's wording for the same refusal, from the one shared helper.
        return Ok(error_result(crate::error::render_error(
            &cmd_roadmap::initiative_edit_requires_a_field(),
        )));
    }
    let (mut config, path) = match registry_config(store) {
        Ok(loaded) => loaded,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    let id = match crate::roadmap::registry::resolve_initiative_id(&config, &reference) {
        Ok(id) => id,
        Err(e) => return Ok(error_result(crate::error::render_error(&e))),
    };
    // The patch shape `tix initiative edit` builds: `--name` always sets (a name
    // is never cleared, which the loader refuses), the other fields follow the
    // blank-clears convention.
    let edit = InitiativeEdit {
        name: name.map_or(
            crate::projects::Patch::Unchanged,
            crate::projects::Patch::Set,
        ),
        outcome: initiative_text_patch(outcome),
        owner: initiative_text_patch(owner),
        status: initiative_text_patch(status),
        priority: match priority {
            None => crate::projects::Patch::Unchanged,
            Some(text) if text.trim().is_empty() => crate::projects::Patch::Clear,
            Some(text) => crate::projects::Patch::Set(loose_priority(text.trim())),
        },
        labels: initiative_reference_patch(labels),
        target_date: initiative_text_patch(target_date),
        projects: initiative_reference_patch(projects),
        epics: initiative_reference_patch(epics),
        milestones: initiative_reference_patch(milestones),
    };
    match crate::roadmap::registry::edit_initiative(&mut config, &path, &id, &edit) {
        Ok(id) => {
            let record = &config.initiatives[id.as_str()];
            Ok(success_result(
                format!("Updated initiative {id}"),
                initiative_record_value(id.as_str(), record),
            ))
        }
        Err(e) => Ok(error_result(crate::error::render_error(&e))),
    }
}

pub(super) fn dispatch(
    config: &Config,
    store: &Store,
    name: &str,
    arguments: &Value,
) -> Option<ToolOutcome> {
    match name {
        "list_initiatives" => Some(list_initiatives(config, store, arguments)),
        "get_initiative" => Some(get_initiative(config, store, arguments)),
        "create_initiative" => Some(create_initiative(config, store, arguments)),
        "update_initiative" => Some(update_initiative(config, store, arguments)),
        "initiative_associate" => Some(initiative_associate(config, store, arguments)),
        "initiative_dereference" => Some(initiative_dereference(config, store, arguments)),
        "delete_initiative" => Some(delete_initiative(config, store, arguments)),
        _ => None,
    }
}