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:
Riz Ashraf committed 2026-10-07 19:00:44 +01:00
1 parent ec977c24dd
commit 8f924b793a
16 files changed
+2622 -163

No files matched your search

+5
View File
@@ -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
View File
@@ -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());
}
}