card

package
v0.1.3 Latest Latest
Warning

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

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

Documentation

Overview

Package card implements the card data model for focus.

A card on disk is a directory named "<padded-id>-<slug>/" containing at minimum an INDEX.md file with YAML frontmatter and a markdown body. This package parses, validates, and re-marshals cards while preserving unknown frontmatter fields per the schema rule in designs/focus-v2.md ("Unknown fields are preserved on read/write").

Index

Constants

View Source
const MaxSlugLen = 64

MaxSlugLen is the soft cap on slug length. Slugs longer than this after normalization get truncated at the nearest hyphen so we don't split words. 64 chars is plenty for a folder name and keeps `ls cards/` readable on an 80-column terminal.

View Source
const SchemaVersion = 2

SchemaVersion is the only frontmatter schema this binary speaks. The CLI refuses to operate on cards with a missing or unknown schema_version (designs/focus-v2.md §"Schema versioning").

Variables

View Source
var ErrEmptySlug = errors.New("title produced empty slug; pass --slug explicitly")

ErrEmptySlug is returned by Slugify when the title produces an empty slug (e.g. the title is emoji-only or all whitespace). Callers should error out and ask for an explicit --slug; auto-deriving a fallback like "card-142" silently would surprise users.

View Source
var ErrInvalidSlug = errors.New("invalid slug")

ErrInvalidSlug is returned by ValidateSlug when a custom --slug contains characters outside the safe set (path separators, whitespace, punctuation, etc).

Functions

func DirName

func DirName(id int, slug string) string

DirName returns the canonical directory name for a card, "<padded-id>-<slug>". slug is expected to be already-normalized.

func Marshal

func Marshal(c *Card) ([]byte, error)

Marshal serializes a Card back into INDEX.md bytes, preserving unknown fields from c.Extra. Field order in the frontmatter is stable: required fields first (in the order they appear in the design doc), then optional recognized fields, then Extra in alphabetical order. Stable order means git diffs stay readable when CLI writes touch a card.

func NormalizeBody

func NormalizeBody(body string) string

NormalizeBody rewrites a card body so paragraphs flow as single long lines. Authored line breaks within a paragraph become spaces; blank lines stay; structural markdown (code fences, lists, headings, blockquotes, indented code, horizontal rules, HTML-ish blocks) is preserved verbatim.

The function is idempotent: running it on already-normalized content is a no-op.

func PaddedID

func PaddedID(id int) string

PaddedID returns the 4-digit zero-padded form of the card id used in the directory name (e.g. 142 → "0142"). Lex-sort over directory names matches numeric sort up to id 9999, which is the upper bound of v2's design.

func Slugify

func Slugify(title string) (string, error)

Slugify converts a card title into the slug used in the card's directory name. The algorithm follows designs/focus-issue-001.md §"Slug rules at creation":

  1. Lowercase.
  2. ASCII-only (non-ASCII characters are dropped, not transliterated; transliteration would require a heavy dep and the design doc doesn't ask for it).
  3. Replace runs of non-alphanumeric with single hyphens.
  4. Trim leading + trailing hyphens.
  5. Truncate to MaxSlugLen at a hyphen boundary if possible.
  6. Empty result → ErrEmptySlug.

Slugs are folder-only — they're not stored in frontmatter and never renamed after creation (see designs/focus-id-strategy.md §"Slug").

func ValidateSlug

func ValidateSlug(slug string) error

ValidateSlug rejects user-supplied --slug values that would either escape the cards/ directory or break id-based directory lookup. FindCardDir globs `cards/<padded-id>-*` non-recursively, so any slug containing a path separator silently corrupts the layout.

Allowed: ASCII letters, digits, and "-" or "_". Anything else returns ErrInvalidSlug. Empty input is also rejected.

Types

type Card

type Card struct {
	// Required fields. The CLI rejects cards on disk that are missing
	// any of these.
	SchemaVersion int       `yaml:"schema_version"`
	ID            int       `yaml:"id"`
	UUID          string    `yaml:"uuid"`
	Title         string    `yaml:"title"`
	Type          Type      `yaml:"type"`
	Status        Status    `yaml:"status"`
	Priority      Priority  `yaml:"priority"`
	Project       string    `yaml:"project"`
	Created       time.Time `yaml:"created"`

	// Optional but recognized fields.
	Epic        *int     `yaml:"epic,omitempty"`
	DependsOn   []int    `yaml:"depends-on,omitempty"`
	Contract    []string `yaml:"contract,omitempty"`
	Tags        []string `yaml:"tags,omitempty"`
	Owner       string   `yaml:"owner,omitempty"`
	Description string   `yaml:"description,omitempty"`
	Area        string   `yaml:"area,omitempty"`

	// Extra holds any frontmatter keys this binary doesn't recognize.
	// Values come from yaml.v3 decode and are re-encoded on Marshal so
	// downstream extensions survive a CLI write round-trip.
	Extra map[string]any `yaml:"-"`

	// Body is the markdown content of the card after the closing "---"
	// of the frontmatter block. Stored verbatim including the trailing
	// newline (or lack thereof) so round-trips are byte-clean as long
	// as nothing in the frontmatter changes.
	Body string `yaml:"-"`
}

Card is the parsed, in-memory representation of a focus card.

The strongly-typed fields cover the required + optional set defined in the schema. Anything else found in the frontmatter is held in Extra and round-tripped on Marshal so users can extend frontmatter without forking the binary.

func Parse

func Parse(data []byte) (*Card, error)

Parse reads an INDEX.md file's bytes and returns a Card.

The frontmatter delimiter is the canonical "---\n...---\n" form. We don't accept TOML or JSON frontmatter — focus is YAML-only, and adrg/frontmatter's auto-detection is therefore overkill. We do the split ourselves to keep unknown-field handling under our control.

func (*Card) Validate

func (c *Card) Validate() error

Validate returns an error if any required field is missing, has the wrong shape, or carries an unknown schema_version. This is the gate run on every card load — see designs/focus-v2.md §"Required vs optional fields".

type Priority

type Priority string

Priority is the card's priority. p0 is highest.

const (
	PriorityP0 Priority = "p0"
	PriorityP1 Priority = "p1"
	PriorityP2 Priority = "p2"
	PriorityP3 Priority = "p3"
)

type Status

type Status string

Status is the card's lifecycle position. Four values, no more (designs/focus-v2.md §"Statuses").

const (
	StatusActive   Status = "active"
	StatusBacklog  Status = "backlog"
	StatusDone     Status = "done"
	StatusArchived Status = "archived"
)

type Type

type Type string

Type is the card's type. v2 has two: regular cards and epics.

const (
	TypeCard Type = "card"
	TypeEpic Type = "epic"
)

Jump to

Keyboard shortcuts

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