feat(dashboard,adrs): enrich dashboard tabs with domain model parity and resolve consequence schema handling
This commit is contained in:
1 parent
73c3d41c87
commit
f546579596
5 files changed
+798
-143
No files matched your search
+144
-2
@@ -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())
|
||||
|
||||
Reference in new issue
Block a user