item

package
v0.9.3 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package item persists lifecycle items (proposals, tasks, bugs, research) as `##`-anchored blocks in the per-context work.md — ACTIVE items only. Rejected and archived items leave work.md; their summaries live in the journal. One item = one block keeps merge conflicts block-local and work.md bounded (anti file-sprawl: no per-item files, ever).

Index

Constants

View Source
const (
	StateDraft     = "draft"
	StateSubmitted = "submitted"
	StateApproved  = "approved"
	StateActive    = "active"
	StateDone      = "done"
	StateArchived  = "archived"
	StateRejected  = "rejected"

	// StateBlocked is a side state, not part of the six-state main order
	// (like rejected): an item lands here only via lifecycle.Escalate, when
	// a done->active reopen would exceed the configured feedback round
	// limit. It is deliberately excluded from lifecycle's stateOrder/
	// orderedStates — Move() refuses every transition into or out of it;
	// only lifecycle.ResolveBlocked (driven by the linked decision item's
	// outcome) can move an item out of blocked.
	StateBlocked = "blocked"
)

States of the lifecycle state machine (see internal/lifecycle).

Variables

View Source
var IDRe = regexp.MustCompile(`^` + idPat + `$`)

IDRe matches an item ID of either scheme: the legacy sequential form (P-0007, ADR-0007, and the legacy adr letter D-0007) and the ADR-0013 kind-prefixed record ID (T-01K9XJ...). Both are accepted permanently — see the pattern doc above for why legacy IDs can never be dropped.

View Source
var LegacyIDRe = regexp.MustCompile(`^` + legacyIDPat + `$`)

LegacyIDRe matches ONLY the pre-ADR-0013 sequential form. It exists to tell the two schemes apart — a migration deciding what still needs rewriting, a caller reporting which scheme a record predates. Nothing may use it to reject an ID: IDRe is the acceptance test, and it accepts both.

Functions

func CheckHeader added in v0.8.4

func CheckHeader(it Item) error

CheckHeader refuses an item whose header writeWork could not read back identically — the corruption B-01KYN3E973F20 recorded, where a newline in an ADR field swallowed every field after it into the body and lost the answer's status write with them.

Two different limits, for two different reasons:

Prose fields may span lines, because Parse rejoins a non-key line onto the field above it, but never a BLANK line: the blank line is exactly what ends the machine header, so writing one puts the remaining fields on the far side of the boundary. A trailing newline is the same defect one character shorter — it emits that blank line too — which is why the test is on v+"\n". A LEADING newline is fine and deliberately allowed: it writes an empty value line the parser reads as "" and then continues onto, which round trips.

Single-line fields may hold no newline at all. Title is not stored as a field: it shares the "## " heading line with the ID, so a newline there splits one item into two — the second with a heading Parse won't match, or worse, one it will.

func ExternalRef added in v0.2.0

func ExternalRef(r string) bool

ExternalRef reports whether a ref cites something outside the workspace by URL rather than an item inside it by ID.

This is the PREFERRED way to cite a tracker issue: structured, unambiguous, and carrying its own repository, instead of prose a parser has to guess at. The alternative — recognizing "GitHub issue 26" in a body — has a blast radius that is not worth it, since acting on a false positive closes a stranger's issue; a URL in the refs field cannot be mistaken for a rule ID, a record count or a version number.

Validation is deliberately shallow: the scheme is checked, the destination is not. Whether the far end exists is the forge's answer to give, and a citation that 404s is still a citation the record should keep.

func KindOf added in v0.2.0

func KindOf(id string) string

KindOf returns the kind an item ID names ("" if the ID is malformed). It reads the prefix, which both schemes share, so a legacy P-0007 and a record-ID P-01K9XJ... both answer "proposal" — callers deriving a kind from an ID must go through here rather than testing prefixes by hand, which is how the four-digit shape leaked into helpers in the first place.

func Letter

func Letter(kind string) string

Letter returns the ID letter for a kind ("" if unknown).

func MintID added in v0.2.0

func MintID(kind string) string

MintID mints a fresh ID for a kind, stamped with the current time.

Per ADR-0013 the number no longer comes from a counter. coord.NextID is serialized per machine but its database is gitignored, so two independent clones drafting concurrently minted the same ID and replay silently collapsed the two records into one (reproduced in R-0006). A UUIDv7 needs no coordination at all to be unique, and because it leads with a millisecond timestamp the canonical encoding also sorts by mint time. Returns "" for an unknown kind, matching Letter.

func MintIDAt added in v0.2.0

func MintIDAt(kind string, ts time.Time) string

MintIDAt mints an ID for a kind stamped with ts instead of the clock. A migration rewriting historical records mints from each record's own creation time so the archive keeps its chronology instead of collapsing into the migration moment; tests use it for deterministic same-millisecond batches.

func NormalizeHeaderValue added in v0.8.4

func NormalizeHeaderValue(v string) string

NormalizeHeaderValue coerces a prose value into something the header can represent: blank lines collapse to single newlines and trailing newlines are dropped, which is exactly what CheckHeader refuses.

Refusing is the right answer for a value a CALLER supplied — the caller can be told, and the record is not yet at stake. It is the wrong answer on a restore path, where the value was manufactured by this program (a truncation marker appended after a cut that landed on a newline) and there is no caller to report to. Refusing there does not protect the record, it strands it: a rejected item that cannot be revoked is unreachable, which is worse than a prose field whose blank line was closed up. So the two paths differ on purpose — CheckHeader guards arguments, this coerces derived values.

func Num

func Num(id string) int

