#[allow(clippy::wildcard_imports)]
use super::*;
pub(super) fn capacity_calculate(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let scope = req_string(args, "scope")?;
let reference = req_string(args, "id")?;
let warnings = opt_bool(args, "warnings")?.unwrap_or(false);
let kind = match cmd_capacity::parse_scope_kind(&scope) {
Ok(kind) => kind,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let reference = match cmd_capacity::required("--id", &reference) {
Ok(reference) => reference.to_string(),
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let (config, root, _path) = match cycle_schedule_config(store) {
Ok(loaded) => loaded,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let calculation = match cmd_capacity::calculation_for(&config, &root, store, kind, &reference) {
Ok(calculation) => calculation,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let mut text = cmd_capacity::calculation_text(&calculation);
let findings = capacity_findings_text(&calculation.diagnostics, warnings);
if !findings.is_empty() {
text.push('\n');
text.push_str(&findings);
text.push('\n');
}
Ok(success_result(
text,
capacity_calculation_value(&calculation),
))
}
pub(super) fn capacity_list(_config: &Config, store: &Store, _args: &Value) -> ToolOutcome {
let (config, root, _path) = match cycle_schedule_config(store) {
Ok(loaded) => loaded,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let index = match schedule_index(&config, &root) {
Ok(index) => index,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let mut text = String::from("id\tscope\treference\tdisplay\tcapacity\tunit\tenabled\n");
let mut entries = Vec::new();
for (id, entry) in config.capacity.iter() {
let Some(kind) = entry.scope_kind() else {
continue;
};
let reference = entry.scope_id().unwrap_or_default();
let display = cmd_capacity::scope_label(kind, reference, &index);
text.push_str(&format!(
"{id}\t{kind}\t{reference}\t{display}\t{ceiling}\t{unit}\t{enabled}\n",
kind = kind.as_str(),
ceiling = cmd_capacity::number(entry.capacity),
unit = entry.unit.as_deref().unwrap_or("-"),
enabled = entry.enabled,
));
entries.push(capacity_entry_value(id, kind, entry, &index));
}
let count = entries.len();
Ok(success_result(
text,
json!({ "entries": entries, "entry_count": count }),
))
}
pub(super) fn capacity_show(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "reference")?;
let scope = opt_string(args, "scope")?;
let (config, root, _path) = match cycle_schedule_config(store) {
Ok(loaded) => loaded,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let index = match schedule_index(&config, &root) {
Ok(index) => index,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let wanted = match scope
.as_deref()
.map(cmd_capacity::parse_scope_kind)
.transpose()
{
Ok(wanted) => wanted,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let (id, kind, entry) = match cmd_capacity::one_entry(&config, wanted, &reference, &index) {
Ok(found) => found,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let entry_reference = entry.scope_id().unwrap_or_default();
let text = format!(
"id: {id}\nscope: {kind}\nreference: {reference}\ndisplay: {display}\n\
capacity: {capacity}\nunit: {unit}\nenabled: {enabled}\n",
kind = kind.as_str(),
reference = entry_reference,
display = cmd_capacity::scope_label(kind, entry_reference, &index),
capacity = cmd_capacity::number(entry.capacity),
unit = entry.unit.as_deref().unwrap_or("-"),
enabled = entry.enabled,
);
Ok(success_result(
text,
capacity_entry_value(id, kind, entry, &index),
))
}
pub(super) fn capacity_snapshot(_config: &Config, store: &Store, args: &Value) -> ToolOutcome {
let reference = req_string(args, "cycle")?;
let warnings = opt_bool(args, "warnings")?.unwrap_or(false);
let (config, root, _path) = match cycle_schedule_config(store) {
Ok(loaded) => loaded,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let index = match schedule_index(&config, &root) {
Ok(index) => index,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let cycle_id = match resolve_cycle(&index, &reference) {
CycleReference::Generated { id, .. } => id,
CycleReference::Legacy { .. } => {
return Ok(cycle_schedule_failure(&crate::error::TixError::NotFound(
format!(
"cycle `{reference}` is a hand-written cycle and has no generated snapshot; \
`tix capacity snapshot` reads the evidence a generated cycle's close \
captured"
),
)));
}
CycleReference::Unknown { .. } => {
return Ok(cycle_schedule_failure(&crate::error::TixError::NotFound(
format!(
"no cycle `{reference}` resolves to a record, so there is no snapshot to read"
),
)));
}
};
let snapshots = CycleSnapshotStore::new(&root);
let Some(stored) = (match snapshots.read(&cycle_id) {
Ok(stored) => stored,
Err(error) => return Ok(cycle_schedule_failure(&error)),
}) else {
return Ok(cycle_schedule_failure(&crate::error::TixError::NotFound(
format!(
"cycle `{cycle_id}` has no snapshot: it has not been closed, or its snapshot was \
removed"
),
)));
};
let evidence = match CapacitySnapshot::from_cycle_snapshot(&stored) {
Ok(evidence) => evidence,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let history = match capacity_history(&root, &cycle_id) {
Ok(history) => history,
Err(error) => return Ok(cycle_schedule_failure(&error)),
};
let snapshot_path = format!(
"docs/cycle_snapshots/{filename}",
filename = CycleSnapshotStore::filename_for(&cycle_id)
);
let mut text = format!("cycle: {cycle_id}\nsnapshot: {snapshot_path}\n");
let structured = match evidence {
None => {
text.push_str("evidence: none\n");
json!({
"cycle_id": cycle_id.as_str(),
"snapshot_path": snapshot_path,
"evidence": "none",
"capacity": Value::Null,
"history": history,
})
}
Some(evidence) => {
text.push_str("evidence: present\n");
text.push_str(&cmd_capacity::evidence_text(&evidence));
let findings = capacity_findings_text(&evidence.diagnostics, warnings);
if !findings.is_empty() {
text.push('\n');
text.push_str(&findings);
text.push('\n');
}
json!({
"cycle_id": cycle_id.as_str(),
"snapshot_path": snapshot_path,
"evidence": "present",
"capacity": capacity_evidence_value(&evidence),
"history": history,
})
}
};
Ok(success_result(text, structured))
}
pub(super) fn dispatch(
config: &Config,
store: &Store,
name: &str,
arguments: &Value,
) -> Option<ToolOutcome> {
match name {
"capacity_list" => Some(capacity_list(config, store, arguments)),
"capacity_show" => Some(capacity_show(config, store, arguments)),
"capacity_calculate" => Some(capacity_calculate(config, store, arguments)),
"capacity_snapshot" => Some(capacity_snapshot(config, store, arguments)),
_ => None,
}
}
pub(super) fn capacity_entry_value(
id: &str,
kind: CapacityScopeKind,
entry: &CapacityEntry,
index: &ScheduleIndex,
) -> Value {
let reference = entry.scope_id().unwrap_or_default();
json!({
"id": id,
"scope": kind.as_str(),
"reference": reference,
"display": cmd_capacity::scope_label(kind, reference, index),
"capacity": entry.capacity,
"unit": entry.unit,
"enabled": entry.enabled,
})
}
pub(super) fn capacity_scope_value(scope: &CapacityScope) -> Value {
json!({
"kind": scope.kind.as_str(),
"reference": scope.reference,
"display": scope.display,
})
}
pub(super) fn capacity_warning_value(warning: &CapacityWarning) -> Value {
json!({
"kind": warning.kind.as_str(),
"scope": capacity_scope_value(&warning.scope),
"issues": warning.issues,
"current": warning.current,
"limit": warning.limit,
"overflow": warning.overflow,
"unit": warning.unit,
"reason": warning.reason,
"remedy": warning.remedy,
})
}
pub(super) fn capacity_calculation_value(calculation: &CapacityCalculation) -> Value {
json!({
"scope": capacity_scope_value(&calculation.scope),
"state": calculation.state().as_str(),
"capacity": calculation.capacity,
"unit": calculation.unit,
"committed": calculation.committed,
"completed": calculation.completed,
"remaining": calculation.remaining,
"overflow": calculation.overflow,
"utilization": calculation.utilization,
"counted_issues": calculation.counted_issues,
"uncounted_issues": calculation.uncounted_issues,
"unestimated_issues": calculation.unestimated_issues,
"over_capacity": calculation.is_over_capacity(),
"source": {
"issues": calculation.source.issues,
"units": calculation.source.units,
},
"diagnostics": calculation
.diagnostics
.iter()
.map(capacity_warning_value)
.collect::<Vec<_>>(),
})
}
pub(super) fn capacity_evidence_value(evidence: &CapacitySnapshot) -> Value {
json!({
"snapshot_id": evidence.snapshot_id,
"cycle_id": evidence.cycle_id,
"closed_at": evidence.closed_at,
"closed_by": evidence.closed_by,
"scope": capacity_scope_value(&evidence.scope),
"state": evidence.state.as_str(),
"capacity": evidence.capacity,
"unit": evidence.unit,
"committed": evidence.committed,
"completed": evidence.completed,
"remaining": evidence.remaining,
"overflow": evidence.overflow,
"utilization": evidence.utilization,
"counted_issues": evidence.counted_issues,
"uncounted_issues": evidence.uncounted_issues,
"unestimated_issues": evidence.unestimated_issues,
"source": {
"issues": evidence.source.issues,
"units": evidence.source.units,
},
"diagnostics": evidence
.diagnostics
.iter()
.map(capacity_warning_value)
.collect::<Vec<_>>(),
})
}
pub(super) fn capacity_findings_text(diagnostics: &[CapacityWarning], warnings: bool) -> String {
diagnostics
.iter()
.filter(|warning| warnings || !matches!(warning.kind, CapacityWarningKind::Overflow))
.map(CapacityWarning::render)
.collect::<Vec<_>>()
.join("\n")
}
pub(super) fn capacity_history(root: &Path, cycle_id: &GeneratedCycleId) -> TixResult<Vec<Value>> {
let store = CycleStore::new(root);
let records = RecordStore::new(store.dir()).list::<crate::cycle_store::CycleRecord>()?;
for file in records {
let Ok(Some(cycle)) = GeneratedCycle::from_record(&file.value) else {
continue;
};
if cycle.id != *cycle_id {
continue;
}
let (history, _) = History::parse_front(&file.body);
return Ok(history
.events()
.into_iter()
.filter(|event| capacity_history_kind(event.kind))
.map(|event| {
json!({
"kind": event.kind.as_str(),
"timestamp": event.timestamp.clone(),
"actor": event.actor.clone(),
"detail": event.detail.clone(),
})
})
.collect());
}
Ok(Vec::new())
}
pub(super) fn capacity_history_kind(kind: HistoryEventKind) -> bool {
matches!(
kind,
HistoryEventKind::CycleClosed | HistoryEventKind::CycleReopened
)
}