Documentation
¶
Overview ¶
Package ticket implements the on-disk ticket format and store.
The file format IS the API: tickets are hand-edited and read in git diffs, so writing one field must leave every other byte of the file untouched. That constraint rules out the obvious implementation (unmarshal into a struct, mutate, marshal back), and it also rules out re-printing a YAML AST — both silently renormalize quoting, key order, and comment alignment on fields the caller never asked to change.
Instead, writes use the parser purely as a locator: it yields the line and column of the value being replaced, and the replacement is spliced into the original bytes. Everything outside the spliced span is preserved by construction rather than by the serializer's good behaviour.
Index ¶
- Constants
- Variables
- func AddItem(body string, sec Section, text string) (string, error)
- func DoDHeadings() []string
- func Filename(id, title string) string
- func FormatTime(t time.Time) string
- func Handle(id string) string
- func HeadingTitle(body string) string
- func IDFromFilename(name string) string
- func NewBody(title, dod string, options []BodyOption) string
- func NewID(now time.Time) string
- func NormalizeIDPrefix(p string) string
- func OptionHeadings() []string
- func PlanHeadings() []string
- func SetItemProof(body string, sec Section, i int, proof string) (string, error)
- func SetItemState(body string, sec Section, i int, st State) (string, error)
- func SetReady(d *Doc, ready bool) error
- func Slug(title string) string
- func Touch(d *Doc, now time.Time) error
- func ValidID(id string) bool
- type BodyOption
- type DoDItem
- type Doc
- func (d *Doc) Body() string
- func (d *Doc) Bytes() []byte
- func (d *Doc) Frontmatter() string
- func (d *Doc) Has(key string) bool
- func (d *Doc) Keys() []string
- func (d *Doc) List(key string) ([]string, error)
- func (d *Doc) Scalar(key string) (string, bool, error)
- func (d *Doc) SetBody(body string)
- func (d *Doc) SetList(key string, items []string) error
- func (d *Doc) SetRaw(key, literal string) error
- func (d *Doc) SetScalar(key, val string) error
- func (d *Doc) String() string
- func (d *Doc) Validate() error
- type Outcome
- type PartialError
- type Section
- type State
- type Store
- func (s *Store) Archive(id string) (string, error)
- func (s *Store) ArchiveDir() string
- func (s *Store) Create(fields map[string]string, lists map[string][]string, body string) (*Ticket, error)
- func (s *Store) DuplicateIDs() []string
- func (s *Store) Init() (created bool, err error)
- func (s *Store) List() ([]*Ticket, error)
- func (s *Store) Load(idOrPrefix string) (*Ticket, error)
- func (s *Store) Mutate(idOrPrefix string, fn func(*Ticket) error) (*Ticket, error)
- func (s *Store) Paths() ([]string, error)
- func (s *Store) Restore(name string) (string, error)
- func (s *Store) Save(t *Ticket) error
- func (s *Store) SessionsDir() string
- func (s *Store) SharedDir() string
- func (s *Store) StateDir() string
- func (s *Store) TicketsDir() string
- type Ticket
Constants ¶
const ( FieldID = "id" FieldTitle = "title" FieldStatus = "status" FieldReady = "ready" FieldCreator = "creator" FieldAssignee = "assignee" FieldGoal = "goal" FieldContext = "context" FieldDoD = "definition-of-done" FieldBlockedBy = "blocked-by" FieldCommits = "commits" FieldModelTier = "model-tier" FieldExecutedBy = "executed-by" FieldCreatedAt = "created-at" FieldUpdatedAt = "updated-at" FieldQuestion = "question" FieldClaimedBy = "claimed-by" FieldClaimedAt = "claimed-at" // The outcome is three flat keys rather than a nested mapping. Nesting would // force the writer to rewrite an indented block to change one field, and the // merge driver resolves per top-level key — flat keys keep both operations // single-line, which is the whole point of the format. FieldOutcomeWhat = "outcome-what" FieldOutcomeWhy = "outcome-why" FieldOutcomeResolves = "outcome-resolves" // Reserved for a future external tracker adapter. jaira never interprets it. FieldExternal = "external" // FieldFollows links a follow-up ticket back to the one whose review produced // it, so the trail from "this was not finished" to "this is the rest" is not // lost the moment the first ticket closes. FieldFollows = "follows" // FieldBlockedReason is why a ticket is parked in the blocked lane. blocked-by // covers the case where the blocker is another ticket; this covers everything // else — a vendor, a decision, an environment — which is the majority and // which the board otherwise records nowhere. A blocked ticket whose reason is // unrecorded is the one a board loses: it looks the same on day one and on // day ninety. FieldBlockedReason = "blocked-reason" // FieldReviewVerdict is the reviewer's conclusion, kept separate from // outcome-resolves. The implementer writes what it believes it achieved and // the reviewer judges the diff; storing both in one field destroys the pair // the sign-off view exists to show. FieldReviewVerdict = "review-verdict" // FieldReviewSummary is what the change actually does, read off the diff. // The verdict alone is a judgement with no account of what was judged; a // later reader needs the summary to know what shipped without re-reading // the diff themselves. FieldReviewSummary = "review-summary" // FieldReviewGaps is what the reviewer found missing or unconvincing. It // must be filled even when there is nothing to report — "none" is a valid // answer, an empty field is not — because an empty field must always mean // "nobody looked", never "nothing found". FieldReviewGaps = "review-gaps" )
Frontmatter field names. These are the API — they appear in every ticket file and in git diffs, so they are stable and referenced by name everywhere rather than being spelled inline.
const ( // DirName is the per-repo directory holding all jaira state. DirName = ".jaira" // TicketsSubdir holds one markdown file per ticket. TicketsSubdir = "tickets" // ArchiveSubdir holds tickets taken off the board. They are moved rather // than deleted: the whole point of the board is that you can still answer // what a task was for months later, and a deleted file on a private board // is not in git history to recover from. ArchiveSubdir = "archive" // LanesSubdir it is committed: publishing a lane is an opt-in act through // the lane settings screen, not a side effect of sharing the board. SharedSubdir = "shared" // SessionsSubdir and locksSubdir live under the user's home directory rather // than inside the repository. tickets/, archive/ and shared/ are the parts of // .jaira/ meant to be committed; lanes/ (see core/lane.ProjectLanesDir) is // this machine's own scoping and stays gitignored even on a shared board // (see core/board.LanesIgnoreLine); sessions and locks stay out of the // repository entirely. SessionsSubdir = "sessions" )
Variables ¶
var ( // ErrNoFrontmatter means the file does not open with a YAML frontmatter block. ErrNoFrontmatter = errors.New("ticket: file does not begin with '---'") // ErrUnterminated means the frontmatter block is never closed. ErrUnterminated = errors.New("ticket: frontmatter block is not terminated by '---'") // ErrUnsafeYAML means the frontmatter uses constructs this tool cannot // rewrite without risking corruption. Reported rather than mangled. ErrUnsafeYAML = errors.New("ticket: frontmatter uses YAML features jaira cannot safely rewrite") )
var ( // ErrNotFound means no ticket matched. ErrNotFound = errors.New("ticket: not found") // ErrAmbiguous means an ID prefix matched more than one ticket. ErrAmbiguous = errors.New("ticket: ambiguous id prefix") // ErrNoStore means no .jaira directory was found. ErrNoStore = errors.New("ticket: no .jaira directory found; run 'jaira init'") )
var SuppliedFields = []string{ FieldTitle, FieldGoal, FieldContext, FieldDoD, FieldAssignee, "diff", }
SuppliedFields names the lane input-requires values a ticket already carries — or that the tool assembling a lane's bounded input derives itself — from the moment of creation, independent of any lane's output-produces. "diff" is not a frontmatter scalar (it is assembled from git commits at request time), so it is named here rather than with a Field constant.
"plan" is deliberately not in this set, even though the Plan heading is seeded on every new ticket: an empty Plan checklist does not satisfy a lane's input-requires (see flow.go's showForLane), so a lane requiring "plan" genuinely depends on an earlier lane — pre-process — having produced one. Exempting it here would make it impossible for the load-order check below to ever catch a lane misordered relative to its own producer, which is the one case this check exists for.
core/lane's load-time contract check compares a lane's input-requires against this set plus every earlier lane's output-produces, so this is the one place both packages read rather than two lists that could drift apart.
Functions ¶
func AddItem ¶
AddItem appends a checklist item to a section, creating the section if the ticket has no such heading.
This exists because there was no way to write a plan at all: frontmatter is set with SetScalar and the body was only editable by hand, so the lane whose entire output is a Plan checklist had no means to produce one. Appending to the end of the file is not the same thing — a checkbox only counts inside its heading, and the end of the file is usually some other section.
func DoDHeadings ¶
func DoDHeadings() []string
DoDHeadings and PlanHeadings expose the recognised section names so a renderer can tell which parts of a body it has already displayed as a checklist.
func FormatTime ¶
FormatTime renders a timestamp the way tickets store it.
func Handle ¶
Handle is the short reference shown to people and printed by the CLI.
It is taken from the end of the ULID rather than the beginning, because the leading ten characters encode only the millisecond timestamp: tickets created in the same burst share them entirely. The trailing characters are the random component, so a tail is distinctive where a head is not.
func HeadingTitle ¶
HeadingTitle returns the first level-one heading of a markdown body.
func IDFromFilename ¶
IDFromFilename recovers the identifier from a ticket filename.
func NewBody ¶
func NewBody(title, dod string, options []BodyOption) string
NewBody is the starting shape of a ticket's markdown, shared by the CLI and the TUI so a ticket created either way looks the same.
options is resolved by the caller (core/ticket must not import core/lane, which itself imports core/ticket) from the installed lanes' requires-option fields, with a per-user default board deciding which ones start ticked. Passing nil options reproduces today's two-option shape with nothing ticked — the regression baseline this function must not silently drift from.
func NewID ¶
NewID mints a ticket identifier.
ULIDs are used rather than sequential numbers (JAIRA-1, JAIRA-2, ...) because a sequential scheme needs a coordinator to hand out the next value. In a store with no server, that coordinator would be a counter file — the single line in the repository that every ticket creation touches, and therefore the worst possible merge-conflict hotspot. A ULID needs no coordinator, and it sorts lexicographically by creation time, so the tickets directory lists in order.
func NormalizeIDPrefix ¶
NormalizeIDPrefix upper-cases a user-supplied prefix, since ULIDs are encoded in uppercase Crockford base32 but nobody types them that way.
func OptionHeadings ¶
func OptionHeadings() []string
func PlanHeadings ¶
func PlanHeadings() []string
func SetItemProof ¶
SetItemProof attaches evidence to the i-th checkbox (0-based) of one section, replacing any proof line already there, and returns the new body.
The proof rides directly beneath its item rather than living in its own section: that is what lets it survive SetItemState and AddItem acting on other items, since neither touches a line it does not address by index. The item's own line is left byte-identical, the same way SetItemState leaves it — the point of a one-character or one-line diff applies here too.
func SetItemState ¶
SetItemState rewrites the marker of the i-th checkbox (0-based) of one section and returns the new body.
Only the single marker character is replaced. The item's text, the surrounding prose, and every line ending are left exactly as they were, so a tick produces a one-character diff rather than a reflowed body — which is what keeps the file readable in a diff and cheap to merge.
Marking an item as in progress clears any other in-progress item in the same section: the marker answers "which one is being worked on", and that question has one answer.
func SetReady ¶
SetReady records whether the promotion gate is currently satisfied. It is a derived convenience for rendering, never the authority: the gate is always recomputed before a move.
func Slug ¶
Slug renders a title into a filename-safe breadcrumb. It is a static hint for humans running ls or grep, deliberately not kept in sync with later title edits: renaming files on every title change would churn git history for no benefit. The frontmatter title is the only field callers should trust.
Types ¶
type BodyOption ¶
BodyOption is one entry in a new ticket's Options checklist.
type DoDItem ¶
type DoDItem struct {
Text string
State State
// Proof is the evidence recorded for this item, written by SetItemProof and
// read back off the plain indented line directly beneath the item's own —
// never its own checkbox, since a checkbox there would be misread as
// another criterion to satisfy rather than support for this one.
Proof string
}
DoDItem is one checkbox of a checklist, in either the Plan or the definition of done.
func ParseDoDItems ¶
ParseDoDItems extracts the checkboxes under a Definition of Done heading.
Only that section is read: checkboxes elsewhere in a ticket (an open-questions list, say) are not acceptance criteria and must not be mistaken for them.
func ParseOptions ¶
ParseOptions extracts the checkboxes under an Options heading.
func ParsePlanItems ¶
ParsePlanItems extracts the checkboxes under a Plan heading.
The Plan is how the work is being done — write the spec, design it, implement it — and carries the in-progress marker. It deliberately does not gate the terminal lane: following a method is not the same as having met the criteria.
type Doc ¶
type Doc struct {
// contains filtered or unexported fields
}
Doc is a ticket file split into the three zones that get different treatment: the opening delimiter, the frontmatter body (the only part YAML ever sees), and the markdown body (never parsed, only carried).
func NewDoc ¶
NewDoc builds a fresh ticket document with fields in canonical order. Only non-empty fields are written, so a new ticket file stays readable rather than being padded with empty keys.
func (*Doc) Frontmatter ¶
Frontmatter returns the raw frontmatter zone, for callers that want to unmarshal it normally. Reads do not need byte fidelity, so unmarshalling here is safe; only writes must go through the splice path.
func (*Doc) SetList ¶
SetList sets a top-level list field. The whole block for that key is replaced (it necessarily spans multiple lines), but no other key is disturbed. A trailing comment on the key line is carried over.
func (*Doc) SetRaw ¶
SetRaw sets a field to a literal YAML scalar without quoting it. Used for values that are unambiguous by construction — booleans and RFC3339 timestamps — so tickets read idiomatically instead of carrying `ready: "false"`. Callers are responsible for passing something valid; anything user-supplied should go through SetScalar, which quotes defensively.
func (*Doc) SetScalar ¶
SetScalar sets a top-level scalar field. An existing value is spliced in place, preserving the line's indentation, quoting style and trailing comment. A missing key is appended at the end of the frontmatter.
func (*Doc) Validate ¶
Validate rejects frontmatter containing constructs the splice writer cannot safely round-trip. Anchors and aliases are the realistic hazard: a hand-edited ticket using them could be silently restructured by a naive rewrite, so the tool refuses to touch such a file rather than risking corruption (STORE-10).
type Outcome ¶
Outcome is the three-part close-out: what changed, why it was needed, and the causal link back to the Definition of Done. The third field exists because "what changed" alone does not let a reviewer judge whether the work is done.
type PartialError ¶
type PartialError struct{ Problems []string }
PartialError reports tickets that could not be read while others succeeded.
func (*PartialError) Error ¶
func (e *PartialError) Error() string
type Section ¶
type Section int
Section names one of the two checklists a ticket can carry.
const ( // SectionDoD is the definition of done: the criteria that decide whether // the work is acceptable. Only this one gates the terminal lane. SectionDoD Section = iota // SectionPlan is the method being followed to get there. SectionPlan // SectionOptions names the steps this ticket does and does not need. SectionOptions )
type State ¶
type State int
State is how far one checklist item has got.
const ( // StateTodo is an item not started, or one whose marker is not recognised. // An unknown marker is read as unfinished rather than skipped: dropping it // would shorten the list and make the checklist easier to complete than the // author wrote it. StateTodo State = iota // StateDoing is the item currently being worked on, written as "[~]". It is // progress, not a completion claim, so it does not satisfy the criterion. StateDoing // StateDone is a finished item, written as "[x]". StateDone )
type Store ¶
type Store struct {
// Root is the directory containing .jaira.
Root string
// contains filtered or unexported fields
}
Store is the ticket directory for one repository.
func (*Store) Archive ¶
Archive moves a ticket out of the board, returning its new path.
The file is moved, never removed. Restoring is moving it back, which is why this returns the destination rather than swallowing it.
func (*Store) ArchiveDir ¶
ArchiveDir is where archived tickets live.
func (*Store) Create ¶
func (s *Store) Create(fields map[string]string, lists map[string][]string, body string) (*Ticket, error)
Create writes a new ticket file and returns it.
func (*Store) DuplicateIDs ¶
DuplicateIDs reports ids claimed by more than one file, discovered during the most recent lookup.
func (*Store) List ¶
List loads every ticket, reading only as much of each file as the frontmatter requires.
func (*Store) Mutate ¶
Mutate applies fn to a ticket under an exclusive lock, then writes it back atomically. Read-modify-write is done inside the lock so two concurrent invocations cannot each read the same original and overwrite one another — having a single write path prevents schema drift but does nothing about interleaving, which is a distinct failure mode (STORE-07).
func (*Store) Paths ¶
Paths lists ticket files in deterministic order. ULID filenames sort lexicographically by creation time, so this is chronological for free.
func (*Store) Save ¶
Save writes a ticket that the caller already mutated, without taking a lock. Used by the merge driver, which runs single-threaded under git.
func (*Store) SessionsDir ¶
SessionsDir is where this working tree's session state lives, outside the repo.
func (*Store) SharedDir ¶
SharedDir is where lanes published to teammates live. Nothing creates this directory at init: an empty shared/ folder committed to every board that never publishes a lane would be its own kind of confusion.
func (*Store) StateDir ¶
StateDir is the per-working-tree directory this store's session and lock state lives under, exported so callers outside the package (the version stamp, in particular) can address it too.
func (*Store) TicketsDir ¶
type Ticket ¶
type Ticket struct {
ID string
Title string
Status string
Ready bool
Creator string
Assignee string
ExecutedBy string
Goal string
Context string
DoD string
BlockedBy []string
Commits []string
ModelTier string
Question string
ClaimedBy string
ClaimedAt time.Time
CreatedAt time.Time
UpdatedAt time.Time
// Follows is the ticket whose review produced this one.
Follows string
// BlockedReason is why the ticket is parked, when the blocker is not another
// ticket.
BlockedReason string
// ReviewVerdict is the reviewer's conclusion, distinct from the
// implementer's own account in Outcome.
ReviewVerdict string
// ReviewSummary is what the change does, in the reviewer's own words.
ReviewSummary string
// ReviewGaps is what the reviewer found missing; "none" is a valid value.
ReviewGaps string
Outcome Outcome
// DoDItems are the checkboxes under a "Definition of Done" heading in the
// body. A checklist is how people actually write acceptance criteria, and
// keeping it in the body means it stays readable and editable prose rather
// than being flattened into a single frontmatter string.
DoDItems []DoDItem
// PlanItems are the checkboxes under a "Plan" heading: the method being
// followed rather than the criteria for acceptance. They carry the
// in-progress marker, which is what makes "designed but not yet built"
// visible instead of collapsing into a single in-progress lane.
PlanItems []DoDItem
// Options are the checkboxes under an "Options" heading, each naming a step
// this ticket does or does not need. A lane can require one, which is how a
// ticket skips planning or review without those lanes being removed.
Options []DoDItem
// Body is the markdown following the frontmatter.
Body string
// Path is where this ticket was loaded from.
Path string
// contains filtered or unexported fields
}
Ticket is the decoded view of a ticket, used for reads, filtering and rendering. Writes never go through this struct — they go through Doc, so that unknown fields survive. Anything read into here is a projection, not the source of truth.
func (*Ticket) DoDComplete ¶
DoDComplete reports whether every checklist item is finished.
An item in progress counts as remaining. It is a statement that work is under way, which is the opposite of the criterion being met, so treating it as anything else would let a ticket close with its own checklist saying otherwise.
func (*Ticket) HasDoD ¶
HasDoD reports whether a ticket declares a definition of done at all, in either supported form.
func (*Ticket) OptionSet ¶
OptionSet reports whether a named option is ticked on this ticket.
Matching is on the text, case-insensitively and ignoring surrounding space, so "- [x] planning" and "- [x] Planning " both select the planning step. An option that is absent is not selected: a step is opt-in, so a ticket that says nothing about planning does not get planned.
func (*Ticket) PlanComplete ¶
PlanComplete reports whether every step of the method has been carried out.
A ticket with no plan is vacuously complete: the plan is optional, and its absence is not evidence of unfinished work.