#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn epic_health(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "epic")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let store = store_with_config(store, config);
let issues = match store.list_all() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let index = crate::epics::EpicIndex::build(&store.config, &issues);
let targets = match epic_reference_targets(&store.config, &store) {
Ok(targets) => targets,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let (graph_issues, done_column) = match store.graph_source() {
Ok(source) => source,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let graph = crate::commands::graph_over(&graph_issues, &done_column);
match index.rollup(&store.config, &reference, &targets, &graph) {
Ok(rollup) => Ok(success_result(
epic_health_text(&rollup),
epic_rollup_value(&rollup),
)),
Err(error) => Ok(epic_refusal(error)),
}
}
pub(super) fn health_history(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let raw = match req_string(args, "scope") {
Ok(raw) => raw,
Err(()) => return Ok(proto_error()),
};
let Some(scope) = crate::health::HealthScope::parse(&raw) else {
return Ok(error_result(format!(
"invalid input: `{raw}` is not a scope; expected issue:<n>, project:<id>, \
epic:<id>, cycle:<id> or milestone:<id>"
)));
};
let _ = store;
let document = match crate::commands::export::run_health_core(config) {
Ok(document) => document,
Err(error) => return Ok(error_result(crate::error::render_error(&error))),
};
let text = crate::commands::health::render_history(&document, &scope);
let mut structured = health_scope_value(&document, &scope);
structured["policy"] = health_policy_value();
Ok(success_result(text, structured))
}
pub(super) fn health_record(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let selector = match selector(args, "issue") {
Ok(selector) => selector,
Err(()) => return Ok(proto_error()),
};
let state = match req_string(args, "state") {
Ok(state) => state,
Err(()) => return Ok(proto_error()),
};
let rationale = match req_string(args, "rationale") {
Ok(rationale) => rationale,
Err(()) => return Ok(proto_error()),
};
let actor = match opt_string(args, "actor") {
Ok(actor) => actor,
Err(()) => return Ok(proto_error()),
};
let date = match opt_string(args, "date") {
Ok(date) => date,
Err(()) => return Ok(proto_error()),
};
let over = match opt_bool(args, "override") {
Ok(value) => value.unwrap_or(false),
Err(()) => return Ok(proto_error()),
};
let confirm = match opt_bool(args, "confirm") {
Ok(value) => value.unwrap_or(false),
Err(()) => return Ok(proto_error()),
};
if over && !confirm {
return Ok(error_result(
"refusing to override derived health without `confirm: true`: an override is \
an explicit act about a whole scope, and it layers over the derived value \
rather than replacing it"
.to_string(),
));
}
let number = match store.resolve_issue(&selector) {
Ok(issue) => issue.number,
Err(error) => return Ok(error_result(crate::error::render_error(&error))),
};
let actor = actor.unwrap_or_else(crate::storage::actor_identity);
let date = date.unwrap_or_else(crate::model::today);
let source = if over {
crate::health::HealthSource::Override
} else {
crate::health::HealthSource::Current
};
match crate::commands::health::record(
store,
crate::health::HealthScope::of_issue(number),
&state,
&actor,
&date,
&rationale,
source,
) {
Ok(applied) => {
let stored = applied
.issue
.health
.as_ref()
.and_then(|health| health.state.clone())
.unwrap_or_default();
let text = crate::commands::health::render_mutation(
number,
&stored,
source,
crate::health::HealthPolicy::v1().version(),
);
let (_, resolution) = crate::workflow::active_resolution(config);
let mut structured = issue_value(&applied.issue, &resolution);
structured["health_recorded"] = json!({
"state": stored,
"source": source.as_str(),
"policy_version": crate::health::HealthPolicy::v1().version(),
"history_entry": {
"kind": applied.event.kind.as_str(),
"timestamp": applied.event.timestamp,
"actor": applied.event.actor,
"detail": applied.event.detail,
},
});
Ok(success_result(text, structured))
}
Err(error) => Ok(error_result(crate::error::render_error(&error))),
}
}
pub(super) fn health_rollup(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let kind = match req_string(args, "scope") {
Ok(kind) => kind,
Err(()) => return Ok(proto_error()),
};
let id = match req_string(args, "id") {
Ok(id) => id,
Err(()) => return Ok(proto_error()),
};
let Some(scope) = crate::health::HealthScope::parse(&format!("{}:{}", kind.trim(), id.trim()))
else {
return Ok(error_result(format!(
"invalid input: `{kind}` is not a health scope; expected issue, project, epic, \
cycle or milestone"
)));
};
let _ = store;
let document = match crate::commands::export::run_health_core(config) {
Ok(document) => document,
Err(error) => return Ok(error_result(crate::error::render_error(&error))),
};
let text = crate::commands::health::render_rollup(&document, &scope);
let mut structured = health_scope_value(&document, &scope);
structured["policy"] = health_policy_value();
Ok(success_result(text, structured))
}
pub(super) fn health_scope(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let scope = match health_requested_scope(store, args) {
Ok(scope) => scope,
Err(result) => return Ok(result),
};
let document = match crate::commands::export::run_health_core(config) {
Ok(document) => document,
Err(error) => return Ok(error_result(crate::error::render_error(&error))),
};
let text = crate::commands::health::render_show(&document, &scope);
let mut structured = health_scope_value(&document, &scope);
structured["policy"] = health_policy_value();
Ok(success_result(text, structured))
}
pub(super) fn project_health(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "project")?;
let (config, _path) = match registry_config(store) {
Ok(loaded) => loaded,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let issues = match store.list() {
Ok(issues) => issues,
Err(e) => return Ok(error_result(crate::error::render_error(&e))),
};
let registry = store_with_config(store, config);
match crate::commands::view::project_reports(®istry, &issues) {
Ok((index, reports)) => match index.resolve(&reference) {
Ok(project) => match reports.get(&project.id) {
Some(report) => Ok(success_result(
project_health_text(report),
project_health_value(report),
)),
None => Ok(error_result(format!(
"internal error: the project service returned no health value for `{}`",
project.id
))),
},
Err(error) => Ok(project_refusal(error)),
},
Err(error) => Ok(error_result(crate::error::render_error(&error))),
}
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"project_health" => Some(project_health(config, store, arguments)),
"epic_health" => Some(epic_health(config, store, arguments)),
"health_scope" => Some(health_scope(config, store, arguments)),
"health_rollup" => Some(health_rollup(config, store, arguments)),
"health_history" => Some(health_history(config, store, arguments)),
"health_record" => Some(health_record(config, store, arguments)),
_ => None,
}
}
pub(super) fn health_policy_value() -> Value {
let policy = crate::health::HealthPolicy::v1();
let states: Vec<&str> = crate::health::HealthState::ALL
.iter()
.map(|state| state.as_str())
.collect();
let precedence: Vec<&str> = policy
.precedence
.iter()
.map(|state| state.as_str())
.collect();
json!({
"version": policy.version(),
"states": states,
"precedence": precedence,
"empty_scope": policy.empty_scope().as_str(),
"missing_reference": policy.missing_reference().as_str(),
})
}
pub(super) fn health_scope_value(
document: &crate::health::PortableHealth,
scope: &crate::health::HealthScope,
) -> Value {
let entry = document.scope(scope);
let diagnostics: Vec<Value> = document
.diagnostics_for(scope)
.into_iter()
.map(health_diagnostic_value)
.collect();
json!({
"scope": scope.as_str(),
"state": entry.map(|entry| entry.state.clone()),
"source": entry.map(|entry| entry.source.clone()),
"recorded": entry.and_then(|entry| entry.recorded.clone()),
"recorded_policy_version": entry.and_then(|entry| entry.recorded_policy_version.clone()),
"updated_at": entry.and_then(|entry| entry.updated_at.clone()),
"events": entry.map(|entry| entry.events.iter().map(|event| json!({
"kind": event.kind,
"timestamp": event.timestamp,
"actor": event.actor,
"detail": event.detail,
})).collect::<Vec<Value>>()),
"rollup": entry.and_then(|entry| entry.rollup.as_ref()).map(|rollup| json!({
"state": rollup.state,
"policy_version": rollup.policy_version,
"source": rollup.source,
"contributors": rollup.contributors.iter().map(|contributor| json!({
"number": contributor.number,
"state": contributor.state,
"recorded": contributor.recorded,
})).collect::<Vec<Value>>(),
})),
"diagnostics": diagnostics,
})
}
pub(super) fn health_diagnostic_value(
diagnostic: &crate::health::PortableHealthDiagnostic,
) -> Value {
let shared = diagnostic.to_diagnostic();
json!({
"kind": shared.kind.as_str(),
"scope": shared.scope.as_str(),
"reference": shared.reference,
"field": shared.field,
"target": shared.target.map(|target| target.as_str()),
"state": shared.state,
"message": shared.message,
"remedy": shared.remedy,
})
}
pub(super) fn health_requested_scope(
store: &Store,
args: &Value,
) -> std::result::Result<crate::health::HealthScope, Value> {
let issue = opt_selector(args, "issue").map_err(|()| proto_error())?;
let scope = opt_string(args, "scope").map_err(|()| proto_error())?;
match (issue, scope) {
(Some(issue), None) => store
.resolve_issue(&issue)
.map(|issue| crate::health::HealthScope::of_issue(issue.number))
.map_err(|error| error_result(crate::error::render_error(&error))),
(None, Some(raw)) => crate::health::HealthScope::parse(&raw).ok_or_else(|| {
error_result(format!(
"invalid input: `{raw}` is not a scope; expected issue:<n>, project:<id>, \
epic:<id>, cycle:<id> or milestone:<id>"
))
}),
(None, None) => Err(error_result(
"invalid input: name either `issue` or `scope`".to_string(),
)),
(Some(_), Some(_)) => Err(error_result(
"invalid input: name exactly one of `issue` or `scope`".to_string(),
)),
}
}
pub(super) fn proto_error() -> Value {
error_result("invalid input: an argument has the wrong type".to_string())
}