#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn analytics_burndown(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Some(value) = reject_hosted_source(args) {
return Ok(value);
}
let request = analytics_args(args)?;
match cmd_analytics::burndown_report(config, store, &request) {
Ok(report) => Ok(success_result(
report.text,
analytics_document_value(&report.document),
)),
Err(error) => Ok(error_result(crate::error::render_error(&error))),
}
}
pub(super) fn analytics_export(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Some(value) = reject_hosted_source(args) {
return Ok(value);
}
let request = analytics_args(args)?;
let as_of = opt_string(args, "as_of")?;
match cmd_analytics::export_document(config, store, &request, as_of.as_deref()) {
Ok(document) => match document.to_json_pretty() {
Ok(json) => Ok(success_result(json, analytics_document_value(&document))),
Err(error) => Ok(error_result(crate::error::render_error(&error))),
},
Err(error) => Ok(error_result(crate::error::render_error(&error))),
}
}
pub(super) fn analytics_metadata(config: &Config, _store: &Store, args: &Value) -> ToolOutcome {
if let Some(value) = reject_hosted_source(args) {
return Ok(value);
}
let modes: Vec<Value> = Mode::ALL
.into_iter()
.map(|mode| {
json!({
"mode": mode.as_str(),
"formula_version": mode.formula_version(),
"precision": precision_of(mode),
"unit_required": mode == Mode::Estimate,
"default": mode == Mode::Count,
})
})
.collect();
let diagnostics: Vec<Value> = AnalyticsDiagnosticKind::ALL
.into_iter()
.map(|kind| json!({ "kind": kind.as_str(), "remedy": kind.default_remedy() }))
.collect();
let declared = declared_units(config);
let structured = json!({
"scope_kinds": ScopeKind::ALL.map(ScopeKind::as_str),
"period": {
"source": "the scope's own planning dates (a cycle's start_date/due_date, a \
milestone's target date)",
"overridable": ["from", "to"],
"format": "YYYY-MM-DD",
"max_days": MAX_SERIES_DAYS,
},
"modes": modes,
"units": {
"rule": "estimate mode needs a unit the project's default board declares a \
conversion for; count mode is unitless",
"declared": declared,
},
"projection": {
"formula_version": PROJECTION_FORMULA_VERSION,
"statuses": ProjectionStatus::ALL.map(ProjectionStatus::as_str),
"origin": "as_of (the caller's day, never a clock)",
"end_date": "the scope's own planning end date, or `to`",
},
"coverage": Coverage::ALL.map(Coverage::as_str),
"snapshot": "a scope bound to a closed snapshot folds that snapshot's period and \
provenance; events recorded after its close are not contributions",
"diagnostics": diagnostics,
"export": { "format": CHART_FORMAT, "schema_version": CHART_SCHEMA_VERSION },
"source": "the local record only \u{2014} no hosted analytics service (C-009)",
});
let mut text = String::new();
text.push_str("source: the local record only (no hosted analytics service)\n");
text.push_str(&format!(
"scope_kinds: {}\n",
ScopeKind::ALL.map(ScopeKind::as_str).join(", ")
));
text.push_str(&format!(
"period: the scope's own planning dates, replaceable one bound at a time with \
from/to; YYYY-MM-DD; at most {MAX_SERIES_DAYS} days\n"
));
for mode in Mode::ALL {
text.push_str(&format!(
"mode: {name} formula={formula} precision={precision} unit={unit}{default}\n",
name = mode.as_str(),
formula = mode.formula_version(),
precision = precision_of(mode),
unit = if mode == Mode::Estimate {
"required"
} else {
"unitless"
},
default = if mode == Mode::Count { " default" } else { "" },
));
}
if declared.is_empty() {
text.push_str(
"units: the default board declares none, so estimate mode refuses with \
missing_conversion\n",
);
} else {
text.push_str(&format!(
"units: the default board declares: {}\n",
declared.join(", ")
));
}
text.push_str(&format!(
"coverage: {}\n",
Coverage::ALL.map(Coverage::as_str).join(", ")
));
text.push_str(&format!(
"projection: formula={PROJECTION_FORMULA_VERSION} statuses: {}; origin as_of; \
end_date the scope's planning end or to\n",
ProjectionStatus::ALL
.map(ProjectionStatus::as_str)
.join(", ")
));
text.push_str(
"snapshot: a scope bound to a closed snapshot folds the snapshot's own period and \
provenance\n",
);
text.push_str(&format!(
"diagnostics: {}\n",
AnalyticsDiagnosticKind::ALL
.map(AnalyticsDiagnosticKind::as_str)
.join(", ")
));
text.push_str(&format!(
"export: {CHART_FORMAT} schema {CHART_SCHEMA_VERSION}\n"
));
Ok(success_result(text, structured))
}
pub(super) fn analytics_projection(config: &Config, store: &Store, args: &Value) -> ToolOutcome {
if let Some(value) = reject_hosted_source(args) {
return Ok(value);
}
let request = analytics_args(args)?;
let as_of = req_string(args, "as_of")?;
match cmd_analytics::projection_report(config, store, &request, &as_of) {
Ok(report) => Ok(success_result(
report.text,
analytics_document_value(&report.document),
)),
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 {
"analytics_metadata" => Some(analytics_metadata(config, store, arguments)),
"analytics_burndown" => Some(analytics_burndown(config, store, arguments)),
"analytics_projection" => Some(analytics_projection(config, store, arguments)),
"analytics_export" => Some(analytics_export(config, store, arguments)),
_ => None,
}
}
pub(super) fn analytics_args(args: &Value) -> std::result::Result<AnalyticsScopeArgs, ()> {
Ok(AnalyticsScopeArgs {
scope: req_string(args, "scope")?,
id: req_string(args, "id")?,
from: opt_string(args, "from")?,
to: opt_string(args, "to")?,
mode: opt_string(args, "mode")?,
unit: opt_string(args, "unit")?,
})
}
pub(super) fn reject_hosted_source(args: &Value) -> Option<Value> {
["source", "target", "remote"].into_iter().find_map(|key| {
args.as_object()
.is_some_and(|object| object.contains_key(key))
.then(|| {
error_result(format!(
"invalid input: analytics reads only the local record, and `{key}` names \
a hosted source. There is no analytics service behind `gitlab`/`linear` \
\u{2014} those names are remote *export mappings*, not chart sources, and \
a configured target that cannot carry a chart document is reported by \
the document's own `unsupported_remote_field` finding. Name the local \
scope with `scope` and `id` instead"
))
})
})
}
pub(super) fn analytics_document_value(document: &ChartExport) -> Value {
serde_json::to_value(document).expect("chart document serialization is infallible")
}