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 SplitPathTitle(raw string) (folder, title 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 SplitPathTitle ¶ added in v0.29.0
SplitPathTitle interprets the path-style filing shorthand shared by the CLI and web note-create surfaces: "work/Auth design" files "Auth design" under work/. The slash counts as filing syntax ONLY when everything before the last slash is path-like (non-empty, no whitespace) and a non-empty title remains after it. Otherwise the slash is prose — "…valid at .claude/x/", "docs live under apps/web" — and the whole string stays the title; returning folder "" tells the caller no filing choice was made.
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, tags and project — 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.
project is folded into the shared-tag test alongside tags: `--project` stores it as a separate `project:` frontmatter field, not a tag, but for a note whose only tag is a class marker (lesson/rule/memory-core — stripped by structuralTag below) the tag set would otherwise be empty and the Jaccard branch could never fire, silently disabling the guard for the most common kind of note. project isn't written to disk as a tag; it only joins the in-memory set this function compares on.
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) Save ¶
Save writes the note atomically with frontmatter. Returns an error — and writes nothing — if any frontmatter value would corrupt the block (see invalidFrontmatterLine); we reject rather than silently strip/escape the offending bytes, because a value that trips this came from a caller (or whatever untrusted text it captured) trying to smuggle extra frontmatter, and silently sanitizing it would hide that instead of surfacing 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.