Num extracts the counter of a LEGACY sequential item ID (P-0007 -> 7), handling both single-letter and multi-letter (ADR-0007) prefixes. It returns 0 for anything else, record IDs included: an ADR-0013 ID has no counter, and a record ID whose base32 tail happens to open with digits would otherwise yield a meaningless number that a caller could mistake for a real one.

func ParseOptions

func ParseOptions(body string) []string

ParseOptions extracts the fixed option set (if any) an adr item's Body records. Options are the rejected alternatives, not just the chosen Decision — this project's own record-keeping rules name them as knowledge worth keeping, so callers that carry an ADR onward (see internal/knowledge's Extract) need them, not just the Context/Decision/ Consequences/Status fields item.Item already exposes.

Three body shapes are understood, tried in this order:

  1. one `option: <text>` line per option, verbatim (no comma-splitting — an option's own text may contain commas). This is what NEW decisions write.
  2. the legacy `options: a, b, c` comma-joined line older decisions wrote — kept forever so existing items/journals stay answerable without migration (when an option's own text contains commas, the comma-split fragments it into more pieces than were intended, but that shattered form is exactly what remains answerable — it is not rewritten to option: lines).
  3. lifecycle.Escalate's `... outcome=a|b|c.` / `... choose=a|b|c.` sentence.

No match = free text (nil).

This is the SINGLE parser. It began as a copy of internal/mcpserver's unexported decideOptions, extracted so internal/knowledge could carry ADR options onto its portable Entry without importing internal/mcpserver; the follow-up this comment used to ask for — delete decideOptions and call this instead — is done, and it was not cosmetic. While both copies existed, a change to the body text they both parse landed in neither.

func Record

func Record(it Item) string

Record renders the dense `i` output line.

func Remove

func Remove(root workspace.Root, it Item) error

Remove deletes an item block from its context's work.md. Same whole-cycle locking as Upsert, and for the same reason.

func Statuses added in v0.8.2

func Statuses() []string

Statuses lists the accepted statuses in a stable order, so a refusal can print the enum without restating it and drifting from what is enforced.

func UnknownRefs

func UnknownRefs(selfID string, refs []string, known map[string]bool) []string

UnknownRefs validates a proposed reference set for item selfID against known, the set of currently loadable item IDs, and reports — in refs' input order — every entry that is unusable as a citation: malformed (does not match IDRe), self-referential (equal to selfID, always a mistake), or absent from known. An entry failing more than one check is reported once.

UnknownRefs is deliberately not called from Parse: a work.md may legitimately cite an item that has since been archived out of work.md, and a parser that refused to load such a file would make a dangling citation unrecoverable. Validation belongs at the write path, which should call UnknownRefs before persisting and reject or warn on a non-empty result.

func Upsert

func Upsert(root workspace.Root, it Item) error

Upsert writes an item block into its context's work.md (replacing an existing block with the same ID) and injects the schema frontmatter.

The whole read-modify-write (LoadWork through writeWork) runs under withWorkLock, not just the final write: two concurrent writers that both read work.md before either one wrote would still both compute their splice against the same stale slice and the later write would erase the earlier one's record, lock or no lock on writeWork alone (measured: B-01KYD5, 16 acknowledged drafts, as few as 10 surviving to disk).

func ValidKind

func ValidKind(k string) bool

ValidKind reports whether k is a known item kind.

func ValidStatus added in v0.8.2

func ValidStatus(s string) bool

ValidStatus accepts the four ADR statuses and empty, which is conventionally read as "proposed" (see Item.Status).

Types

type Item

type Item struct {
	ID     string
	Kind   string
	State  string
	Title  string
	Dir    string // context dir (repo-relative, "" = root)
	Parent string

	// Refs is a general citation set: item IDs this item cites, of any kind
	// in either direction — research citing research, a proposal citing the
	// research that produced it, an ADR naming the research that fed it.
	// Order-preserving and duplicate-free (duplicates collapse on write).
	// It differs from both other cross-item fields: Parent is a single
	// structural owner (one task belongs to one proposal), Needs means
	// blocked-on and drives the escalation exits, while Refs carries no
	// lifecycle meaning at all — a plain citation the state machine never
	// interprets. Shape and existence are validated at the write path (see
	// UnknownRefs), never by Parse: a ref may legitimately point at an item
	// archived out of work.md.
	Refs []string

	Created string // YYYY-MM-DD
	Targets []string
	Body    string

	// Feedback-loop / escalation fields (SDD orchestration v2).
	Rounds   int      // done->active reopen count since the last reset (rescope/override-once)
	Grilled  string   // most recent grill feedback (freeform, survives rescope)
	Needs    []string // IDs this item is blocked on (decision items minted by Escalate)
	Override bool     // override-once already spent — cannot be spent again

	// ADR template fields (architecture decision records; kind=="adr").
	// Always empty and omitted for every other kind — these are structured
	// replacements for what used to be prose in Body, not general-purpose
	// fields. Status follows the classic ADR convention proposed|accepted|
	// superseded|deprecated; an empty Status on an adr item is conventionally
	// read as "proposed".
	Context      string // the forces and constraints behind the decision
	Decision     string // the chosen option, verbatim
	Consequences string // trade-offs and follow-on effects of the decision
	Status       string // proposed|accepted|superseded|deprecated
}

Item is one lifecycle item.

func Get

func Get(root workspace.Root, id string) (Item, bool, error)

Get finds one item by ID.

func LoadAll

func LoadAll(root workspace.Root) ([]Item, error)

LoadAll returns all active items of the workspace.

func LoadWork

func LoadWork(path, ctx string) ([]Item, error)

LoadWork parses a work.md file (missing file = no items).

Jump to

Keyboard shortcuts

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