/// FIST-Mbt store: SQLite 持久化后端(M1 内建数据库)。
/// 依赖 mizchi/sqlite(native+js 双端)。五表骨架单调落盘,其中 tasks 完整接入 Store trait;
/// specs / runs / heartbeats / archive 先建表供 Phase 2(Omega) / Phase 5(scheduler) 使用。
/// 列约定:parent_id / assignee / completed_by 等可空字段用空串 "" 表示 None。
/// SQLite 后端:持有打开的 Database(Option 以承载 open 失败场景)。
pub struct SqliteStore {
db : @sqlite.Database?
}
/// 打开数据库并初始化五表 schema。失败返回 None(调用方决定是否 abort / 回退内存)。
pub fn SqliteStore::open(db_path : String) -> Option[SqliteStore] {
match @sqlite.Database::open(db_path) {
None => None
Some(db) => {
let s = { db: Some(db) }
if s.create_schema() {
Some(s)
} else {
db.close()
None
}
}
}
}
/// 以默认路径(fist-mbt.db,随运行目录)打开的便捷构造。
pub fn SqliteStore::new() -> Option[SqliteStore] {
SqliteStore::open("fist-mbt.db")
}
/// 初始化 schema:tasks 为完整任务表,specs/runs/heartbeats/archive 为后续阶段预置骨架。
fn SqliteStore::create_schema(self : SqliteStore) -> Bool {
let sqls : Array[String] = [
"CREATE TABLE IF NOT EXISTS tasks (id TEXT PRIMARY KEY, parent_id TEXT DEFAULT '', project_dir TEXT NOT NULL DEFAULT '', priority TEXT NOT NULL DEFAULT '中', importance TEXT NOT NULL DEFAULT '中', depth INTEGER NOT NULL DEFAULT 3, split_n INTEGER NOT NULL DEFAULT 3, status TEXT NOT NULL DEFAULT '待领取', assignee TEXT DEFAULT '', description TEXT NOT NULL DEFAULT '', deliverable TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '', completed_by TEXT DEFAULT '', cleanup_mode TEXT NOT NULL DEFAULT 'deferred')",
"CREATE TABLE IF NOT EXISTS specs (id TEXT PRIMARY KEY, task_id TEXT NOT NULL DEFAULT '', spec_type TEXT NOT NULL DEFAULT 'spec', content TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'pending', created_at TEXT NOT NULL DEFAULT '', updated_at TEXT NOT NULL DEFAULT '')",
"CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, task_id TEXT NOT NULL DEFAULT '', run_type TEXT NOT NULL DEFAULT 'verify', status TEXT NOT NULL DEFAULT 'running', detail TEXT NOT NULL DEFAULT '', started_at TEXT NOT NULL DEFAULT '', finished_at TEXT DEFAULT '')",
"CREATE TABLE IF NOT EXISTS heartbeats (agent_id TEXT NOT NULL, task_id TEXT NOT NULL, last_seen TEXT NOT NULL DEFAULT '', status TEXT NOT NULL DEFAULT 'active', PRIMARY KEY (agent_id, task_id))",
"CREATE TABLE IF NOT EXISTS archive (id TEXT PRIMARY KEY, task_json TEXT NOT NULL DEFAULT '', archived_at TEXT NOT NULL DEFAULT '')",
]
match self.db {
None => false
Some(db) => {
var ok = true
for s in sqls {
if !db.exec(s) { ok = false }
}
ok
}
}
}
// —— 行 <-> Task 序列化辅助 ——
fn SqliteStore::task_columns() -> String {
"id, parent_id, project_dir, priority, importance, depth, split_n, status, assignee, description, deliverable, created_at, updated_at, completed_by, cleanup_mode"
}
fn SqliteStore::bind_task_values(stmt : @sqlite.Statement, t : Task) -> Unit {
ignore(stmt.bind(1, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.get_id()))))
ignore(stmt.bind(2, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.get_parent())))))
ignore(stmt.bind(3, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.project_dir))))
ignore(stmt.bind(4, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.priority))))
ignore(stmt.bind(5, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.importance))))
ignore(stmt.bind(6, @sqlite.SqlValue::Int(t.depth)))
ignore(stmt.bind(7, @sqlite.SqlValue::Int(t.split_n)))
ignore(stmt.bind(8, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.status.to_string()))))
ignore(stmt.bind(9, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.get_assignee())))))
ignore(stmt.bind(10, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.description))))
ignore(stmt.bind(11, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.deliverable))))
ignore(stmt.bind(12, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.created_at))))
ignore(stmt.bind(13, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.updated_at))))
ignore(stmt.bind(14, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.completed_by)))))
ignore(stmt.bind(15, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.cleanup_mode))))
}
/// UPDATE 专用绑定:SET 列顺序(parent_id..cleanup_mode)+ WHERE id
fn SqliteStore::bind_task_update_values(stmt : @sqlite.Statement, t : Task) -> Unit {
ignore(stmt.bind(1, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.get_parent())))))
ignore(stmt.bind(2, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.project_dir))))
ignore(stmt.bind(3, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.priority))))
ignore(stmt.bind(4, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.importance))))
ignore(stmt.bind(5, @sqlite.SqlValue::Int(t.depth)))
ignore(stmt.bind(6, @sqlite.SqlValue::Int(t.split_n)))
ignore(stmt.bind(7, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.status.to_string()))))
ignore(stmt.bind(8, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.get_assignee())))))
ignore(stmt.bind(9, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.description))))
ignore(stmt.bind(10, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.deliverable))))
ignore(stmt.bind(11, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.created_at))))
ignore(stmt.bind(12, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.updated_at))))
ignore(stmt.bind(13, @sqlite.SqlValue::Text(@encoding.encode(UTF8, _opt_str(t.completed_by)))))
ignore(stmt.bind(14, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.cleanup_mode))))
}
fn str_from_bytes(b : Bytes) -> String {
let sb = StringBuilder::new()
@encoding.decode_to(b, sb, encoding=UTF8) catch {
_ => ()
}
sb.to_string()
}
fn _opt_str(o : String?) -> String {
match o {
Some(s) => s
None => ""
}
}
fn row_to_task(stmt : @sqlite.Statement) -> Task {
let parent_raw = str_from_bytes(stmt.column_text(1))
let assignee_raw = str_from_bytes(stmt.column_text(8))
let completed_raw = str_from_bytes(stmt.column_text(13))
let status_s = str_from_bytes(stmt.column_text(7))
let status = match TaskStatus::from_string(status_s) {
Some(s) => s
None => Pending
}
{
id : str_from_bytes(stmt.column_text(0)),
parent_id : if parent_raw == "" { None } else { Some(parent_raw) },
project_dir : str_from_bytes(stmt.column_text(2)),
priority : str_from_bytes(stmt.column_text(3)),
importance : str_from_bytes(stmt.column_text(4)),
depth : stmt.column_int(5),
split_n : stmt.column_int(6),
status,
assignee : if assignee_raw == "" { None } else { Some(assignee_raw) },
description : str_from_bytes(stmt.column_text(9)),
deliverable : str_from_bytes(stmt.column_text(10)),
created_at : str_from_bytes(stmt.column_text(11)),
updated_at : str_from_bytes(stmt.column_text(12)),
completed_by : if completed_raw == "" { None } else { Some(completed_raw) },
cleanup_mode : str_from_bytes(stmt.column_text(14)),
}
}
// —— Store trait 实现 ——
impl Store for SqliteStore with create_task(self, task) {
match self.db {
None => Err("sqlite store 未打开")
Some(db) => {
if self.get_task(task.get_id()) is Some(_) {
return Err("task already exists: \{task.get_id()}")
}
let sql = "INSERT INTO tasks (\{SqliteStore::task_columns()}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)"
match db.prepare(sql) {
None => Err("sqlite prepare 失败: create_task")
Some(stmt) => {
SqliteStore::bind_task_values(stmt, task)
let exec_ok = stmt.execute()
stmt.finalize()
if exec_ok { Ok(()) } else { Err("sqlite 写入失败: \{task.get_id()}") }
}
}
}
}
}
impl Store for SqliteStore with get_task(self, id) {
match self.db {
None => None
Some(db) => {
let sql = "SELECT \{SqliteStore::task_columns()} FROM tasks WHERE id = ?"
match db.prepare(sql) {
None => None
Some(stmt) => {
ignore(stmt.bind(1, @sqlite.SqlValue::Text(@encoding.encode(UTF8, id))))
let found = if stmt.step() { Some(row_to_task(stmt)) } else { None }
stmt.finalize()
found
}
}
}
}
}
impl Store for SqliteStore with update_task(self, task) {
match self.db {
None => Err("sqlite store 未打开")
Some(db) => {
if self.get_task(task.get_id()) is None {
return Err("task not found: \{task.get_id()}")
}
let sql = "UPDATE tasks SET parent_id = ?, project_dir = ?, priority = ?, importance = ?, depth = ?, split_n = ?, status = ?, assignee = ?, description = ?, deliverable = ?, created_at = ?, updated_at = ?, completed_by = ?, cleanup_mode = ? WHERE id = ?"
match db.prepare(sql) {
None => Err("sqlite prepare 失败: update_task")
Some(stmt) => {
SqliteStore::bind_task_update_values(stmt, task)
ignore(stmt.bind(15, @sqlite.SqlValue::Text(@encoding.encode(UTF8, task.get_id()))))
let exec_ok = stmt.execute()
stmt.finalize()
if exec_ok { Ok(()) } else { Err("sqlite 更新失败: \{task.get_id()}") }
}
}
}
}
}
impl Store for SqliteStore with list_tasks(self) {
let out : Array[Task] = []
match self.db {
None => out
Some(db) => {
let sql = "SELECT \{SqliteStore::task_columns()} FROM tasks"
match db.prepare(sql) {
None => out
Some(stmt) => {
while stmt.step() {
out.push(row_to_task(stmt))
}
stmt.finalize()
out
}
}
}
}
}
impl Store for SqliteStore with delete_task(self, id) {
match self.db {
None => Err("sqlite store 未打开")
Some(db) => {
if self.get_task(id) is None {
return Err("task not found: \{id}")
}
let sql = "DELETE FROM tasks WHERE id = ?"
match db.prepare(sql) {
None => Err("sqlite prepare 失败: delete_task")
Some(stmt) => {
ignore(stmt.bind(1, @sqlite.SqlValue::Text(@encoding.encode(UTF8, id))))
let exec_ok = stmt.execute()
stmt.finalize()
if exec_ok { Ok(()) } else { Err("sqlite 删除失败: \{id}") }
}
}
}
}
}
impl Store for SqliteStore with clear(self) {
match self.db {
None => ()
Some(db) => {
ignore(db.exec("DELETE FROM tasks"))
ignore(db.exec("DELETE FROM archive"))
}
}
}
/// 提升声明:允许通过具体类型直接调用 trait 方法。
pub extend SqliteStore with Store::{create_task, get_task, update_task, list_tasks, delete_task, clear}
// —— archive 快照(Phase 2/5 预置 API,不改变 Store trait)——
/// 将已归档任务写入 archive 表快照。
pub fn SqliteStore::archive_task(self : SqliteStore, t : Task, archived_at~ : String) -> Result[Unit, String] {
match self.db {
None => Err("sqlite store 未打开")
Some(db) => {
let sql = "INSERT OR REPLACE INTO archive (id, task_json, archived_at) VALUES (?, ?, ?)"
match db.prepare(sql) {
None => Err("sqlite prepare 失败: archive_task")
Some(stmt) => {
ignore(stmt.bind(1, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.get_id()))))
ignore(stmt.bind(2, @sqlite.SqlValue::Text(@encoding.encode(UTF8, t.to_json().stringify()))))
ignore(stmt.bind(3, @sqlite.SqlValue::Text(@encoding.encode(UTF8, archived_at))))
let exec_ok = stmt.execute()
stmt.finalize()
if exec_ok { Ok(()) } else { Err("sqlite 归档写入失败: \{t.get_id()}") }
}
}
}
}
}
/// 列出全部归档快照(id -> 原 task_json)。
pub fn SqliteStore::list_archived(self : SqliteStore) -> Array[(String, String)] {
let out : Array[(String, String)] = []
match self.db {
None => out
Some(db) => {
match db.prepare("SELECT id, task_json FROM archive ORDER BY archived_at") {
None => out
Some(stmt) => {
while stmt.step() {
out.push((str_from_bytes(stmt.column_text(0)), str_from_bytes(stmt.column_text(1))))
}
stmt.finalize()
out
}
}
}
}
}