notes

package
v0.1.6 Latest Latest
Warning

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

Go to latest
Published: May 4, 2026 License: MIT Imports: 16 Imported by: 0

Documentation

Overview

Package notes implements the disk-backed notes subsystem ported from kgraph. Notes are Markdown files on disk (source of truth) with an optional YAML frontmatter block. An FTS5 index in the per-project SQLite database is maintained as a derived, rebuildable view.

Index

Constants

View Source
const MaxKeyLen = 512

MaxKeyLen caps note keys. Keeps paths well under filesystem limits even with the `.md` suffix and nested dirs. Phase-2 decision.

Variables

View Source
var ErrInvalidKey = errors.New("invalid note key")

ErrInvalidKey is returned when a key contains path-traversal components, absolute path prefixes, null bytes, or exceeds MaxKeyLen.

View Source
var ErrNotFound = errors.New("note not found")

ErrNotFound is returned when a note key does not exist on disk.

Functions

func Delete

func Delete(notesDir, key string) error

Delete removes the note file. Returns ErrNotFound if missing. Serialized via the per-project mutex (see Write).

func EncodeFrontmatter

func EncodeFrontmatter(data map[string]any, body []byte) ([]byte, error)

EncodeFrontmatter emits `---\n<yaml>\n---\n<body>`. An empty map emits the body verbatim (no leading delimiter). Keys are sorted by yaml.v3's default encoder (alphabetical); kgraph's gray-matter also sorts, so round-trips are stable.

func ExtractWikilinks(body []byte) []string

ExtractWikilinks returns the de-duplicated list of note keys referenced by `[[wikilink]]` or `[[wikilink|alias]]` occurrences in body, in the order of first appearance. Whitespace inside a target is preserved verbatim (Obsidian-style keys can contain spaces, though we normalize them to paths at the call site).

func ListKeys

func ListKeys(notesDir string) ([]string, error)

ListKeys is the cheap variant of List: it walks the tree once and returns keys only. Useful for the lean `GET /api/projects/.../notes` endpoint where frontmatter isn't needed.

func ParseFrontmatter

func ParseFrontmatter(raw []byte) (map[string]any, []byte, error)

ParseFrontmatter splits raw note bytes into a frontmatter map and a body. If the input does not begin with a `---` delimiter line the whole input is returned as the body with an empty map — this matches the kgraph gray-matter behavior (no frontmatter is not an error).

A body that happens to contain `---` on its own line elsewhere is preserved verbatim; only the FIRST delimiter pair is consumed.

func Related(g *Graph, key string) []string

Related returns the set of note keys directly connected to `key`, in either direction (outlinks + backlinks). Self-links are deduped. The result is sorted for deterministic API output.

func ValidateKey

func ValidateKey(key string) error

ValidateKey enforces the invariants documented on ErrInvalidKey. Public so handlers can short-circuit on bad input before touching the filesystem.

func Watch

func Watch(notesDir string, onChange func(key string)) (func(), error)

Watch scans notesDir every second and fires onChange(key) whenever a `.md` file's mtime changes, a new `.md` file appears, or an existing one is removed. The returned `stop` func halts the background goroutine.

This is a polling watcher — chosen over fsnotify to keep the watcher dependency-free and cross-platform. kgraph uses fs.watch (Node), which is similarly coarse in practice; the 1-second cadence is adequate for the "re-index after a user edits a note in their IDE" use case.

func Write

func Write(notesDir string, n *Note) error

Write persists n to disk atomically (temp file + rename). Parent directories are created as needed. The frontmatter embedded in the file is built from n.Frontmatter, then overlaid with Author, Tags, CreatedAt, UpdatedAt so the struct fields win over stale map entries.

Acquires the per-project mutex for the whole write+auto-commit sequence so concurrent writes to the same notesDir are serialized — without this, write N's bytes could land on disk before write N-1's commit runs, causing the earlier commit to record the later content.

Types

type Edge

type Edge struct {
	Source       string `json:"source"`
	Target       string `json:"target"`
	CrossProject bool   `json:"cross_project,omitempty"`
}

