Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,17 @@ Frontmatter is parsed line-by-line — no YAML dependency. Four fields:

`synopsis` is the key design decision: read it, decide whether to open the file, move on. `source` lets future-you reconstruct *why* a note exists without re-reading it.

### Schema contract

The frontmatter contract is published as a public, stdlib-only Go package at `github.com/polymorcodeus/park/schema` and exposed through the CLI:

```bash
park schema # human-readable contract
park schema --json # machine-readable contract for downstream tooling
```

The JSON output includes the canonical category enum, field kinds, the date format, and the exact write template. Import the package directly when building tools that write or validate park notes.

### Reclassification

`park reclassify <file> -c <category>` rewrites frontmatter *before* moving the file. A failed move never leaves a note in a half-updated state. Same-category moves are rejected.
Expand All @@ -109,6 +120,8 @@ Frontmatter is parsed line-by-line — no YAML dependency. Four fields:
| `show <file>` | glamour-render a note, no TUI |
| `reclassify <file> -c <cat>` | reclassify a note (alias `recat`) |
| `config` | print the default TOML config |
| `schema` | print the frontmatter schema contract |
| `schema --json` | print the contract as machine-readable JSON |

### `park new` options

Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v0.3.0
v0.4.0
18 changes: 18 additions & 0 deletions cmd/park/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ func Main() {
parkRoot, parkConfig string
reclassifyCategory string
newCategory string
schemaJSON bool
)

