note

package
v0.27.0 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 13 Imported by: 0

Documentation

Overview

Package note implements nt's markdown notes with light YAML frontmatter (SPEC §5). Notes are one file each under notes/, so they need no shared lock: creation and edits are atomic single-file writes.

Index

Constants

View Source
const (
	// TierSmallStore is the note count at or below which the index shows
	// everything untired (today's behavior).
	TierSmallStore = 30
	// TierRecentDays is the recency window for the middle tier.
	TierRecentDays = 14
	// TierRecentCap bounds the recent tier even during a busy fortnight.
	TierRecentCap = 50
	// TierPinnedWarn is the pinned-tier size doctor warns at — "always shown"
	// invites dumping, so the cost is made legible rather than forbidden.
	TierPinnedWarn = 15
)

The index's tiering knobs. The catalog used to print one stub per note forever (~60 tokens each), so a session-start `nt index` grew linearly with store HISTORY; tiering bounds it by store CONVENTIONS (pinned) + recent ACTIVITY instead. Small stores are never tiered — completeness is cheap there.

View Source
const TaskNoteFolder = "__tasks__"

TaskNoteFolder is the subfolder under notes/ where a task's "body" notes live (auto-split paragraph captures and explicit task detail). The double-underscore name is deliberately "reserved-looking" so it won't collide with a plain "tasks" folder a user might keep for their own hand-curated notes; grouping these machine-created notes here keeps them out of a human's folders — like the "journal" folder does for daily notes.

Variables

View Source
var Kinds = map[string]Kind{
	"lesson":   {Folder: "lessons", Tag: "lesson"},
	"decision": {Folder: "decisions", Tag: "decision"},
	"ref":      {Folder: "ref", Tag: "ref"},
	"rule":     {Folder: "rules", Tag: "rule"},
	"memory":   {Folder: "memory", Tag: "memory-core"},
}

Kinds maps a note class (CLI --kind / MCP kind:) to its canonical folder + tag, so multi-agent stores converge on one taxonomy instead of inventing folders. Shared by the CLI and MCP surfaces — keep them identical. "memory" is the always-loaded core-memory layer (the OpenCode plugin injects it): files under memory/ carrying the memory-core tag, NOT a bare "memory" tag.

Functions

func ApplyReversal added in v0.24.0

func ApplyReversal(s *store.Store, ws string, force, wantRedo bool) (path, op string, did bool, err error)

ApplyReversal reverts (or re-applies, if wantRedo) the most recent pending note edit: it restores the file's exact prior bytes and swaps the journal entry to a "redo:"-prefixed inverse holding the bytes it just overwrote — the same toggle-in-place mechanic mutate.applyReversal uses for tasks. did is false when there's nothing pending, or the pending entry doesn't match wantRedo (a plain undo won't re-apply a redo entry, and vice versa). ws/force mirror the task journal's shared-store ownership check: on a shared store, reverting another workstream's note edit is refused unless force is set.

func DisplayOp added in v0.24.0

func DisplayOp(op string) string

DisplayOp strips the internal "redo:" prefixes off an op label for messages.

func IsRedoEntry added in v0.24.0

func IsRedoEntry(op string) bool

IsRedoEntry mirrors the task journal's convention (mutate.isRedoEntry): an odd number of "redo:" prefixes on Op means the entry re-applies a previously undone edit rather than being a fresh forward change.

func PendingTS added in v0.24.0

func PendingTS(s *store.Store) (ts time.Time, ok bool)

PendingTS returns the pending note-undo entry's timestamp (RFC3339Nano) and whether one exists, without changing anything — used to decide whether `nt undo` should target the note journal or the task journal (whichever is more recent). Parse failures report ok=false so a corrupt/old-format timestamp never wins the race by accident.

func RecordUndo added in v0.24.0

func RecordUndo(s *store.Store, e UndoEntry) error

