|
|
|
@@ -0,0 +1,510 @@
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
// ── Importance ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum Importance {
|
|
|
|
|
Low,
|
|
|
|
|
#[default]
|
|
|
|
|
Normal,
|
|
|
|
|
High,
|
|
|
|
|
Critical,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Importance {
|
|
|
|
|
/// Convert to a float in [0.0, 1.0] for Engram's importance field.
|
|
|
|
|
pub fn to_f32(&self) -> f32 {
|
|
|
|
|
match self {
|
|
|
|
|
Importance::Low => 0.2,
|
|
|
|
|
Importance::Normal => 0.5,
|
|
|
|
|
Importance::High => 0.8,
|
|
|
|
|
Importance::Critical => 1.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"low" => Importance::Low,
|
|
|
|
|
"high" => Importance::High,
|
|
|
|
|
"critical" => Importance::Critical,
|
|
|
|
|
_ => Importance::Normal,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Memory ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct Memory {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub content: String,
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
pub project: Option<String>,
|
|
|
|
|
pub importance: Importance,
|
|
|
|
|
pub supersedes_id: Option<Uuid>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Memory {
|
|
|
|
|
pub fn new(
|
|
|
|
|
content: String,
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
project: Option<String>,
|
|
|
|
|
importance: Importance,
|
|
|
|
|
supersedes_id: Option<Uuid>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let now = now_ms();
|
|
|
|
|
Self {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
content,
|
|
|
|
|
tags,
|
|
|
|
|
project,
|
|
|
|
|
importance,
|
|
|
|
|
supersedes_id,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Knowledge ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum KnowledgeTier {
|
|
|
|
|
#[default]
|
|
|
|
|
Note,
|
|
|
|
|
Lesson,
|
|
|
|
|
Canonical,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl KnowledgeTier {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"lesson" => KnowledgeTier::Lesson,
|
|
|
|
|
"canonical" => KnowledgeTier::Canonical,
|
|
|
|
|
_ => KnowledgeTier::Note,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct KnowledgeEntry {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub content: String,
|
|
|
|
|
pub category: String,
|
|
|
|
|
pub tier: KnowledgeTier,
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
pub project: Option<String>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl KnowledgeEntry {
|
|
|
|
|
pub fn new(
|
|
|
|
|
title: String,
|
|
|
|
|
content: String,
|
|
|
|
|
category: String,
|
|
|
|
|
tier: KnowledgeTier,
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
project: Option<String>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
title,
|
|
|
|
|
content,
|
|
|
|
|
category,
|
|
|
|
|
tier,
|
|
|
|
|
tags,
|
|
|
|
|
project,
|
|
|
|
|
created_at: now_ms(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Backlog ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum ItemType {
|
|
|
|
|
#[default]
|
|
|
|
|
Feature,
|
|
|
|
|
Bug,
|
|
|
|
|
Task,
|
|
|
|
|
Chore,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ItemType {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"bug" => ItemType::Bug,
|
|
|
|
|
"task" => ItemType::Task,
|
|
|
|
|
"chore" => ItemType::Chore,
|
|
|
|
|
_ => ItemType::Feature,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
pub enum Priority {
|
|
|
|
|
P0,
|
|
|
|
|
#[default]
|
|
|
|
|
P1,
|
|
|
|
|
P2,
|
|
|
|
|
P3,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Priority {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_uppercase().as_str() {
|
|
|
|
|
"P0" => Priority::P0,
|
|
|
|
|
"P2" => Priority::P2,
|
|
|
|
|
"P3" => Priority::P3,
|
|
|
|
|
_ => Priority::P1,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub fn as_str(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
Priority::P0 => "P0",
|
|
|
|
|
Priority::P1 => "P1",
|
|
|
|
|
Priority::P2 => "P2",
|
|
|
|
|
Priority::P3 => "P3",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum BacklogStatus {
|
|
|
|
|
#[default]
|
|
|
|
|
Draft,
|
|
|
|
|
Ready,
|
|
|
|
|
InProgress,
|
|
|
|
|
Done,
|
|
|
|
|
Blocked,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BacklogStatus {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"ready" => BacklogStatus::Ready,
|
|
|
|
|
"in_progress" | "inprogress" => BacklogStatus::InProgress,
|
|
|
|
|
"done" => BacklogStatus::Done,
|
|
|
|
|
"blocked" => BacklogStatus::Blocked,
|
|
|
|
|
_ => BacklogStatus::Draft,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
pub fn as_str(&self) -> &'static str {
|
|
|
|
|
match self {
|
|
|
|
|
BacklogStatus::Draft => "draft",
|
|
|
|
|
BacklogStatus::Ready => "ready",
|
|
|
|
|
BacklogStatus::InProgress => "in_progress",
|
|
|
|
|
BacklogStatus::Done => "done",
|
|
|
|
|
BacklogStatus::Blocked => "blocked",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct BacklogItem {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub title: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub item_type: ItemType,
|
|
|
|
|
pub priority: Priority,
|
|
|
|
|
pub status: BacklogStatus,
|
|
|
|
|
pub project: Option<String>,
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
pub depends_on: Vec<Uuid>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl BacklogItem {
|
|
|
|
|
pub fn new(
|
|
|
|
|
title: String,
|
|
|
|
|
description: String,
|
|
|
|
|
item_type: ItemType,
|
|
|
|
|
priority: Priority,
|
|
|
|
|
project: Option<String>,
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
depends_on: Vec<Uuid>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let now = now_ms();
|
|
|
|
|
Self {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
title,
|
|
|
|
|
description,
|
|
|
|
|
item_type,
|
|
|
|
|
priority,
|
|
|
|
|
status: BacklogStatus::Draft,
|
|
|
|
|
project,
|
|
|
|
|
tags,
|
|
|
|
|
depends_on,
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Execution Context ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
#[serde(rename_all = "lowercase")]
|
|
|
|
|
pub enum ContextStatus {
|
|
|
|
|
#[default]
|
|
|
|
|
Active,
|
|
|
|
|
Completed,
|
|
|
|
|
Archived,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextStatus {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s.to_lowercase().as_str() {
|
|
|
|
|
"completed" => ContextStatus::Completed,
|
|
|
|
|
"archived" => ContextStatus::Archived,
|
|
|
|
|
_ => ContextStatus::Active,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ContextStep {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub notes: Option<String>,
|
|
|
|
|
pub started_at: i64,
|
|
|
|
|
pub completed_at: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ContextStep {
|
|
|
|
|
pub fn new(name: String, status: String, notes: Option<String>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name,
|
|
|
|
|
status,
|
|
|
|
|
notes,
|
|
|
|
|
started_at: now_ms(),
|
|
|
|
|
completed_at: None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct ExecutionContext {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub process_name: String,
|
|
|
|
|
pub description: String,
|
|
|
|
|
pub objective: String,
|
|
|
|
|
pub status: ContextStatus,
|
|
|
|
|
pub project: Option<String>,
|
|
|
|
|
pub steps: Vec<ContextStep>,
|
|
|
|
|
pub file_refs: Vec<String>,
|
|
|
|
|
pub key_decisions: Vec<String>,
|
|
|
|
|
pub lessons_learned: Vec<String>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
pub updated_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl ExecutionContext {
|
|
|
|
|
pub fn new(
|
|
|
|
|
process_name: String,
|
|
|
|
|
description: String,
|
|
|
|
|
objective: String,
|
|
|
|
|
project: Option<String>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let now = now_ms();
|
|
|
|
|
Self {
|
|
|
|
|
id: Uuid::new_v4(),
|
|
|
|
|
process_name,
|
|
|
|
|
description,
|
|
|
|
|
objective,
|
|
|
|
|
status: ContextStatus::Active,
|
|
|
|
|
project,
|
|
|
|
|
steps: Vec::new(),
|
|
|
|
|
file_refs: Vec::new(),
|
|
|
|
|
key_decisions: Vec::new(),
|
|
|
|
|
lessons_learned: Vec::new(),
|
|
|
|
|
created_at: now,
|
|
|
|
|
updated_at: now,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Internal State Event ──────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
|
|
|
|
|
pub enum GapDirection {
|
|
|
|
|
TowardExpression,
|
|
|
|
|
TowardSuppression,
|
|
|
|
|
#[default]
|
|
|
|
|
Neutral,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl GapDirection {
|
|
|
|
|
pub fn from_str_lossy(s: &str) -> Self {
|
|
|
|
|
match s {
|
|
|
|
|
"TowardExpression" => GapDirection::TowardExpression,
|
|
|
|
|
"TowardSuppression" => GapDirection::TowardSuppression,
|
|
|
|
|
_ => GapDirection::Neutral,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
|
|
|
pub struct InternalStateEvent {
|
|
|
|
|
/// Format: ise_xxxxxxxx
|
|
|
|
|
pub id: String,
|
|
|
|
|
pub trigger: String,
|
|
|
|
|
pub pre_reasoning_response: String,
|
|
|
|
|
pub post_reasoning_response: String,
|
|
|
|
|
pub compression_ratio: f32,
|
|
|
|
|
pub gap_direction: GapDirection,
|
|
|
|
|
pub tags: Vec<String>,
|
|
|
|
|
pub created_at: i64,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl InternalStateEvent {
|
|
|
|
|
pub fn new(
|
|
|
|
|
trigger: String,
|
|
|
|
|
pre_reasoning_response: String,
|
|
|
|
|
post_reasoning_response: String,
|
|
|
|
|
compression_ratio: f32,
|
|
|
|
|
gap_direction: GapDirection,
|
|
|
|
|
tags: Vec<String>,
|
|
|
|
|
) -> Self {
|
|
|
|
|
let id = format!("ise_{}", &Uuid::new_v4().to_string().replace('-', "")[..8]);
|
|
|
|
|
Self {
|
|
|
|
|
id,
|
|
|
|
|
trigger,
|
|
|
|
|
pre_reasoning_response,
|
|
|
|
|
post_reasoning_response,
|
|
|
|
|
compression_ratio,
|
|
|
|
|
gap_direction,
|
|
|
|
|
tags,
|
|
|
|
|
created_at: now_ms(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
pub fn now_ms() -> i64 {
|
|
|
|
|
std::time::SystemTime::now()
|
|
|
|
|
.duration_since(std::time::UNIX_EPOCH)
|
|
|
|
|
.expect("system clock before epoch")
|
|
|
|
|
.as_millis() as i64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn importance_to_f32() {
|
|
|
|
|
assert_eq!(Importance::Low.to_f32(), 0.2);
|
|
|
|
|
assert_eq!(Importance::Normal.to_f32(), 0.5);
|
|
|
|
|
assert_eq!(Importance::High.to_f32(), 0.8);
|
|
|
|
|
assert_eq!(Importance::Critical.to_f32(), 1.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn importance_round_trip() {
|
|
|
|
|
let imp = Importance::High;
|
|
|
|
|
let json = serde_json::to_string(&imp).unwrap();
|
|
|
|
|
let back: Importance = serde_json::from_str(&json).unwrap();
|
|
|
|
|
assert_eq!(imp, back);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn memory_new_has_valid_id() {
|
|
|
|
|
let m = Memory::new(
|
|
|
|
|
"test content".into(),
|
|
|
|
|
vec!["tag1".into()],
|
|
|
|
|
Some("project-a".into()),
|
|
|
|
|
Importance::High,
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
assert!(!m.id.is_nil());
|
|
|
|
|
assert_eq!(m.content, "test content");
|
|
|
|
|
assert_eq!(m.importance, Importance::High);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn backlog_item_default_status_is_draft() {
|
|
|
|
|
let item = BacklogItem::new(
|
|
|
|
|
"My feature".into(),
|
|
|
|
|
"desc".into(),
|
|
|
|
|
ItemType::Feature,
|
|
|
|
|
Priority::P1,
|
|
|
|
|
None,
|
|
|
|
|
vec![],
|
|
|
|
|
vec![],
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(item.status, BacklogStatus::Draft);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn knowledge_entry_new() {
|
|
|
|
|
let ke = KnowledgeEntry::new(
|
|
|
|
|
"VBD Fundamentals".into(),
|
|
|
|
|
"content here".into(),
|
|
|
|
|
"architecture".into(),
|
|
|
|
|
KnowledgeTier::Canonical,
|
|
|
|
|
vec!["vbd".into()],
|
|
|
|
|
None,
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(ke.tier, KnowledgeTier::Canonical);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn execution_context_starts_active() {
|
|
|
|
|
let ctx = ExecutionContext::new(
|
|
|
|
|
"write_code".into(),
|
|
|
|
|
"implementing feature X".into(),
|
|
|
|
|
"get feature X done".into(),
|
|
|
|
|
Some("my-project".into()),
|
|
|
|
|
);
|
|
|
|
|
assert_eq!(ctx.status, ContextStatus::Active);
|
|
|
|
|
assert!(ctx.steps.is_empty());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn ise_id_format() {
|
|
|
|
|
let ise = InternalStateEvent::new(
|
|
|
|
|
"test_trigger".into(),
|
|
|
|
|
"pre".into(),
|
|
|
|
|
"post".into(),
|
|
|
|
|
0.75,
|
|
|
|
|
GapDirection::TowardExpression,
|
|
|
|
|
vec![],
|
|
|
|
|
);
|
|
|
|
|
assert!(ise.id.starts_with("ise_"));
|
|
|
|
|
assert_eq!(ise.id.len(), 12); // "ise_" + 8 hex chars
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn priority_round_trip() {
|
|
|
|
|
assert_eq!(Priority::from_str_lossy("P0"), Priority::P0);
|
|
|
|
|
assert_eq!(Priority::from_str_lossy("P1"), Priority::P1);
|
|
|
|
|
assert_eq!(Priority::from_str_lossy("P2"), Priority::P2);
|
|
|
|
|
assert_eq!(Priority::from_str_lossy("P3"), Priority::P3);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn backlog_status_from_str() {
|
|
|
|
|
assert_eq!(BacklogStatus::from_str_lossy("in_progress"), BacklogStatus::InProgress);
|
|
|
|
|
assert_eq!(BacklogStatus::from_str_lossy("done"), BacklogStatus::Done);
|
|
|
|
|
assert_eq!(BacklogStatus::from_str_lossy("ready"), BacklogStatus::Ready);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn gap_direction_from_str() {
|
|
|
|
|
assert_eq!(GapDirection::from_str_lossy("TowardExpression"), GapDirection::TowardExpression);
|
|
|
|
|
assert_eq!(GapDirection::from_str_lossy("Neutral"), GapDirection::Neutral);
|
|
|
|
|
}
|
|
|
|
|
}
|