defaultRoot := os.Getenv("PARK_ROOT")
Expand Down Expand Up @@ -221,6 +222,23 @@ func Main() {
return nil
},
},
{
Name: "schema",
Usage: "print the frontmatter schema contract",
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "json",
Destination: &schemaJSON,
Usage: "output machine-readable JSON",
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
if err := schemaPark(schemaJSON, cmd.Root().Writer); err != nil {
return styledExit(err, 1)
}
return nil
},
},
{
Name: "show",
Usage: "render a note to the terminal",
Expand Down
47 changes: 47 additions & 0 deletions cmd/park/park.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package cmd

import (
"encoding/json"
"fmt"
"io"
"os"
"strings"

"github.com/polymorcodeus/park/schema"

tea "charm.land/bubbletea/v2"
"github.com/urfave/cli/v3"
Expand Down Expand Up @@ -104,6 +108,49 @@ func runNoteForm(cfg *config.Config, w io.Writer, seed *note.Draft) error {
return nil
}

// schemaPark prints the canonical frontmatter schema, either as JSON or as
// a short human-readable summary.
func schemaPark(asJSON bool, w io.Writer) error {
if asJSON {
data, err := json.MarshalIndent(schema.Describe(), "", " ")
if err != nil {
return fmt.Errorf("marshal schema: %w", err)
}
if _, err := fmt.Fprintln(w, string(data)); err != nil {
return fmt.Errorf("write schema output: %w", err)
}
return nil
}

s := schema.Describe()
var b strings.Builder
fmt.Fprintf(&b, "park frontmatter schema (version %d)\n\n", s.SchemaVersion)
fmt.Fprintln(&b, "Fields:")
for _, f := range s.Fields {
extra := ""
switch f.Kind {
case "enum":
extra = "values: " + strings.Join(f.Values, ", ")
case "date":
extra = "format: " + f.Format
if f.Auto {
extra += " (auto-stamped)"
}
}
if extra != "" {
fmt.Fprintf(&b, " %-8s required %-5s %s\n", f.Name, f.Kind, extra)
} else {
fmt.Fprintf(&b, " %-8s required %-5s\n", f.Name, f.Kind)
}
}
fmt.Fprintf(&b, "\nDate format: %s\n", s.DateFormat)
fmt.Fprintf(&b, "Write template:\n%s", s.WriteTemplate)
if _, err := fmt.Fprint(w, b.String()); err != nil {
return fmt.Errorf("write schema output: %w", err)
}
return nil
}

func assistPark(cfg *config.Config, w io.Writer) error {
m, err := model.NewAssistModel(cfg)
if err != nil {
Expand Down
11 changes: 6 additions & 5 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/BurntSushi/toml"
"github.com/polymorcodeus/park/internal/fs"
"github.com/polymorcodeus/park/schema"
)

// Config is the top-level configuration for park.
Expand Down Expand Up @@ -189,12 +190,12 @@ func DefaultConfig(root string) *Config {
root = DefaultRootPath()
}
return &Config{
DefaultCategory: "inbox",
DefaultCategory: string(schema.CategoryInbox),
Categories: []Category{
{Name: "inbox", Path: filepath.Join(root, "_inbox"), Key: "i"},
{Name: "projects", Path: filepath.Join(root, "_projects"), Key: "p"},
{Name: "areas", Path: filepath.Join(root, "_areas"), Key: "a"},
{Name: "archive", Path: filepath.Join(root, "_archive"), Key: "x"},
{Name: string(schema.CategoryInbox), Path: filepath.Join(root, "_inbox"), Key: "i"},
{Name: string(schema.CategoryProjects), Path: filepath.Join(root, "_projects"), Key: "p"},
{Name: string(schema.CategoryAreas), Path: filepath.Join(root, "_areas"), Key: "a"},
{Name: string(schema.CategoryArchive), Path: filepath.Join(root, "_archive"), Key: "x"},
},
}
}
3 changes: 2 additions & 1 deletion internal/model/assist.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
tea "charm.land/bubbletea/v2"
"github.com/polymorcodeus/park/internal/config"
"github.com/polymorcodeus/park/internal/store"
"github.com/polymorcodeus/park/schema"
)

// listItem adapts an Item to bubbles/list's list.Item interface.
Expand All @@ -19,7 +20,7 @@ type listItem struct {
}

func (i listItem) Title() string {
created, _ := time.Parse("2006-01-02", i.item.Created)
created, _ := time.Parse(schema.DateFormat, i.item.Created)
if created.IsZero() {
created = i.item.ModTime
}
Expand Down
41 changes: 7 additions & 34 deletions internal/note/note.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,38 +12,12 @@ import (

"github.com/polymorcodeus/park/internal/config"
"github.com/polymorcodeus/park/internal/fs"
"github.com/polymorcodeus/park/schema"
)

// Metadata is the set of fields persisted as frontmatter in every note.
type Metadata struct {
Category string
Created string
Source string
Synopsis string
}

// IsComplete reports whether all metadata fields are populated.
func (m Metadata) IsComplete() bool {
return fieldSet(m.Category) && fieldSet(m.Created) && fieldSet(m.Source) && fieldSet(m.Synopsis)
}

// MissingFields returns the metadata fields that are empty.
func (m Metadata) MissingFields() []string {
var missing []string
if !fieldSet(m.Category) {
missing = append(missing, "category")
}
if !fieldSet(m.Created) {
missing = append(missing, "created")
}
if !fieldSet(m.Source) {
missing = append(missing, "source")
}
if !fieldSet(m.Synopsis) {
missing = append(missing, "synopsis")
}
return missing
}
// Metadata is the shared metadata block for parked notes. It is an alias
// so the canonical contract lives in one place: the public schema package.
type Metadata = schema.Frontmatter

// Note is the persisted representation of a parked note. Path is empty when
// the note is parsed from a string rather than read from a file.
Expand Down Expand Up @@ -242,8 +216,7 @@ func Write(path string, n Note) (err error) {
}
}()

if _, err = fmt.Fprintf(f, "---\ncategory: %s\ncreated: %s\nsource: %s\nsynopsis: %s\n---\n\n%s\n",
n.Category, n.Created, n.Source, n.Synopsis, n.Body); err != nil {
if _, err = fmt.Fprint(f, schema.Render(n.Metadata, n.Body)); err != nil {
return fmt.Errorf("write temp %q: %w", tmpPath, err)
}

Expand All @@ -255,7 +228,7 @@ func Write(path string, n Note) (err error) {

// Today returns the current date in the frontmatter's date format.
func Today() string {
return time.Now().Format("2006-01-02")
return time.Now().Format(schema.DateFormat)
}

// Result is the outcome of attempting to add a note headlessly.
Expand Down Expand Up @@ -393,7 +366,7 @@ func Create(cfg *config.Config, d Draft) (string, error) {
n := Note{
Path: path,
Body: d.Body,
Metadata: Metadata{
Metadata: schema.Frontmatter{
Category: d.Category,
Created: Today(),
Source: d.Source,
Expand Down
121 changes: 121 additions & 0 deletions schema/schema.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Package schema describes the canonical frontmatter contract for parked
// notes. It is a pure leaf package: no internal dependencies, stdlib only, so
// downstream tooling can import the contract instead of re-deriving it.
package schema

import (
"strings"
)

// SchemaVersion is the current version of the frontmatter contract.
// It increments on any breaking change to the contract.
const SchemaVersion = 1

// DateFormat is the canonical date layout used for the `created` field.
const DateFormat = "2006-01-02"

// Category is a canonical parked-note category. The set below is the
// contract's enum and is independent of any loaded TOML config.
type Category string

// Canonical categories.
const (
CategoryInbox Category = "inbox"
CategoryProjects Category = "projects"
CategoryAreas Category = "areas"
CategoryArchive Category = "archive"
)

// Categories returns the canonical category values in order.
func Categories() []string {
return []string{
string(CategoryInbox),
string(CategoryProjects),
string(CategoryAreas),
string(CategoryArchive),
}
}

// Frontmatter is the set of fields persisted as frontmatter in every note.
type Frontmatter struct {
Category string
Created string
Source string
Synopsis string
}

// IsComplete reports whether all frontmatter fields are populated.
func (f Frontmatter) IsComplete() bool {
return fieldSet(f.Category) && fieldSet(f.Created) && fieldSet(f.Source) && fieldSet(f.Synopsis)
}

// MissingFields returns the frontmatter fields that are empty.
func (f Frontmatter) MissingFields() []string {
var missing []string
if !fieldSet(f.Category) {
missing = append(missing, "category")
}
if !fieldSet(f.Created) {
missing = append(missing, "created")
}
if !fieldSet(f.Source) {
missing = append(missing, "source")
}
if !fieldSet(f.Synopsis) {
missing = append(missing, "synopsis")
}
return missing
}

// WriteTemplate is the canonical frontmatter+body layout. Placeholders are
// {category}, {created}, {source}, {synopsis}, and {body}.
const WriteTemplate = "---\ncategory: {category}\ncreated: {created}\nsource: {source}\nsynopsis: {synopsis}\n---\n\n{body}\n"

// Render returns the frontmatter block and body with the given values
// substituted into WriteTemplate.
func Render(f Frontmatter, body string) string {
return strings.NewReplacer(
"{category}", f.Category,
"{created}", f.Created,
"{source}", f.Source,
"{synopsis}", f.Synopsis,
"{body}", body,
).Replace(WriteTemplate)
}

func fieldSet(s string) bool {
return strings.TrimSpace(s) != ""
}

// Field describes a single frontmatter field in the JSON schema export.
type Field struct {
Name string `json:"name"`
Required bool `json:"required"`
Kind string `json:"kind"`
Values []string `json:"values,omitempty"`
Format string `json:"format,omitempty"`
Auto bool `json:"auto,omitempty"`
}

// Spec is the JSON-serializable schema description.
type Spec struct {
SchemaVersion int `json:"schema_version"`
Fields []Field `json:"fields"`
DateFormat string `json:"date_format"`
WriteTemplate string `json:"write_template"`
}

// Describe returns the current schema contract as a serializable value.
func Describe() Spec {
return Spec{
SchemaVersion: SchemaVersion,
Fields: []Field{
{Name: "category", Required: true, Kind: "enum", Values: Categories()},
{Name: "created", Required: true, Kind: "date", Format: DateFormat, Auto: true},
{Name: "source", Required: true, Kind: "string"},
{Name: "synopsis", Required: true, Kind: "string"},
},
DateFormat: DateFormat,
WriteTemplate: WriteTemplate,
}
}
Loading
Loading