RecordUndo appends an undo entry for a note edit, then compacts the journal once it grows past compactThreshold. Call it with the note's raw file bytes captured BEFORE the edit was written.

func Slug

func Slug(title string) string

Slug derives a filesystem-safe slug from a title, falling back to a timestamp when the title yields nothing usable (à la nb).

func TitleOverlap added in v0.20.0

func TitleOverlap(a, b string) float64

TitleOverlap is the word-set Jaccard (0..1) of two titles, ignoring short and stopword tokens — the similarity heuristic behind duplicate detection, exported so task-side dedup can reuse the exact same notion nt uses for notes.

Types

type Cache added in v0.4.0

type Cache struct {
	// contains filtered or unexported fields
}

Cache is an mtime-keyed parse cache for note files. List walks notes/ but only re-reads and re-parses files whose size or mtime changed since last time — turning a snapshot rebuild from "read+parse every note" into "stat every note, read+parse the few that changed". This is what lets the in-memory read-model scale to thousands of notes (a single edit no longer re-reads the whole store).

Returned *Note values are shared with the cache; the read-model treats them as read-only, so this is safe. Cache is safe for concurrent use.

func NewCache added in v0.4.0

func NewCache() *Cache

NewCache returns an empty note cache.

func (*Cache) ByID added in v0.20.0

func (c *Cache) ByID(id string) *Note

ByID returns the note with the given id as of the last List (nil if none). It does not walk the dir, so callers should List first to refresh — the id index is rebuilt on every List. This turns a get-by-id from an O(notes) resolve scan into an O(1) map lookup.

func (*Cache) List added in v0.4.0

func (c *Cache) List(s *store.Store) ([]*Note, error)

List returns all notes under notes/, reusing unchanged files from the cache and re-parsing only those that were added or modified. Deleted files are evicted. Output ordering matches note.List (by Rel).

type Kind added in v0.22.0

type Kind struct {
	Folder string // canonical folder under notes/
	Tag    string // canonical tag stamped on the note
}

Kind is the canonical folder + tag pair for one note class (see Kinds).

type Note

type Note struct {
	Path     string
	Rel      string // path relative to notes/ (slash-separated), set by List
	ID       string
	Title    string
	Tags     []string
	Aliases  []string
	Source   string
	Created  string
	Updated  string // stamped when nt rewrites the note (retag, --field)
	Archived bool   // frontmatter archived: true — retired from active views, still on disk
	Favorite bool   // frontmatter favorite: true — starred/pinned for quick access
	// SupersededBy is the id of the note that replaces this one (frontmatter
	// superseded_by:). A superseded note is dropped from active views like an
	// archived one — so a resume sees the single canonical decision, not both
	// forks — while the pointer preserves the trail.
	SupersededBy string
	// ValidFrom/ValidUntil are optional frontmatter dates (YYYY-MM-DD or
	// RFC3339) marking a fact's validity window — Zep's idea, without a
	// temporal graph database: a note can be "true as of" or "true until"
	// without a full supersede. Unlike Archived/SupersededBy (which retire a
	// note from active views entirely), an expired note stays visible — just
	// down-ranked in nt_recall and flagged, so an agent still finds it but
	// knows to doubt it. See Expired/NotYetValid.
	ValidFrom  string
	ValidUntil string
	// ModTime is the note file's last-modified time, set by List/Load/cache. It
	// captures every change — including edits made outside nt (Obsidian, git) that
	// never touch the `updated:` frontmatter — so "changed since T" is reliable.
	ModTime time.Time
	Body    string
	Extra   []string // raw frontmatter lines for keys nt doesn't model (preserved verbatim)
}

Note is a parsed markdown note.

func Active added in v0.12.0

func Active(ns []*Note) []*Note

Active drops notes retired from the working set: archived notes and superseded ones (a superseded note has a newer canonical version, so views show only the current decision, not both forks).

func Create

func Create(s *store.Store, title, body string, tags []string, source, folder string) (*Note, error)

