feat(reconciliation): implement deterministic state reconciliation engine and post-commit hook
- Implement ReconciliationEngine in server/src/handlers/reconciliation.rs - Auto-transition ADRs to implemented, resolve tech debts, and cascade task unblocking - Ingest git commit events via POST /api/git/commit and scripts/git-reconcile.py post-commit hook - Add universal pagination, search filtering, and keyboard navigation to dashboard - Implement non-destructive task TTL expiry sweeper and gate verification - Implements: ADR-0102, ADR-0103
This commit is contained in:
1 parent
ec977c24dd
commit
8f924b793a
16 files changed
+2622
-163
No files matched your search
@@ -36,6 +36,10 @@ pub async fn gate_verify_handler(
|
||||
State(app_state): State<Arc<AppState>>,
|
||||
Query(q): Query<GateVerifyReq>,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
let mut found = None;
|
||||
let mut to_remove = None;
|
||||
app_state.handler.state.env.gates.modify(|gates| {
|
||||
@@ -44,6 +48,7 @@ pub async fn gate_verify_handler(
|
||||
&& g.target == q.target
|
||||
&& g.namespace == q.namespace
|
||||
&& g.params == q.params
|
||||
&& g.expires_at.is_none_or(|exp| exp > now)
|
||||
}) {
|
||||
found = Some(gates[idx].clone());
|
||||
if q.consume {
|
||||
|
||||
+78
-1
@@ -188,11 +188,52 @@ pub fn create_router(app_state: Arc<AppState>) -> Router {
|
||||
for t in tasks.iter_mut() {
|
||||
if t.id == id {
|
||||
t.status = "completed".to_string();
|
||||
t.updated_at = crate::handlers::utils::now_secs();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
axum::Json(serde_json::json!({"status": "success"}))
|
||||
let unblocked = crate::handlers::reconciliation::cascade_unblock_tasks(&state_clone);
|
||||
let milestones = crate::handlers::reconciliation::recalculate_milestones(&state_clone);
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "success",
|
||||
"unblocked_tasks": unblocked,
|
||||
"updated_milestones": milestones
|
||||
}))
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/git/commit",
|
||||
post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::Json(payload): axum::Json<crate::handlers::reconciliation::GitCommitPayload>| async move {
|
||||
let result = crate::handlers::reconciliation::reconcile_commit_or_code_change(
|
||||
&state_clone,
|
||||
&payload.message,
|
||||
payload.files.first().map(|s| s.as_str()),
|
||||
Some(&payload.commit),
|
||||
payload.branch.as_deref(),
|
||||
)
|
||||
.await;
|
||||
axum::Json(result)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.route(
|
||||
"/api/reconcile/commit",
|
||||
post({
|
||||
let state_clone = app_state.handler.state.clone();
|
||||
move |axum::Json(payload): axum::Json<crate::handlers::reconciliation::GitCommitPayload>| async move {
|
||||
let result = crate::handlers::reconciliation::reconcile_commit_or_code_change(
|
||||
&state_clone,
|
||||
&payload.message,
|
||||
payload.files.first().map(|s| s.as_str()),
|
||||
Some(&payload.commit),
|
||||
payload.branch.as_deref(),
|
||||
)
|
||||
.await;
|
||||
axum::Json(result)
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -909,4 +950,40 @@ mod tests {
|
||||
assert_eq!(summaries.len(), 1);
|
||||
assert_eq!(summaries[0].summary, "Session 2 summary");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_git_commit_endpoint() {
|
||||
let (app, app_state, _dir) = setup_app().await;
|
||||
|
||||
app_state.handler.state.code.adrs.modify(|adrs| {
|
||||
adrs.push(crate::models::Adr {
|
||||
id: "ADR-0102".to_string(),
|
||||
title: "Neovim Dynamic Projection".to_string(),
|
||||
status: "accepted".to_string(),
|
||||
..Default::default()
|
||||
});
|
||||
});
|
||||
|
||||
let payload = serde_json::json!({
|
||||
"commit": "git-hash-999",
|
||||
"branch": "master",
|
||||
"message": "feat: implement ADR-0102 and finish architecture",
|
||||
"files": ["nvim-core/src/lib.rs"]
|
||||
});
|
||||
|
||||
let request = Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/git/commit")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Body::from(payload.to_string()))
|
||||
.unwrap();
|
||||
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let adr = app_state.handler.state.code.adrs.read_with(|adrs| adrs[0].clone());
|
||||
assert_eq!(adr.status, "implemented");
|
||||
assert_eq!(adr.git_commit.as_deref(), Some("git-hash-999"));
|
||||
assert!(adr.resolved_at.is_some());
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user