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 ¶
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.
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 ¶
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.
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 ¶
DirName returns the canonical directory name for a card, "<padded-id>-<slug>". slug is expected to be already-normalized.
func Marshal ¶
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 ¶
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 ¶
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 ¶
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":
- Lowercase.
- ASCII-only (non-ASCII characters are dropped, not transliterated; transliteration would require a heavy dep and the design doc doesn't ask for it).
- Replace runs of non-alphanumeric with single hyphens.
- Trim leading + trailing hyphens.
- Truncate to MaxSlugLen at a hyphen boundary if possible.
- 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 ¶
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 ¶
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.