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 SetItemText(body string, sec Section, i int, text string) (string, error)
- func SetReady(d *Doc, ready bool) error
- func Slug(title string) string
- func Touch(d *Doc, now time.Time) error
- func TouchBy(d *Doc, actor string) error
- func ValidID(id string) bool
- func WriteAtomic(path string, data []byte) error
- 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) Delete(id string) (string, error)
- func (s *Store) DuplicateIDs() []string
- func (s *Store) FileLane(lane, folder string, prepare func(*Ticket) error) ([]Trimmed, error)
- 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) Lock(name string) (func(), error)
- func (s *Store) Logbook(id, folder string) (string, error)
- func (s *Store) LogbookDir() string
- func (s *Store) LoggedPerDay(now time.Time, days int) []int
- func (s *Store) Mutate(idOrPrefix string, fn func(*Ticket) error) (*Ticket, error)
- func (s *Store) Overflow(lane string, keep int, newest string) ([]*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) StampCommits(t *Ticket, derive func(*Ticket) []string) ([]string, error)
- func (s *Store) StateDir() string
- func (s *Store) TicketsDir() string
- func (s *Store) TrimLane(lane string, keep int, folder, newest string) ([]Trimmed, error)
- type Ticket
- type Trimmed
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" // FieldUpdatedBy is who wrote the most recent change, beside when it // happened. Several people and several agent sessions write the same store, // and updated-at alone says a ticket moved under you without saying who // moved it — which is the thing you actually need before touching it. FieldUpdatedBy = "updated-by" 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" // FieldReviewCheck is how to check the change by hand: run this, then that, // then look at this. Every other review field is an account of what // happened; this one is the only thing the reader can act on, and without it // signing off means either trusting the account or reconstructing the steps // from the diff. Written for someone who knows none of what the reviewer // knows — see the writing register the create and note helps carry. FieldReviewCheck = "review-check" // FieldTags is a plain list of topic labels — "ui", "backend" — so a // backlog can be read one subject at a time instead of one lane at a time. // A list rather than a comma-joined scalar because that is what every other // multi-valued field here is, which is what makes 'jaira set tags=a,b' and // the merge driver's union-instead-of-pick behaviour come for free. // // Colours are deliberately not here: they live once per board in .jaira/tags // (core/tag), because a colour is a fact about the tag rather than about the // ticket wearing it. FieldTags = "tags" )
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" // LogbookSubdir holds tickets whose work is finished and whose commit list // is final, grouped into dated folders by who took them off the board. A // folder under here is a readable record of one person's sweep — unlike // ArchiveSubdir, every ticket that lands here has already had its commits // stamped, because leaving the board is the point at which every commit // is finally known. LogbookSubdir = "logbook" // 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", "notes", }
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 SetItemText ¶ added in v0.1.1
SetItemText rewrites the words of the i-th checkbox (0-based) of one section, leaving its marker and its proof line exactly as they were.
Without this there was no way to reword a criterion at all: the only route was to tick the stale one with the proof "obsolete, replaced by item 6" and append a new one, which leaves ticked items that were never met. Rewording is the honest form of that edit; SetItemState with StateSuperseded is the other.
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.
func Touch ¶
Touch stamps updated-at. Every mutation path calls this so recency-based conflict resolution has something to work with.
func TouchBy ¶ added in v0.1.1
TouchBy records who made a change, beside when Touch records that it happened. An empty actor writes nothing: a caller that does not know who it is must not claim the change was nobody's.
func ValidID ¶
ValidID reports whether id is a well-formed ULID.
Every reference between tickets — dependencies, follow-ups, the handle shown on a card — resolves through the id, so an id that does not parse makes the ticket unreachable rather than merely untidy.
func WriteAtomic ¶ added in v0.1.1
WriteAtomic writes via a temporary file in the same directory and renames it into place, so a crash or a full disk leaves the previous file intact rather than a truncated one (STORE-06).
Exported because the ticket files are not the only thing in .jaira that two sessions write at once: the tag registry (core/tag) needs the same guarantee, and a second copy of this dance is a second place for it to be subtly wrong.
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.
func (DoDItem) Checked ¶
Checked reports whether this item is finished. Only StateDone counts: an item in progress is outstanding work, and a superseded one was never done at all.
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 // StateSuperseded is an item that did not happen and will not, written as // "[-]". It is not a quieter [x]: the work was never done, so nothing may // report it as achieved. It exists because the only way to retire a // criterion used to be ticking it with the proof "obsolete, replaced by item // 6" — which leaves ticked items that were never met, and a tick that can // mean "never happened" means nothing anywhere on the board. StateSuperseded )
type Store ¶
type Store struct {
// Root is the directory containing .jaira.
Root string
// Actor is who this process writes as, recorded on every mutation as
// updated-by. It is a field rather than a package-level value because a
// process can hold two stores at once — the board switcher does — and it is
// set by the caller that already knows the identity, so core/ticket does
// not have to depend on core/identity. Empty records nothing.
Actor 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) Delete ¶ added in v0.1.1
Delete removes a ticket's file and returns the path it was at.
The only irreversible operation in the store, and it exists because archiving is the wrong answer for a ticket that should never have been written: a mistyped create or a throwaway probe leaves a file that otherwise only 'rm' removes, and 'rm' means the caller has to know the file layout. Whether the caller is sure is decided above this line — the CLI asks for the handle typed back, and the board asks for it again.
Symlinks are resolved for the same reason Archive resolves them: deleting the link alone would leave the ticket on disk but off the board. Both go, since a half-deleted ticket is worse than either outcome.
func (*Store) DuplicateIDs ¶
DuplicateIDs reports ids claimed by more than one file, discovered during the most recent lookup.
func (*Store) FileLane ¶ added in v0.1.1
FileLane empties a lane into the given logbook folder, oldest first — the lane is a doorway (logbook-on-entry), so the just-landed ticket goes along with anything still sitting there from before the doorway existed. prepare runs on each ticket before its file moves (the callers stamp commits there); nil skips it. A ticket that cannot be filed — unreadable, a stamp failure, a name collision in the folder — is skipped and named rather than allowed to jam the doorway: a sweep aborts on nothing, because the same failure would repeat on every later landing and the lane would block for good. What moved and what was skipped are both always reported.
func (*Store) List ¶
List loads every ticket, reading only as much of each file as the frontmatter requires.
func (*Store) Lock ¶ added in v0.1.1
Lock takes the store's advisory lock under a caller-chosen name, for state in .jaira that is not one ticket.
The name shares one namespace with the per-ticket locks, which are keyed by id, so any name that is not a 26-character ULID is safe — "tags" is not one. A name that IS a ULID would silently take that ticket's lock, and nothing here checks for it: the callers are in this repository, not a plugin surface.
The caller must call the returned function, normally with defer. Failure to acquire within the timeout is an error, not a silent write.
func (*Store) Logbook ¶ added in v0.1.1
Logbook moves a ticket out of the board and into a dated logbook folder, returning its new path. Like Archive, the file is moved rather than deleted, and a name collision inside the destination folder is refused by name rather than silently overwritten — this only happens if the same ticket is logged into the same folder twice, which a caller should treat as a bug to look into, not a state to paper over.
func (*Store) LogbookDir ¶ added in v0.1.1
LogbookDir is where tickets taken off the board with their commits stamped live, grouped into dated per-person folders.
func (*Store) LoggedPerDay ¶ added in v0.1.1
LoggedPerDay counts the logbook's tickets by the day of their folder, for the days days ending today: out[days-1] is today, out[0] the oldest. The folder name carries the date — <initials>-<yyyymmdd> — so no ticket is read, and a folder whose name does not end in a date is skipped. A folder under the logbook's old name counts too, the same way Restore finds it.
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) Overflow ¶ added in v0.1.1
Overflow returns the tickets of a lane beyond the newest keep, oldest first. "Newest" is measured by updated-at: a ticket in a terminal lane is normally never touched again, so updated-at is its acceptance time. newest names the ticket whose arrival is being accounted for — it is pinned to the top regardless of its stamp, so the ticket that triggered a trim is never the one trimmed. Ties in updated-at are routine (the stamp has second resolution) and break toward the newer ULID: among tickets touched in the same second, the most recently created counts as newer. keep <= 0 means the lane has no cap, so nothing ever overflows. A board with unreadable tickets refuses to answer rather than guess at which ticket is oldest.
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) Restore ¶
Restore moves an archived or logged ticket back onto the board, resolving name from the bare filename in either case — never from a caller-supplied path, so a name built to escape the store (e.g. "../../etc/passwd") cannot walk it anywhere but back into TicketsDir.
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) StampCommits ¶ added in v0.1.1
StampCommits writes the derived commit union onto the ticket and returns what was written. Derived shas come first, in git order; any sha already recorded that the derivation did not find is appended rather than dropped — a sha a person wrote down deliberately is evidence this tool has no business discarding. derive may be nil, the same "no derivation on offer" convention core/gate uses.
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 ¶
func (*Store) TrimLane ¶ added in v0.1.1
TrimLane enforces a lane's cap after something new landed in it: the tickets beyond the newest keep leave for the given logbook folder, oldest first. newest is passed through to Overflow so the just-landed ticket is never trimmed. It returns what moved — including on error, so a caller can still report the tickets that had already left before the failure.
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
// UpdatedBy is who made that most recent change. Empty on a ticket last
// written by a version that did not record it, which reads as "unknown"
// rather than as anybody.
UpdatedBy string
// Tags are the ticket's topic labels, normalized to lowercase kebab by the
// write path. Read here as written: a hand-edited file may carry anything,
// and a reader that silently rewrote it would hide the fact rather than fix
// it.
Tags []string
// 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
// ReviewCheck is the steps a person follows to check the change themselves.
ReviewCheck 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.
type Trimmed ¶ added in v0.1.1
type Trimmed struct {
ID string
Title string
// Path is where the file now lives, under the logbook.
Path string
}
Trimmed records one ticket a lane cap moved off the board, so the caller can say what happened — the cap is allowed to move files only because it never does so silently.