feat: enhance lesson management and review system

- Added LessonAccept and LessonReject actions to manage lesson outcomes.
- Implemented probe for build/test verification before triggering reviews.
- Introduced a mechanism for recording shadow hits and evaluating trials in the ReviewSystem.
- Enhanced lesson structure with additional fields for scope, snippets, and provenances.
- Updated command parsing to include lesson acceptance and rejection commands.
- Improved session state management with staleness sweeps and retrospective creation.
- Enhanced UI to display detailed usage statistics and quality trends.
This commit is contained in:
asepharyana
2026-07-11 21:06:22 +07:00
parent fe82840c03
commit 2ded2d8bf1
9 changed files with 780 additions and 19 deletions
+94 -2
View File
@@ -52,6 +52,12 @@ pub enum Action {
LessonImport {
path: String,
},
LessonAccept {
name: String,
},
LessonReject {
name: String,
},
#[expect(dead_code)]
RecordUsage {
tokens_in: u64,
@@ -71,6 +77,7 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
match action {
Action::Quit => {
save_current_session(state);
auto_create_retrospective(state);
state.quit = true;
}
Action::ForceQuit => {
@@ -326,7 +333,13 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
Action::Tick => {
let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms);
let events: Vec<TurnEvent> = {
// Idle-time housekeeping.
crate::app::review::maybe_run_staleness_sweep(state);
if let Some(ref rt) = state.session_runtime {
let _ = crate::app::review::process_pending_lessons(&rt.session_dir, &state.memory_dir);
}
let events: Vec<TurnEvent> = {
if let Ok(mut q) = state.turn_events.lock() {
q.drain(..).collect()
} else {
@@ -371,8 +384,31 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
if should_trigger_review(state, Origin::Main) {
let _ = trigger_review(state);
}
} else if kind == "review" {
// Track review outcome: check if lessons were found.
// Format: "Quality review: <verdict> [N lesson(s)]"
let lessons_found = if message.contains("lesson") || message.contains("Lesson") {
// Check for "N lesson(s)" pattern at end
message.rsplit(' ').next().and_then(|w| {
w.trim_end_matches(')').trim_end_matches('s')
.split('(').next_back()
.and_then(|n| n.parse::<u32>().ok())
}).unwrap_or(0)
} else {
0
};
if let Some(ref mut rt) = state.session_runtime {
if lessons_found > 0 {
rt.consecutive_empty_reviews = 0;
rt.lesson_count += lessons_found;
} else {
rt.consecutive_empty_reviews += 1;
}
}
state.push_toast(Toast::new(ToastKind::Info, message));
} else {
state.push_toast(Toast::new(ToastKind::Info, message));
}
state.push_toast(Toast::new(ToastKind::Info, message));
}
TurnEvent::Error(msg) => {
state.push_toast(Toast::new(ToastKind::Error, msg));
@@ -402,6 +438,26 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
}
state.dirty = true;
}
Action::LessonAccept { name } => {
if let Some(ref rt) = state.session_runtime {
let _ = crate::app::review::resolve_pending_lesson(
&rt.session_dir, &state.memory_dir, &name, true,
);
}
state.push_toast(Toast::new(ToastKind::Success,
format!("accepted lesson: {}", name)));
state.dirty = true;
}
Action::LessonReject { name } => {
if let Some(ref rt) = state.session_runtime {
let _ = crate::app::review::resolve_pending_lesson(
&rt.session_dir, &state.memory_dir, &name, false,
);
}
state.push_toast(Toast::new(ToastKind::Info,
format!("rejected lesson: {}", name)));
state.dirty = true;
}
}
}
@@ -655,3 +711,39 @@ fn save_current_session(state: &AppStateRest) {
}
}
}
fn auto_create_retrospective(state: &mut AppStateRest) {
if state.session_runtime.is_none() {
return;
}
let session = crate::model::session::Session::new(
state.session_id.clone(),
"session".to_string(),
);
match crate::model::memory::auto_create_retrospective(&state.session_dir, &session) {
Ok(Some(retro)) => {
state.push_toast(crate::app::state::types::Toast::new(
crate::app::state::types::ToastKind::Info,
format!("Retrospective created: {}", retro.name),
));
}
Ok(None) => {}
Err(e) => {
// Silently handle — retrospective is best-effort.
let _ = e;
}
}
// Attempt consensus promotion for global-scope lessons.
let lessons: Vec<crate::model::memory::Memory> = crate::model::memory::Memory::list(&state.memory_dir)
.iter()
.filter_map(|n| crate::model::memory::Memory::read(&state.memory_dir, n).ok())
.filter(|m| m.kind == "lesson")
.collect();
if let Some(global_dir) = dirs::data_dir().map(|d| d.join("zesdex")) {
for lesson in &lessons {
if lesson.scope.as_deref() != Some("global") {
let _ = crate::model::memory::promote_with_consensus(&global_dir, lesson);
}
}
}
}