Create builds and writes a new note, returning it. The body is prefixed with an H1 title when it doesn't already start with one. Create writes a new note. folder, when non-empty, is a slash-separated subfolder under notes/ (e.g. "work" or "work/auth"); it is created as needed. The filename is slugged from the title; the body and frontmatter are written by Save.

func FindSimilar added in v0.20.0

func FindSimilar(notes []*Note, title string, tags []string) []*Note

FindSimilar returns active, non-reserved notes that look like near-duplicates of a note with the given title and tags — a guard against concurrent forks (two agents independently recording the same decision). A candidate matches when it has the identical slug, OR it shares a tag AND its title word-set overlaps heavily (Jaccard ≥ 0.5) — UNLESS the pair is a "parallel sibling": each note carries a distinguishing tag the other lacks that also appears in its own title ("taskly repo map" @taskly vs "ratelim repo map" @ratelim). Multi-project stores legitimately hold same-shaped notes per project; the project tag in the title is how the pair self-identifies as distinct. This is a cheap heuristic, not semantic dedup.

func List

func List(s *store.Store) ([]*Note, error)

func Load

func Load(path string) (*Note, error)

Load parses a note file (frontmatter + body). Unknown frontmatter keys are ignored, not an error.

func (*Note) ChangedDate added in v0.22.0

func (n *Note) ChangedDate() string

ChangedDate is the note's effective change date (YYYY-MM-DD): the later of its file mtime (catches external edits) and its frontmatter updated/created.

func (*Note) Description added in v0.20.0

func (n *Note) Description(max int) string

List loads all notes in the store's notes directory, recursing into subfolders so an Obsidian-style nested vault works. Hidden dirs (.obsidian/, .trash/, .git/) and non-.md files are skipped. Each note's Rel (path relative to notes/, slash-separated) is set for link resolution; results are sorted by Rel for deterministic ordering. Active drops archived notes — the working set, for views/search that should hide retired notes. List itself returns everything (archived included) so link-rewriting and the archived view still see them. Description returns the note's one-line summary for index/stub views: its `description:` frontmatter if set (kept in Extra, since nt doesn't model the key), else the first non-heading body line. Clamped to a single line ≤max chars. This is the "one-sentence summary" granularity of progressive disclosure — what an agent reads to decide whether to open the full note.

func (*Note) Expired added in v0.24.0

func (n *Note) Expired(now time.Time) bool

Expired reports whether n's valid_until has passed as of now. false when valid_until is unset or unparseable — absence is never treated as expired.

func (*Note) MTimeToken added in v0.24.0

func (n *Note) MTimeToken() string

MTimeToken returns a stable, comparable string for n's ModTime — the optimistic-concurrency token callers round-trip through SaveIfUnchanged. "" when ModTime is unset (e.g. a just-created note that hasn't been loaded from disk).

func (*Note) NotYetValid added in v0.24.0

func (n *Note) NotYetValid(now time.Time) bool

NotYetValid reports whether n's valid_from is still in the future as of now. false when valid_from is unset or unparseable.

func (*Note) Pinned added in v0.22.0

func (n *Note) Pinned() bool

Pinned reports whether a note belongs to the always-shown index tier: standing knowledge whose relevance does not decay with file age.

func (*Note) Project added in v0.22.0

func (n *Note) Project() string

Project returns the note's `project:` frontmatter value ("" when unset). The key isn't modeled (it rides in Extra, preserved verbatim); this accessor is how recall's same-project boost reads a note's declared project membership — alongside its tags and folder path.

func (*Note) Reserved added in v0.20.0

func (n *Note) Reserved() bool

Reserved reports whether a note lives in a machine-managed folder that isn't part of the human/agent knowledge base — currently notes/__tasks__/, where nt files the detail bodies of split tasks. These are reachable by id/link but are kept out of the KB catalog (nt index) and search so they don't pollute it.

func (*Note) Save

func (n *Note) Save() error

