feat(dashboard,adrs): enrich dashboard tabs with domain model parity and resolve consequence schema handling

This commit is contained in:
Riz Ashraf committed 2026-10-07 14:38:02 +01:00
1 parent 73c3d41c87
commit f546579596
5 files changed
+798 -143

No files matched your search

+144 -2
View File
@@ -378,7 +378,7 @@ impl McpTool for DecisionsHandler {
fn schema(&self) -> Value {
crate::mcp::tool_def::<DecisionsTool>(
"decisions",
"Consolidated Architectural Decision Records (ADRs) management (log, query, delete)",
"Consolidated Architectural Decision Records (ADRs) management (log, query, update, delete)",
)
}
@@ -396,6 +396,12 @@ impl McpTool for DecisionsHandler {
let context = req.context.unwrap_or_default();
let decision = req.decision.unwrap_or_default();
let consequence = req.consequences.unwrap_or_default();
let status_lower = status.to_ascii_lowercase();
let resolved_at = if status_lower == "implemented" || status_lower == "resolved" {
Some(crate::handlers::utils::now_secs())
} else {
None
};
let idx = state.get_search_index().await;
let mut final_id = String::new();
@@ -404,7 +410,7 @@ impl McpTool for DecisionsHandler {
state.code.adrs.modify(|adrs| {
if let Some(superseded_id) = &req.supersedes {
for old_adr in adrs.iter_mut() {
if old_adr.id == *superseded_id {
if old_adr.id.eq_ignore_ascii_case(superseded_id) {
old_adr.status = "superseded".to_string();
adrs_to_index.push(old_adr.clone());
break;
@@ -427,6 +433,10 @@ impl McpTool for DecisionsHandler {
alternatives_considered: req.alternatives_considered.unwrap_or_default(),
affected_components: req.affected_components.unwrap_or_default(),
author: req.author,
git_commit: req.git_commit,
git_branch: req.git_branch,
resolved_at,
task_id: req.task_id,
};
adrs_to_index.push(a.clone());
@@ -444,6 +454,94 @@ impl McpTool for DecisionsHandler {
);
Ok(format!("Logged decision {}: {}", final_id, title))
}
DecisionAction::Update => {
let id = req.id.ok_or_else(|| {
crate::error::AppError::Internal("Missing required parameter 'id' for action 'update'. Next step: Provide ADR 'id' string in request and retry.".to_string())
})?;
let mut updated_adr = None;
let mut adrs_to_index = Vec::new();
state.code.adrs.modify(|adrs| {
let target_pos = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(&id));
if let Some(pos) = target_pos {
if let Some(superseded_id) = &req.supersedes {
if let Some(s_pos) = adrs.iter().position(|a| a.id.eq_ignore_ascii_case(superseded_id)) {
if s_pos != pos {
adrs[s_pos].status = "superseded".to_string();
adrs_to_index.push(adrs[s_pos].clone());
}
}
}
let a = &mut adrs[pos];
if let Some(t) = req.title {
a.title = t;
}
if let Some(c) = req.context {
a.context = c;
}
if let Some(d) = req.decision {
a.decision = d;
}
if let Some(cons) = req.consequences {
a.consequence = cons;
}
if let Some(s) = req.status {
let s_lower = s.to_ascii_lowercase();
if (s_lower == "implemented" || s_lower == "resolved") && a.resolved_at.is_none() {
a.resolved_at = Some(crate::handlers::utils::now_secs());
} else if s_lower != "implemented" && s_lower != "resolved" {
a.resolved_at = None;
}
a.status = s;
}
if req.supersedes.is_some() {
a.supersedes = req.supersedes;
}
if req.repo_name.is_some() {
a.repo_name = req.repo_name;
}
if let Some(alts) = req.alternatives_considered {
a.alternatives_considered = alts;
}
if let Some(aff) = req.affected_components {
a.affected_components = aff;
}
if req.author.is_some() {
a.author = req.author;
}
if req.git_commit.is_some() {
a.git_commit = req.git_commit;
}
if req.git_branch.is_some() {
a.git_branch = req.git_branch;
}
if req.task_id.is_some() {
a.task_id = req.task_id;
}
adrs_to_index.push(a.clone());
updated_adr = Some(a.clone());
}
});
if let Some(adr) = updated_adr {
let idx = state.get_search_index().await;
for a in &adrs_to_index {
drop(idx.index_adr(a));
}
state.record_activity(
"decision",
&format!("Updated {}: {}", adr.id, adr.title),
Some(&adr.status),
);
Ok(format!("Updated decision {}: {} (status: {})", adr.id, adr.title, adr.status))
} else {
Err(crate::error::AppError::Internal(
format!("Decision with id '{}' not found", id),
))
}
}
DecisionAction::Query => {
let limit = req.limit.unwrap_or(20);
let include_body = req.include_body.unwrap_or(true);
@@ -458,6 +556,7 @@ impl McpTool for DecisionsHandler {
crate::handlers::utils::contains_ignore_ascii_case(&a.title, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.context, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.decision, q)
|| crate::handlers::utils::contains_ignore_ascii_case(&a.consequence, q)
} else {
true
}
@@ -476,6 +575,10 @@ impl McpTool for DecisionsHandler {
"title": a.title,
"status": a.status,
"timestamp": a.timestamp,
"git_commit": a.git_commit,
"git_branch": a.git_branch,
"resolved_at": a.resolved_at,
"task_id": a.task_id,
})
})
.collect();
@@ -1787,6 +1890,11 @@ mod tests {
.map_err(|e| crate::error::AppError::Internal(e.to_string()))
.unwrap();
assert_eq!(res1, "Logged decision ADR-0001: Architecture");
let q_dec = decision_handler
.execute(json!({"action": "query"}), state.clone())
.await
.unwrap();
assert!(q_dec.contains("Simple"));
let debt_handler = TechDebtHandler;
let args_debt = json!({
@@ -2000,6 +2108,40 @@ mod tests {
.await
.unwrap();
assert!(q_dec_res.contains("Use Axum"));
assert!(q_dec_res.contains("Fast async API routing"));
let q_by_consequence = handler_dec
.execute(serde_json::json!({"action": "query", "query": "Fast async"}), state.clone())
.await
.unwrap();
assert!(q_by_consequence.contains("Use Axum"));
let update_res = handler_dec
.execute(
serde_json::json!({
"action": "update",
"id": "ADR-0001",
"status": "implemented",
"git_commit": "abc1234",
"git_branch": "master",
"task_id": "TASK-123"
}),
state.clone(),
)
.await
.unwrap();
assert!(update_res.contains("Updated decision ADR-0001"));
assert!(update_res.contains("implemented"));
let q_after_update = handler_dec
.execute(serde_json::json!({"action": "query", "include_body": false}), state.clone())
.await
.unwrap();
assert!(q_after_update.contains("implemented"));
assert!(q_after_update.contains("abc1234"));
assert!(q_after_update.contains("master"));
assert!(q_after_update.contains("TASK-123"));
assert!(q_after_update.contains("resolved_at"));
let del_dec_res = handler_dec
.execute(serde_json::json!({"action": "delete", "id": "ADR-0001"}), state.clone())