Documentation
¶
Overview ¶
Package mutate is the single ULID-keyed mutation engine shared by the CLI and (later) the TUI, implementing the write contract from SPEC §6.1–6.3. Every write to tasks.txt goes through Apply: acquire the lock, re-read the file from disk, run the caller's mutation against the fresh Doc while recording before-images, append the undo transaction, then atomically write. Nothing else writes tasks.txt, so a concurrent `nt add` can never be clobbered by a stale in-memory snapshot.
Index ¶
- func Complete(d *task.Doc, rec *Recorder, t *task.Task, today string)
- func Today() string
- type DoctorReport
- type Engine
- func (e *Engine) Apply(op string, fn func(d *task.Doc, rec *Recorder) error) error
- func (e *Engine) Archive() (int, error)
- func (e *Engine) Doctor(apply bool) (DoctorReport, error)
- func (e *Engine) PeekUndo() (label string, isRedo bool, ok bool)
- func (e *Engine) PeekUndoTxn() (txn undo.Txn, label string, ok bool)
- func (e *Engine) Read() (*task.Doc, error)
- func (e *Engine) Redo(ws string, force bool) (undo.Txn, bool, error)
- func (e *Engine) RenameNote(src *note.Note, all []*note.Note, dest string) (newRel string, updated int, err error)
- func (e *Engine) TrashNote(n *note.Note) error
- func (e *Engine) Undo() (op string, did bool, err error)
- func (e *Engine) UndoScoped(ws string, force bool) (undo.Txn, bool, error)
- func (e *Engine) UnlinkNote(target *note.Note) (updated int, err error)
- type Recorder
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Complete ¶
Complete marks a task done, spawning the next occurrence if it recurs (SPEC §9). Both the completion and the spawned task are recorded in the same undo transaction (rec captures the original's before-image and the spawn as an add), so a single `nt undo` reopens the original and removes the new one (SPEC §6.3). Must run inside an Engine.Apply closure.
Types ¶
type DoctorReport ¶
type DoctorReport struct {
DupIDsRemoved int // duplicate-ULID lines dropped (within tasks.txt)
CrossFileDups int // tasks dropped from tasks.txt because done.txt has them
IDsAssigned int // task lines that lacked an id and got one
Actions []string // human-readable, one per fix
Warnings []string // structural problems doctor reports but can't auto-fix
}
DoctorReport summarizes what Doctor found (and, unless dry-run, fixed).
func (DoctorReport) HasProblems ¶ added in v0.4.0
func (r DoctorReport) HasProblems() bool
HasProblems reports whether anything is wrong — fixable issues OR warnings — so `nt doctor --check` exits non-zero on a dependency cycle / dangling edge even when there's nothing to rewrite.
func (DoctorReport) Issues ¶
func (r DoctorReport) Issues() int
Issues is the count of auto-fixable problems (used to decide whether to write).
type Engine ¶
type Engine struct {
S *store.Store
// LockTimeout bounds how long a mutation waits for the store lock. Zero
// means lock.DefaultTimeout, which is what every production path uses.
// It exists for tests that deliberately pile many concurrent writers onto
// one store: writers there serialize behind each other, so the tail of the
// queue can exceed the default budget on a loaded machine and report the
// store busy — a property of the test's own contention, not of the
// lock-and-re-read contract it means to assert.
LockTimeout time.Duration
}
Engine owns the store and serializes all task-file writes.
func (*Engine) Apply ¶
Apply runs fn under the lock against a freshly-read Doc, journals the resulting transaction, and atomically writes the file. fn mutates the Doc and uses rec to record what it touched.
func (*Engine) Archive ¶
Archive moves completed tasks from tasks.txt to done.txt under the lock. It is an explicit housekeeping action and is intentionally not journaled — the cross-file move is the open question in SPEC §15, so `nt undo` does not revert it (the CLI says so). Returns the number of tasks moved.
func (*Engine) Doctor ¶
func (e *Engine) Doctor(apply bool) (DoctorReport, error)
Doctor reconciles tasks.txt after a git merge (or hand-editing): it drops duplicate-ULID lines that a `merge=union` merge can leave behind, and assigns ids to any task line missing one. Like Archive it runs under the lock and is intentionally NOT journaled — it's a repair, and git is the recovery path for a git-tracked store. With apply=false it reports without writing (dry-run).
Duplicate resolution keeps one line per id, preferring a completed line so a "done on one branch" state is never lost.
func (*Engine) PeekUndo ¶ added in v0.4.0
PeekUndo reports what the next reversal targets, without changing anything: the human label (with the internal "redo:" prefixes stripped) and whether the pending journal entry is a redo (an odd number of "redo:" prefixes) rather than a fresh forward op. ok is false when the journal is empty. It is a display-only read of the atomically-written journal, so it runs lock-free.
func (*Engine) PeekUndoTxn ¶ added in v0.22.0
PeekUndoTxn returns the pending journal transaction itself (op label raw, including any internal "redo:" prefixes), the stripped display label, and whether one exists — so callers can inspect its timestamp/workstream before deciding to revert (e.g. the CLI's note-edited-since guard). Lock-free, display-only, like PeekUndo.
func (*Engine) Read ¶
Read returns the current parsed document (no lock; read-only callers such as list/search tolerate a consistent-at-read-time snapshot).
func (*Engine) Redo ¶ added in v0.22.0
Redo re-applies the most recently undone transaction (the swapped "redo:" entry Undo leaves on the journal). did is false when the pending entry is not a redo — i.e. there is nothing to redo. Ownership is enforced like UndoScoped.
func (*Engine) RenameNote ¶
func (e *Engine) RenameNote(src *note.Note, all []*note.Note, dest string) (newRel string, updated int, err error)
RenameNote renames or moves a note file and rewrites every [[link]] to it across tasks.txt and notes/ (preserving each link's folder prefix, #fragment, and |alias). dest is a new name or a folder/path relative to notes/ (.md optional); a bare name keeps the note in its current folder. It refuses a destination whose basename collides with another note (which would make bare links ambiguous). A pure move (same basename) needs no link rewrite, since resolution is by path-suffix. Like Archive it is not a single undo transaction.
func (*Engine) TrashNote ¶ added in v0.16.0
TrashNote moves a note file into the store's .trash/ (recoverable by hand). Like RenameNote/Archive it is a file move, not a journaled undo transaction; callers resolve inbound links first (UnlinkNote) when they don't want dangles.
func (*Engine) Undo ¶
Undo reverts the most recent transaction (SPEC §6.3). Under the lock it:
- validates that current state still matches the transaction's recorded post-image by ULID — if the world moved underneath (another writer changed/removed a touched task), it refuses rather than corrupting state;
- applies each change's inverse (restoring before-images by ULID) WITHOUT resurrecting a task another writer removed;
- durably writes the reverted tasks file FIRST, then removes the journal entry and records a swapped redo transaction — so a tasks-write failure can't lose the inverse (it stays in the journal, retryable).
The returned op label names what was undone; did is false when there is nothing to undo.
func (*Engine) UndoScoped ¶ added in v0.22.0
UndoScoped is Undo with workstream ownership enforced, for shared stores where several agents write concurrently: when ws is non-empty and the pending transaction was written by a DIFFERENT workstream (or by an unscoped writer), it refuses — unless force — so one agent can't silently revert another's work. The check runs under the same lock as the revert, so the answer can't go stale between look and act. The reverted transaction is returned so the caller can show exactly what changed.
func (*Engine) UnlinkNote ¶ added in v0.16.0
UnlinkNote strips every inbound [[link]] to the target note across tasks.txt and notes/, replacing each with its plain display text, so deleting the note leaves no dangling links. Returns how many lines/files were rewritten. Like RenameNote it is not a single undo transaction.
type Recorder ¶
type Recorder struct {
// contains filtered or unexported fields
}
Recorder accumulates the before/after images of touched tasks so a single undo transaction can reverse the whole mutation.