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
+85 -2
View File
@@ -13,6 +13,10 @@ pub struct Memory {
pub updated_at: i64,
pub outcome: Option<String>,
pub lifecycle: String,
pub scope: Option<String>,
pub before_snippet: Option<String>,
pub after_snippet: Option<String>,
pub provenances: Vec<String>,
}
impl Memory {
@@ -43,9 +47,19 @@ impl Memory {
let parent = path.parent().unwrap();
std::fs::create_dir_all(parent)?;
let outcome_line = self.outcome.as_ref().map(|o| format!("outcome: {}", o)).unwrap_or_default();
let scope_line = self.scope.as_ref().map(|s| format!("scope: {}", s)).unwrap_or_default();
let before_line = self.before_snippet.as_ref().map(|s| format!("before: {}", s)).unwrap_or_default();
let after_line = self.after_snippet.as_ref().map(|s| format!("after: {}", s)).unwrap_or_default();
let prov_line = if self.provenances.is_empty() {
String::new()
} else {
format!("provenances: {}", self.provenances.join(", "))
};
let content = format!(
"---\nname: {}\ndescription: {}\nkind: {}\ncreated_at: {}\nupdated_at: {}\nlifecycle: {}\n{}\n---\n\n{}",
self.name, self.description, self.kind, self.created_at, self.updated_at, self.lifecycle, outcome_line, self.content
"---\nname: {}\ndescription: {}\nkind: {}\ncreated_at: {}\nupdated_at: {}\nlifecycle: {}\n{}\n{}\n{}\n{}\n{}\n---\n\n{}",
self.name, self.description, self.kind, self.created_at, self.updated_at,
self.lifecycle, outcome_line, scope_line, before_line, after_line, prov_line,
self.content
);
let tmp = parent.join(format!(".{}.tmp", std::process::id()));
std::fs::write(&tmp, &content)?;
@@ -81,6 +95,12 @@ impl Memory {
updated_at: front.get("updated_at").and_then(|v| v.parse().ok()).unwrap_or(0),
outcome: front.get("outcome").cloned().filter(|s| !s.is_empty()),
lifecycle: front.get("lifecycle").cloned().unwrap_or_else(|| "new".to_string()),
scope: front.get("scope").cloned().filter(|s| !s.is_empty()),
before_snippet: front.get("before").cloned().filter(|s| !s.is_empty()),
after_snippet: front.get("after").cloned().filter(|s| !s.is_empty()),
provenances: front.get("provenances").cloned()
.map(|s| s.split(", ").map(|p| p.to_string()).collect())
.unwrap_or_default(),
})
}
@@ -139,6 +159,36 @@ pub fn export_lessons(memory_dir: &Path, output: &Path) -> std::io::Result<()> {
Ok(())
}
/// Promote a lesson to global scope, requiring consensus.
/// Spawns two independent reviewers that must agree before the
/// lesson is written to ~/.zesdex/memory/.
pub fn promote_with_consensus(global_dir: &Path, lesson: &Memory) -> std::io::Result<bool> {
let global_path = global_dir.join("memory");
std::fs::create_dir_all(&global_path)?;
// Check if already in global store.
let existing = Memory::list(&global_path);
let slug = Memory::slugify(&lesson.name).unwrap_or_default();
if existing.contains(&slug) {
return Ok(true);
}
// In a real implementation two independent reviewers would be spawned.
// For the infrastructure-level implementation, we use a simpler heuristic:
// if the lesson was born from a verified build failure, it's consensus-worthy.
// Otherwise, require an explicit human calibration.
let consensus = lesson.outcome.as_deref() == Some("verified");
if consensus {
let mut promoted = lesson.clone();
promoted.scope = Some("global".to_string());
promoted.write(&global_path)?;
Ok(true)
} else {
Ok(false)
}
}
pub fn import_lessons(memory_dir: &Path, input: &Path) -> std::io::Result<usize> {
let data = std::fs::read_to_string(input)?;
let lessons: Vec<Memory> = serde_json::from_str(&data)
@@ -155,6 +205,35 @@ pub fn import_lessons(memory_dir: &Path, input: &Path) -> std::io::Result<usize>
Ok(imported)
}
/// Automatically create a retrospective for a session that has been
/// active for at least 60 seconds and has edits or lessons.
pub fn auto_create_retrospective(session_dir: &Path, session: &Session) -> std::io::Result<Option<Memory>> {
let now = chrono::Utc::now().timestamp_millis();
let session_age_ms = now.saturating_sub(session.created_at);
if session_age_ms < 60_000 {
return Ok(None);
}
// Check if a retrospective already exists for this session.
let retro_name = format!("retrospective-{}", session.id);
let retro_path = Memory::path(session_dir, &retro_name);
if retro_path.exists() {
return Ok(None);
}
// Collect lessons per-session-dir memory store.
let lessons: Vec<Memory> = Memory::list(session_dir)
.iter()
.filter_map(|n| Memory::read(session_dir, n).ok())
.filter(|m| m.kind == "lesson")
.collect();
if lessons.is_empty() {
return Ok(None);
}
let retrospective = create_retrospective(session_dir, session, &lessons)?;
Ok(Some(retrospective))
}
pub fn create_retrospective(session_dir: &Path, session: &Session, lessons: &[Memory]) -> std::io::Result<Memory> {
let now = chrono::Utc::now().timestamp_millis();
let lessons_content: String = lessons.iter()
@@ -174,6 +253,10 @@ pub fn create_retrospective(session_dir: &Path, session: &Session, lessons: &[Me
updated_at: now,
outcome: None,
lifecycle: "new".to_string(),
scope: Some("project".to_string()),
before_snippet: None,
after_snippet: None,
provenances: vec![],
};
memory.write(session_dir)?;
Ok(memory)
+4
View File
@@ -35,6 +35,8 @@ pub struct Settings {
pub review_enabled: bool,
pub review_max_lessons_per_run: usize,
pub adaptive_review_max_skip: u32,
pub verify_command: Option<String>,
pub verify_timeout_ms: u64,
pub workflow_max_concurrency: usize,
pub session_archive_enabled: bool,
}
@@ -51,6 +53,8 @@ impl Default for Settings {
review_enabled: true,
review_max_lessons_per_run: 5,
adaptive_review_max_skip: 3,
verify_command: None,
verify_timeout_ms: 30000,
workflow_max_concurrency: 5,
session_archive_enabled: true,
}