Edge represents a directed `[[wikilink]]` reference from Source → Target. The optional CrossProject field is true when Source and Target belong to different projects.

type Graph

type Graph struct {
	Nodes []NoteNode `json:"nodes"`
	Edges []Edge     `json:"edges"`
}

Graph is the wikilink-derived relation between notes in a project.

func BuildGraph

func BuildGraph(notesDir string, projectsRoot ...string) (*Graph, error)

BuildGraph walks every note in notesDir, extracts wikilinks from the body, and returns the resulting node+edge graph. Edges may point to nonexistent targets (dangling wikilinks) — that is intentional, and matches kgraph's behavior.

projectsRoot, when non-empty, is the parent directory that contains all project data dirs (i.e. the directory whose children are <slug>/notes/). It is used to resolve cross-project wikilinks. Pass "" to disable cross-project resolution (backward-compatible: same-project only).

type HistoryEntry

type HistoryEntry struct {
	Commit  string    `json:"commit"`
	Author  string    `json:"author"`
	Time    time.Time `json:"time"`
	Message string    `json:"message"`
}

HistoryEntry is a single commit entry in a note's auto-commit log.

func History

func History(notesDir, key string, limit int) ([]HistoryEntry, error)

History returns recent commits touching the given note key, newest first. If git is not installed or the repo has no history for the file, returns an empty slice with nil error — the endpoint should not 500 just because a project has never been committed to.

limit <= 0 is treated as "no cap".

type Note

type Note struct {
	Key         string         `json:"key"`
	Content     string         `json:"content"`
	Author      string         `json:"author,omitempty"`
	Tags        []string       `json:"tags,omitempty"`
	Frontmatter map[string]any `json:"frontmatter,omitempty"`
	CreatedAt   time.Time      `json:"created_at"`
	UpdatedAt   time.Time      `json:"updated_at"`
}

Note is the in-memory representation of a Markdown note.

Frontmatter is the raw YAML map; Author/Tags are convenience copies hoisted from common keys. Timestamps are derived from filesystem mtime on Read and from time.Now() on Write.

func List

func List(notesDir string) ([]*Note, error)

List returns every note found under notesDir, sorted by key. Missing notesDir is treated as "no notes" (empty slice, nil error) — handlers should not 500 just because nobody has written a note yet.

func Read

func Read(notesDir, key string) (*Note, error)

Read loads the note at key from notesDir. Returns ErrNotFound if the `.md` file does not exist.

type NoteNode

type NoteNode struct {
	Key     string   `json:"key"`
	Title   string   `json:"title"`
	Folder  string   `json:"folder"`
	Tags    []string `json:"tags,omitempty"`
	Project string   `json:"project,omitempty"` // non-empty for cross-project nodes
	Missing bool     `json:"missing,omitempty"` // true when target file is absent
}

NoteNode is a lean projection of a Note for graph rendering. The optional Project field is non-empty only for cross-project nodes. The optional Missing field is true when the target note does not exist on disk.

type TreeNode

type TreeNode struct {
	Name     string      `json:"name"`
	Path     string      `json:"path"`
	Type     string      `json:"type"` // "folder" | "note"
	Children []*TreeNode `json:"children,omitempty"`
}

TreeNode is the recursive folder/file tree returned by Tree().

func Tree

func Tree(notesDir string) (*TreeNode, error)

Tree returns the recursive folder/note tree rooted at notesDir. Used by the `/api/projects/{p}/tree` endpoint.

type Wikilink struct {
	Target       string // raw target string, unchanged
	Project      string // "" for same-project; slug of the target project otherwise
	Key          string // the note key within that project (or the raw target for same-project)
	CrossProject bool   // true iff target is a cross-project reference
}

Wikilink holds a parsed wikilink target broken into its constituent parts.

func ParseWikilink(target string) Wikilink

ParseWikilink parses a raw wikilink target (as returned by ExtractWikilinks) into a Wikilink. A target matches the cross-project pattern when it is of the form "projects/<slug>/<rest>" and <slug> is a non-empty string of alphanumerics, hyphens, and underscores. Any other shape is treated as a same-project key.

Jump to

Keyboard shortcuts

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