Save writes the note atomically with frontmatter.

func (*Note) SaveIfUnchanged added in v0.24.0

func (n *Note) SaveIfUnchanged(expect string) error

SaveIfUnchanged writes n atomically, refusing with a *StaleNoteError when expect is non-empty and doesn't match the file's CURRENT on-disk mtime (a fresh os.Stat, not n's own possibly-stale ModTime) — "only save if the file still looks like what I last saw." expect == "" (the default — most callers have no token to offer) skips the check entirely, matching plain Save.

This is best-effort optimistic concurrency, not a hard guarantee: it catches the dominant real case (an agent saving a copy it loaded some time ago while another writer touched the file meanwhile), the same way the web layer's If-Match does — except the web layer serializes through one process, while the CLI/MCP path is multi-process, so a residual stat-then-rename race window remains between the check and the write.

type Pair added in v0.24.0

type Pair struct{ A, B *Note }

Pair is one near-duplicate-by-title match — shared by `nt doctor`'s near-dup lint (CLI), `nt distill`/`nt_distill` (uncapped, full fields), and anywhere else two notes need to be flagged as probably-the-same-thing.

func NearDupPairs added in v0.24.0

func NearDupPairs(active []*Note) []Pair

NearDupPairs finds pairs of active, non-reserved (not a machine task-detail note) notes with near-duplicate titles — the store rot that degrades recall most (title-token overlap + a shared tag, or an exact-except-case title; see FindSimilar). A pair where EITHER note carries the `distinct` tag is a sanctioned fork (a deliberate --force the author already acknowledged) — excluded so a caller doesn't nag forever about it. At most one match is reported per note (the first found).

type StaleNoteError added in v0.24.0

type StaleNoteError struct {
	Path string
}

StaleNoteError reports that a note changed on disk since the caller last saw it — SaveIfUnchanged's refusal.

func (*StaleNoteError) Error added in v0.24.0

func (e *StaleNoteError) Error() string

type Tiers added in v0.22.0

type Tiers struct {
	Pinned        []*Note
	Recent        []*Note
	OlderByFolder map[string]int
	OlderTotal    int
	Tiered        bool // false ⇒ small store, everything is in Recent
}

Tiers is the index default view of a large store: standing notes always, recent activity in full, and the long tail as per-folder counts (each with the expansion path printed by the caller — nothing hides silently).

func TierIndex added in v0.22.0

func TierIndex(notes []*Note, now time.Time) Tiers

TierIndex splits an already-filtered index note set into tiers as of `now`. Order within tiers: Pinned by folder/title (stable shape), Recent newest first (what changed since last session reads top-down).

func (*Tiers) LimitRecent added in v0.22.0

func (t *Tiers) LimitRecent(n int)

LimitRecent truncates the recent tier to n stubs, moving the overflow into the rollup counts so pinned + recent + older still equals the total — a --limit must shrink the listing, never make notes vanish from the math.

type UndoEntry added in v0.24.0

type UndoEntry struct {
	Op     string `json:"op"`           // human label: "appended to" | "replaced body of" | "edited" | "set description of"
	TS     string `json:"ts"`           // RFC3339Nano
	WS     string `json:"ws,omitempty"` // workstream that made the edit ("" = unscoped)
	Path   string `json:"path"`
	Before string `json:"before"` // the file's full raw bytes before the edit
}

UndoEntry is one undoable note-body edit: the file's exact raw bytes (frontmatter + body) before the edit. Unlike the task journal's Change (which records both before AND after per task line, because a task Doc is validated line-by-line against a post-image), a note edit only ever needs its before-image — a note is a single file, so the after-image is always just "whatever's on disk now," recoverable by reading it.

func PeekUndo added in v0.24.0

func PeekUndo(s *store.Store) (UndoEntry, bool, error)

PeekUndo returns the most recent pending note-undo entry, or ok=false if there is none.

Jump to

Keyboard shortcuts

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