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
- Variables
- func ApplyReversal(s *store.Store, ws string, force, wantRedo bool) (path, op string, did bool, err error)
- func DisplayOp(op string) string
- func IsRedoEntry(op string) bool
- func PendingTS(s *store.Store) (ts time.Time, ok bool)
- func RecordUndo(s *store.Store, e UndoEntry) error
- func Slug(title string) string
- func TitleOverlap(a, b string) float64
- type Cache
- type Kind
- type Note
- func (n *Note) ChangedDate() string
- func (n *Note) Description(max int) string
- func (n *Note) Expired(now time.Time) bool
- func (n *Note) MTimeToken() string
- func (n *Note) NotYetValid(now time.Time) bool
- func (n *Note) Pinned() bool
- func (n *Note) Project() string
- func (n *Note) Reserved() bool
- func (n *Note) Save() error
- func (n *Note) SaveIfUnchanged(expect string) error
- type Pair
- type StaleNoteError
- type Tiers
- type UndoEntry
Constants ¶
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.
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 ¶
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
DisplayOp strips the internal "redo:" prefixes off an op label for messages.
func IsRedoEntry ¶ added in v0.24.0
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
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
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 ¶
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
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.
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
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
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 Load ¶
Load parses a note file (frontmatter + body). Unknown frontmatter keys are ignored, not an error.
func (*Note) ChangedDate ¶ added in v0.22.0
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
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
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
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
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
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
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
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) SaveIfUnchanged ¶ added in v0.24.0
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
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
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
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.