mutate

package
v0.18.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 17, 2026 License: MIT Imports: 11 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func Complete

func Complete(d *task.Doc, rec *Recorder, t *task.Task, today string)

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.

func Today

func Today() string

Today returns today's date in todo.txt form.

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
}

Engine owns the store and serializes all task-file writes.

func Open

func Open() (*Engine, error)

Open resolves the store and returns an engine.

func (*Engine) Apply

func (e *Engine) Apply(op string, fn func(d *task.Doc, rec *Recorder) error) error

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

func (e *Engine) Archive() (int, error)

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

func (e *Engine) PeekUndo() (label string, isRedo bool, ok bool)

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) Read

func (e *Engine) Read() (*task.Doc, error)

Read returns the current parsed document (no lock; read-only callers such as list/search tolerate a consistent-at-read-time snapshot).

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

func (e *Engine) TrashNote(n *note.Note) error

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

func (e *Engine) Undo() (op string, did bool, err error)

Undo reverts the most recent transaction (SPEC §6.3). Under the lock it:

  1. 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;
  2. applies each change's inverse (restoring before-images by ULID) WITHOUT resurrecting a task another writer removed;
  3. 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) UnlinkNote added in v0.16.0

func (e *Engine) UnlinkNote(target *note.Note) (updated int, err error)

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.

func (*Recorder) Added

func (r *Recorder) Added(t *task.Task)

Added records a newly appended task (no before-image; undo deletes it).

func (*Recorder) Before

func (r *Recorder) Before(t *task.Task)

Before records a task's current line before the caller mutates it. Call it immediately prior to changing the task.

func (*Recorder) Removed

func (r *Recorder) Removed(id, before string)

Removed records a task removed from the document, capturing its before-image.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL