From f76535402c84fdc2066ee5ef2dd1f835838eef73 Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Thu, 6 Aug 2026 12:30:41 -0500 Subject: [PATCH 1/4] bug: fixed front matter fields provided inline --- VERSION | 2 +- internal/note/note.go | 25 +++++- internal/note/note_test.go | 169 +++++++++++++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 81fd7ba..eac0a14 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.2.0 \ No newline at end of file +v0.2.1 \ No newline at end of file diff --git a/internal/note/note.go b/internal/note/note.go index d971664..2dce7ac 100644 --- a/internal/note/note.go +++ b/internal/note/note.go @@ -352,11 +352,34 @@ func AddNote(cfg *config.Config, in NoteInput) (NoteOutcome, error) { } hasInput := in.Body != "" || in.FromFile != "" + hasMetadata := fieldSet(in.Filename) && fieldSet(in.Synopsis) && fieldSet(in.Source) + + if hasInput && hasMetadata { + // Caller supplied all required metadata and a body; create the note + // directly. Strip any frontmatter in the body so it isn't duplicated + // by WriteFrontmatter; if there is no frontmatter, keep the body as-is. + body := in.Body + if _, parsed, hasFM := ParseFrontmatterString(in.Body); hasFM { + body = parsed + } + var path string + var err error + if in.FromFile != "" { + path, err = IngestFile(cfg, in.FromFile, in.Filename, in.Synopsis, in.Source, target, body) + } else { + path, err = NewWithBody(cfg, in.Filename, in.Synopsis, in.Source, target, body) + } + if err != nil { + return NoteOutcome{}, err + } + return NoteOutcome{Path: path}, nil + } + if hasInput { return addFromInput(cfg, in, target) } - if fieldSet(in.Filename) && fieldSet(in.Synopsis) && fieldSet(in.Source) { + if hasMetadata { path, err := NewWithBody(cfg, in.Filename, in.Synopsis, in.Source, target, "") if err != nil { return NoteOutcome{}, err diff --git a/internal/note/note_test.go b/internal/note/note_test.go index aaedb6a..9abce87 100644 --- a/internal/note/note_test.go +++ b/internal/note/note_test.go @@ -3,6 +3,7 @@ package note import ( "os" "path/filepath" + "strings" "testing" "github.com/polymorcodeus/park/internal/config" @@ -300,3 +301,171 @@ func TestExtractH1(t *testing.T) { }) } } + +func TestAddNote(t *testing.T) { + tmp := t.TempDir() + cfg := config.DefaultConfig(tmp) + for i := range cfg.Categories { + if err := os.MkdirAll(cfg.Categories[i].Path, 0o755); err != nil { + t.Fatalf("create category folder: %v", err) + } + } + + t.Run("all metadata no body creates note", func(t *testing.T) { + in := NoteInput{ + Filename: "test-note", + Synopsis: "a test", + Source: "terminal", + Category: "inbox", + } + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form != nil { + t.Fatal("expected direct creation, got form") + } + if out.Path == "" { + t.Fatal("expected path, got empty") + } + fm, _, err := ParseFrontmatter(out.Path) + if err != nil { + t.Fatalf("ParseFrontmatter() error = %v", err) + } + if fm.Synopsis != "a test" || fm.Source != "terminal" || fm.Category != "inbox" { + t.Errorf("frontmatter mismatch: %+v", fm) + } + }) + + t.Run("file with all metadata creates directly", func(t *testing.T) { + src := filepath.Join(tmp, "body.md") + content := "## Body\n\ncontent\n" + if err := os.WriteFile(src, []byte(content), 0o644); err != nil { + t.Fatalf("WriteFile() error = %v", err) + } + in := NoteInput{ + Filename: "from-file", + Synopsis: "from file", + Source: "migration", + Category: "archive", + Body: content, + FromFile: src, + } + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form != nil { + t.Fatal("expected direct creation, got form") + } + if _, err := os.Stat(src); !os.IsNotExist(err) { + t.Errorf("source file was not removed") + } + fm, body, err := ParseFrontmatter(out.Path) + if err != nil { + t.Fatalf("ParseFrontmatter() error = %v", err) + } + if fm.Category != "archive" || fm.Synopsis != "from file" || fm.Source != "migration" { + t.Errorf("frontmatter mismatch: %+v", fm) + } + if body != content { + t.Errorf("body = %q, want %q", body, content) + } + }) + + t.Run("stdin body without frontmatter and all metadata creates directly", func(t *testing.T) { + in := NoteInput{ + Filename: "piped", + Synopsis: "piped body", + Source: "stdin", + Category: "projects", + Body: "# Piped title\n\ntext\n", + } + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form != nil { + t.Fatal("expected direct creation, got form") + } + fm, body, err := ParseFrontmatter(out.Path) + if err != nil { + t.Fatalf("ParseFrontmatter() error = %v", err) + } + if fm.Category != "projects" { + t.Errorf("category = %q, want projects", fm.Category) + } + if body != "# Piped title\n\ntext\n" { + t.Errorf("body = %q, want %q", body, "# Piped title\n\ntext\n") + } + }) + + t.Run("body with frontmatter uses frontmatter values", func(t *testing.T) { + in := NoteInput{ + Body: "---\ncategory: areas\nsource: chat\nsynopsis: fm-driven\ncreated: 2026-07-01\n---\n\n# Title\n\nbody\n", + } + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form != nil { + t.Fatal("expected direct creation, got form") + } + fm, _, err := ParseFrontmatter(out.Path) + if err != nil { + t.Fatalf("ParseFrontmatter() error = %v", err) + } + if fm.Category != "areas" || fm.Source != "chat" || fm.Synopsis != "fm-driven" { + t.Errorf("frontmatter mismatch: %+v", fm) + } + }) + + t.Run("body with frontmatter and explicit metadata uses explicit metadata", func(t *testing.T) { + in := NoteInput{ + Filename: "explicit", + Synopsis: "explicit", + Source: "explicit", + Category: "archive", + Body: "---\ncategory: areas\nsource: chat\nsynopsis: fm-driven\ncreated: 2026-07-01\n---\n\n# Title\n\nbody\n", + } + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form != nil { + t.Fatal("expected direct creation, got form") + } + fm, body, err := ParseFrontmatter(out.Path) + if err != nil { + t.Fatalf("ParseFrontmatter() error = %v", err) + } + if fm.Category != "archive" || fm.Source != "explicit" || fm.Synopsis != "explicit" { + t.Errorf("frontmatter mismatch: %+v", fm) + } + if strings.Contains(body, "fm-driven") { + t.Errorf("body still contains old frontmatter") + } + }) + + t.Run("missing metadata without body returns form", func(t *testing.T) { + in := NoteInput{Filename: "only-filename"} + out, err := AddNote(cfg, in) + if err != nil { + t.Fatalf("AddNote() error = %v", err) + } + if out.Form == nil { + t.Fatal("expected form outcome") + } + }) + + t.Run("body with incomplete frontmatter returns error", func(t *testing.T) { + in := NoteInput{ + Body: "---\ncategory: inbox\n---\n\nbody\n", + } + _, err := AddNote(cfg, in) + if err == nil { + t.Fatal("expected error for incomplete frontmatter") + } + }) +} + From 0589998027cb1267e04e5aaea49b78cf3bc81f50 Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Thu, 6 Aug 2026 12:35:47 -0500 Subject: [PATCH 2/4] fmt: bad space --- internal/note/note_test.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/note/note_test.go b/internal/note/note_test.go index 9abce87..8abe4d2 100644 --- a/internal/note/note_test.go +++ b/internal/note/note_test.go @@ -468,4 +468,3 @@ func TestAddNote(t *testing.T) { } }) } - From 5dd2a7247daa6da717d2d6fd133cdf271b8949e9 Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Thu, 6 Aug 2026 16:19:46 -0500 Subject: [PATCH 3/4] bug: proper extension added to filename excluded from slugify --- internal/note/note.go | 1 + internal/note/note_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/internal/note/note.go b/internal/note/note.go index 2dce7ac..b9be78d 100644 --- a/internal/note/note.go +++ b/internal/note/note.go @@ -258,6 +258,7 @@ func IngestFile(cfg *config.Config, srcPath, filename, synopsis, source, targetC func slugify(s string) string { s = strings.ToLower(strings.TrimSpace(s)) + s, _ = strings.CutSuffix(s, ".md") var b strings.Builder lastDash := false for _, r := range s { diff --git a/internal/note/note_test.go b/internal/note/note_test.go index 8abe4d2..bfd4906 100644 --- a/internal/note/note_test.go +++ b/internal/note/note_test.go @@ -468,3 +468,30 @@ func TestAddNote(t *testing.T) { } }) } + +func TestSlugify(t *testing.T) { + tests := []struct { + name string + in string + want string + }{ + {"plain", "Book Todo Legacy", "book-todo-legacy"}, + {"already slug", "book-todo-legacy", "book-todo-legacy"}, + {"with markdown extension", "book-todo-legacy.md", "book-todo-legacy"}, + {"with uppercase markdown extension", "book-todo-legacy.MD", "book-todo-legacy"}, + {"mixed case extension", "book-todo-legacy.Md", "book-todo-legacy"}, + {"with date", "Book Todo Legacy 2026-08-06", "book-todo-legacy-2026-08-06"}, + {"with date and extension", "Book Todo Legacy 2026-08-06.md", "book-todo-legacy-2026-08-06"}, + {"trailing spaces", " book-todo-legacy.md ", "book-todo-legacy"}, + {"empty becomes empty", "", ""}, + {"only extension", ".md", ""}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := slugify(tt.in) + if got != tt.want { + t.Errorf("slugify(%q) = %q, want %q", tt.in, got, tt.want) + } + }) + } +} From 6c1ca433c392da25f55fed100a3e2cf9992244e7 Mon Sep 17 00:00:00 2001 From: Aaron Martell Date: Thu, 6 Aug 2026 21:57:21 -0500 Subject: [PATCH 4/4] streamlining README --- README.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/README.md b/README.md index 728fa3e..f91f2df 100644 --- a/README.md +++ b/README.md @@ -246,15 +246,6 @@ default_category = "inbox" Categories are fully configurable: name, storage path, and TUI hotkey. Add or remove categories to match your workflow. `default_category` is where `park new` lands notes when `-c` is omitted. -## Design - -Core decisions that shape the project: - -- **Composable architecture.** The six `internal/*` packages (`config`, `note`, `store`, `render`, `model`, `theme`, `fs`) are self-contained with minimal coupling. Importing `internal/model` into another CLI is just wiring commands to exported functions — `cmd/park` exists solely as a reference consumer. -- **No YAML dependency for frontmatter.** Parsed line-by-line as flat `key: value` pairs. One less dependency, zero ambiguity about which YAML dialect. -- **Atomic reclassification.** Frontmatter is rewritten before the file is moved. A failed rename doesn't corrupt state. -- **Synopsis-first triage.** The frontmatter `synopsis` field is the entire inbox UX. No metadata-surfing required. - ## Tech stack | component | library |