///|
/// 设计版本控制:时间旅行 / 分支 / 变更回放。
///
/// 每次对项目的修改都产生一个"commit"(快照 + 操作记录)。
/// Agent 可以:
/// - 查看完整历史(谁在什么时候做了什么)
/// - 时间旅行到任意版本(预览旧状态)
/// - 从任意版本分叉新分支(探索不同方向)
/// - 回放两个版本之间的变更(逐步看每步做了什么)
/// - 撤销 / 重做
pub(all) struct HistoryEntry {
revision : Int
timestamp : String
agent_id : String
message : String
ops : Array[DesignOp]
snapshot : Document
}
///|
pub(all) struct DesignHistory {
entries : Array[HistoryEntry]
mut current_revision : Int
mut branch_name : String
mut undo_stack : Array[Int]
mut redo_stack : Array[Int]
}
///|
pub fn DesignHistory::new(initial_doc : Document) -> DesignHistory {
let h : DesignHistory = {
entries: [],
current_revision: 0,
branch_name: "main",
undo_stack: [],
redo_stack: [],
}
h.entries.push({
revision: 0,
timestamp: "init",
agent_id: "system",
message: "初始版本",
ops: [],
snapshot: initial_doc,
})
h
}
///|
pub fn DesignHistory::commit(
self : DesignHistory,
agent_id : String,
message : String,
ops : Array[DesignOp],
doc : Document,
) -> Int {
// 撤销后新提交:截断 current 之后的 redo 分支(git 语义)。否则
// current+1 会与 redo 链上已存在的 revision 撞号,checkout/redo 的
// 线性扫描命中旧快照、新提交被静默遮蔽(review 5a5641a)。
// entries 按 revision 升序追加,从尾部弹出即精确截断。
while self.entries.length() > 0 &&
self.entries[self.entries.length() - 1].revision > self.current_revision {
let _ = self.entries.pop()
}
self.redo_stack = []
let rev = self.current_revision + 1
self.entries.push({
revision: rev,
timestamp: "t" + rev.to_string(),
agent_id,
message,
ops,
snapshot: doc,
})
self.undo_stack.push(self.current_revision)
self.current_revision = rev
rev
}
///|
pub fn DesignHistory::rev(self : DesignHistory) -> Int {
self.current_revision
}
///|
pub fn DesignHistory::checkout(
self : DesignHistory,
revision : Int,
) -> Result[Document, String] {
for e in self.entries.iter() {
if e.revision == revision {
return Ok(e.snapshot)
}
}
Err("revision_not_found: " + revision.to_string())
}
///|
pub fn DesignHistory::undo(self : DesignHistory) -> Result[Document, String] {
if self.undo_stack.length() == 0 {
return Err("nothing_to_undo")
}
let prev = self.undo_stack[self.undo_stack.length() - 1]
let _ = self.undo_stack.pop()
self.redo_stack.push(self.current_revision)
self.current_revision = prev
self.checkout(prev)
}
///|
pub fn DesignHistory::redo(self : DesignHistory) -> Result[Document, String] {
if self.redo_stack.length() == 0 {
return Err("nothing_to_redo")
}
let next = self.redo_stack[self.redo_stack.length() - 1]
let _ = self.redo_stack.pop()
self.undo_stack.push(self.current_revision)
self.current_revision = next
self.checkout(next)
}
///|
pub fn DesignHistory::log(self : DesignHistory) -> String {
let sb = StringBuilder::new()
let total = self.entries.length().to_string()
sb.write_string("History (" + self.branch_name + ", " + total + " commits)\n")
for i in 0.. 0 {
let ops_count = e.ops.length().to_string()
sb.write_string(" ops: " + ops_count + "\n")
}
}
sb.to_string()
}
///|
pub fn DesignHistory::replay(
self : DesignHistory,
from_rev : Int,
to_rev : Int,
) -> Result[String, String] {
let ops_found : Array[String] = []
let mut found = false
for e in self.entries.iter() {
if e.revision > from_rev && e.revision <= to_rev {
found = true
for op in e.ops.iter() {
let desc = op.describe()
ops_found.push("[rev" + e.revision.to_string() + "] " + desc)
}
}
}
if !found {
return Err("invalid_range")
}
let j = jstr18(ops_found)
let count = ops_found.length().to_string()
let fr = from_rev.to_string()
let tr = to_rev.to_string()
Ok(
"{\"from\":" +
fr +
",\"to\":" +
tr +
",\"total_ops\":" +
count +
",\"replay\":[" +
j +
"]}",
)
}
///|
pub fn DesignHistory::branch(
self : DesignHistory,
from_rev : Int,
branch_name : String,
) -> Result[Document, String] {
let doc = match self.checkout(from_rev) {
Ok(d) => d
Err(e) => return Err(e)
}
self.branch_name = branch_name
Ok(doc)
}
///|
pub fn DesignHistory::diff(
self : DesignHistory,
from_rev : Int,
to_rev : Int,
) -> Result[String, String] {
match (self.checkout(from_rev), self.checkout(to_rev)) {
(Ok(from_doc), Ok(to_doc)) => {
let d = diff_docs(from_doc, to_doc)
Ok(d.to_json())
}
_ => Err("revision_not_found")
}
}
///|
fn jstr18(parts : Array[String]) -> String {
let sb = StringBuilder::new()
for i in 0.. 0 {
sb.write_string(",")
}
sb.write_string("\"" + parts[i] + "\"")
}
sb.to_string()
}
///|
/// 把 Document 写回指定画板(history 时间旅行的落地原语)。
/// 边界层(CLI/MCP)持有 DesignHistory,checkout/undo/redo 的结果经此
/// 落回 Project——「源码是唯一事实源」不变:写回后仍走 canonical 序列化。
pub fn Project::set_artboard_doc(
self : Project,
artboard~ : String,
doc : Document,
) -> Result[Unit, String] {
match self.artboards.get(artboard) {
None => Err("unknown_artboard: \{artboard}")
Some(ab) => {
// clone 防别名:checkout/undo 返回的历史快照若按引用落回画板,
// 后续编辑会原地改掉该 revision 的快照,时间旅行语义被静默破坏
ab.doc = doc.clone()
// 审查修复:整板替换必须推进 project.revision——canon_cache 等以
// project.revision 为失效键的会话级缓存依赖它感知内容变化
self.revision = self.revision + 1
Ok(())
}
}
}