v0.7.2: undo/redo — TDD

Operation-level memory undo/redo built on existing Merkle snapshot
infrastructure. undo-snapshot captures state before destructive tool
execution. /undo and /redo TUI commands send structured events.

- core-memory: undo-snapshot, undo, redo functions + 3 tests
- core-perceive: :undo/:redo sensor handlers
- core-act: auto-snapshot before non-read-only tools
- core-package: undo/redo symbol exports
- channel-tui-main: /undo, /redo commands + 2 tests
- Core: 73/73  TUI Main: 74/74
This commit is contained in:
2026-05-08 16:39:00 -04:00
parent d2d61c5b44
commit 26fd756222
10 changed files with 276 additions and 6 deletions

View File

@@ -151,6 +151,50 @@
(error (c) (log-message "MEMORY WARNING - Failed to load snapshot: ~a" c)))))
t)
;; v0.7.2 — Undo/Redo
(defvar *undo-stack* nil
"Ring buffer of pre-operation memory snapshots. Newest first, max 20.")
(defvar *redo-stack* nil
"Stack of snapshots saved during undo for redo. Max 20.")
(defun undo-snapshot ()
"Save current memory state to the undo stack."
(let ((snap (list :timestamp (get-universal-time)
:data (memory-hash-table-copy *memory-store*))))
(push snap *undo-stack*)
(when (> (length *undo-stack*) 20)
(setf *undo-stack* (subseq *undo-stack* 0 20)))))
(defun undo (&optional source)
"Restore memory to the most recent undo snapshot. Returns T on success, NIL if stack empty."
(declare (ignore source))
(if *undo-stack*
(let ((snap (pop *undo-stack*)))
(push (list :timestamp (get-universal-time)
:data (memory-hash-table-copy *memory-store*))
*redo-stack*)
(when (> (length *redo-stack*) 20)
(setf *redo-stack* (subseq *redo-stack* 0 20)))
(setf *memory-store* (memory-hash-table-copy (getf snap :data)))
(log-message "UNDO: Memory restored to snapshot ~a" (getf snap :timestamp))
t)
(progn (log-message "UNDO: No snapshots to undo") nil)))
(defun redo (&optional source)
"Restore memory to the most recent redo snapshot. Returns T on success, NIL if stack empty."
(declare (ignore source))
(if *redo-stack*
(let ((snap (pop *redo-stack*)))
(push (list :timestamp (get-universal-time)
:data (memory-hash-table-copy *memory-store*))
*undo-stack*)
(when (> (length *undo-stack*) 20)
(setf *undo-stack* (subseq *undo-stack* 0 20)))
(setf *memory-store* (memory-hash-table-copy (getf snap :data)))
(log-message "REDO: Memory restored to snapshot ~a" (getf snap :timestamp))
t)
(progn (log-message "REDO: No snapshots to redo") nil)))
(eval-when (:compile-toplevel :load-toplevel :execute)
(ql:quickload :fiveam :silent t))
@@ -211,3 +255,50 @@
(rollback-memory 0)
(is (not (null (memory-object-get "snap-a"))))
(is (null (memory-object-get "snap-b"))))
(test test-undo-snapshot-restore
"Contract v0.7.2: undo-snapshot captures state, undo restores."
(let ((orig-store passepartout::*memory-store*)
(orig-undo passepartout::*undo-stack*)
(orig-redo passepartout::*redo-stack*))
(unwind-protect
(progn
(setf passepartout::*memory-store* (make-hash-table :test 'equal)
passepartout::*undo-stack* nil
passepartout::*redo-stack* nil)
(passepartout::undo-snapshot)
(setf (gethash "x" passepartout::*memory-store*) "hello")
(is (string= "hello" (gethash "x" passepartout::*memory-store*)))
(is (passepartout::undo))
(is (null (gethash "x" passepartout::*memory-store*))))
(setf passepartout::*memory-store* orig-store
passepartout::*undo-stack* orig-undo
passepartout::*redo-stack* orig-redo))))
(test test-undo-redo-cycle
"Contract v0.7.2: redo restores undone state."
(let ((orig-store passepartout::*memory-store*)
(orig-undo passepartout::*undo-stack*)
(orig-redo passepartout::*redo-stack*))
(unwind-protect
(progn
(setf passepartout::*memory-store* (make-hash-table :test 'equal)
passepartout::*undo-stack* nil
passepartout::*redo-stack* nil)
(passepartout::undo-snapshot)
(setf (gethash "y" passepartout::*memory-store*) "world")
(is (passepartout::undo))
(is (null (gethash "y" passepartout::*memory-store*)))
(is (passepartout::redo))
(is (string= "world" (gethash "y" passepartout::*memory-store*))))
(setf passepartout::*memory-store* orig-store
passepartout::*undo-stack* orig-undo
passepartout::*redo-stack* orig-redo))))
(test test-undo-empty-stack-nil
"Contract v0.7.2: undo returns nil on empty stack."
(let ((orig-undo passepartout::*undo-stack*))
(unwind-protect
(progn (setf passepartout::*undo-stack* nil)
(is (null (passepartout::undo))))
(setf passepartout::*undo-stack* orig-undo))))