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:
+85
-2
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user