lore

package
v0.48.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 19 Imported by: 0

Documentation

Overview

Package lore keeps a change's intent, decisions, findings, verdicts and lessons as immutable markdown entries beside the code (spec: docs/lore.md). It imports nothing else from this module, so it can be lifted out whole.

Index

Constants

View Source
const (
	TrailerChange = "Change"
	TrailerTask   = "Task"
)

The trailers that link a commit to its change; Task is f10's alias.

View Source
const (
	ViaTrailer = "trailer"
	ViaAnchor  = "anchor"
)

How Why linked the blamed commit to a change.

Variables

View Source
var (
	Kinds      = []Kind{KindIntent, KindDecision, KindFinding, KindVerdict, KindLesson}
	Severities = []string{"blocker", "major", "minor", "nit"}
	Statuses   = []string{"open", "fixed", "wontfix", "duplicate"}
	// Outcomes are a judge's four verdicts, spelled as its verdict files spell them.
	Outcomes = []string{"proceed", "proceed with changes", "rethink", "stop"}
	Gates    = []string{"judge", "review", "human"}
	// Sources are where a finding came from: a local review or the host's.
	Sources = []string{"local", "remote"}
	// Judged is what a verdict judged: the task itself, its plan, or the change.
	Judged = []string{"task", "plan", "change"}
)

The vocabularies the enumerated fields accept.

View Source
var ErrBadFileName = errors.New("lore: not an entry file name")

ErrBadFileName is a file in a change directory not named `<ulid>-<kind>.md`.

View Source
var ErrNoRecord = errors.New("lore: no record")

ErrNoRecord is a valid empty answer: no entries, no trailer, a line not committed yet, no email configured. It is never a git failure.

Functions

func Marshal

func Marshal(e Entry) ([]byte, error)

Marshal renders an entry as its file: frontmatter, a blank line, the body.

func RenderLessons

func RenderLessons(w io.Writer, path string, lessons []Entry) error

RenderLessons writes the lessons for a file, or says there are none.

func RenderShow

func RenderShow(w io.Writer, change string, entries []Entry) error

RenderShow writes a change's entries for a person: a header line per entry, its fields indented under it, then its body.

func RenderWhy

func RenderWhy(w io.Writer, r *WhyResult) error

RenderWhy writes a why answer: the line and how it was linked, then per change its intent, its latest verdict and the entries on the line.

func Validate

func Validate(e Entry) error

Validate checks one entry against the schema in docs/lore.md, on its own: what needs the rest of the change (supersedes targets, forks) is Store.Validate's. It returns nil or a *ValidationError.

Types

type Agent

type Agent struct {
	Model   string `json:"model,omitempty"   yaml:"model,omitempty"`
	Via     string `json:"via,omitempty"     yaml:"via,omitempty"`
	Session string `json:"session,omitempty" yaml:"session,omitempty"`
}

Agent is who did the work when it was not the human alone.

type Anchor

type Anchor struct {
	Path    string `json:"path"               yaml:"path"`
	Line    int    `json:"line"               yaml:"line"`
	EndLine int    `json:"end_line,omitempty" yaml:"end_line,omitempty"`
	Commit  string `json:"commit"             yaml:"commit"`
	Symbol  string `json:"symbol,omitempty"   yaml:"symbol,omitempty"`
}

Anchor is where an entry points in the code. EndLine 0 means the anchor covers Line alone.

func ParseAnchor

func ParseAnchor(s string) (Anchor, error)

ParseAnchor reads `path:line[-end][@commit][#symbol]`. The commit may be left out; Validate requires one by the time the entry is written.

func (Anchor) Covers

func (a Anchor) Covers(file string, line int) bool

Covers reports whether the anchor is on path and its lines include line.

func (Anchor) String

func (a Anchor) String() string

String is the anchor in the form `--anchor` takes.

type CLIGit

type CLIGit struct {
	Dir string
}

CLIGit answers through the git binary, run in Dir, the repository root.

func (CLIGit) BlameLine

func (g CLIGit) BlameLine(ctx context.Context, path string, line int, rev string) (string, error)

BlameLine asks blame for one line; git names an uncommitted line with an all-zero sha.

func (CLIGit) Head

func (g CLIGit) Head(ctx context.Context) (string, error)

Head asks rev-parse, which fails on an unborn branch.

func (CLIGit) IsAncestor

func (g CLIGit) IsAncestor(ctx context.Context, ancestor, commit string) (bool, error)

IsAncestor reads merge-base's exit code: 0 yes, 1 no; an unknown object is a no too, since an anchor may name a commit squashed away.

func (CLIGit) Trailers

func (g CLIGit) Trailers(ctx context.Context, commit string) (map[string][]string, error)

Trailers parses `%(trailers:only,unfold)`: one `Key: value` per line.

func (CLIGit) UserEmail

func (g CLIGit) UserEmail(ctx context.Context) (string, error)

UserEmail reads the config; git exits 1 with no output for an unset key.

type CheckResult

type CheckResult struct {
	Change  string   `json:"change"`
	Reasons []string `json:"reasons,omitempty"`
}

CheckResult is the gate's answer: it passes with no reasons.

func (*CheckResult) Pass

func (c *CheckResult) Pass() bool

Pass reports whether nothing held the change back.

type Entry

type Entry struct {
	ID         string    `json:"id"                   yaml:"-"`
	Kind       Kind      `json:"kind"                 yaml:"kind"`
	Change     string    `json:"change"               yaml:"change"`
	Human      string    `json:"human"                yaml:"human"`
	Agent      *Agent    `json:"agent,omitempty"      yaml:"agent,omitempty"`
	At         time.Time `json:"at"                   yaml:"at"`
	Anchors    []Anchor  `json:"anchors,omitempty"    yaml:"anchors,omitempty"`
	Supersedes string    `json:"supersedes,omitempty" yaml:"supersedes,omitempty"`

	Chosen   string     `json:"chosen,omitempty"   yaml:"chosen,omitempty"`
	Rejected []Rejected `json:"rejected,omitempty" yaml:"rejected,omitempty"`

	Severity   string `json:"severity,omitempty"   yaml:"severity,omitempty"`
	Status     string `json:"status,omitempty"     yaml:"status,omitempty"`
	Resolution string `json:"resolution,omitempty" yaml:"resolution,omitempty"`
	Source     string `json:"source,omitempty"     yaml:"source,omitempty"`

	Outcome string `json:"outcome,omitempty" yaml:"outcome,omitempty"`
	Gate    string `json:"gate,omitempty"    yaml:"gate,omitempty"`
	Commit  string `json:"commit,omitempty"  yaml:"commit,omitempty"`
	Judged  string `json:"judged,omitempty"  yaml:"judged,omitempty"`

	Retired bool `json:"retired,omitempty" yaml:"retired,omitempty"`

	Body string `json:"body" yaml:"-"`
	// contains filtered or unexported fields
}

Entry is one lore file. ID comes from the filename, never the frontmatter; the per-kind fields are flat, and Validate rejects one on another kind.

func Parse

func Parse(name string, data []byte) (Entry, error)

Parse reads one entry file. name is the file's base name, which carries the id; a kind that disagrees with the frontmatter is Validate's to report.

func (*Entry) FileName

func (e *Entry) FileName() string

FileName is the entry's name inside its change directory.

type Git

type Git interface {
	// BlameLine is the commit that last touched the line, in the working
	// tree or, with rev, in the file as that revision has it; ErrNoRecord
	// when the line is not committed yet, or the path is absent at rev.
	BlameLine(ctx context.Context, path string, line int, rev string) (string, error)
	// Trailers is the commit message's trailers, keys as written except
	// Change and Task, which are normalised so a lookup ignores case.
	Trailers(ctx context.Context, commit string) (map[string][]string, error)
	// UserEmail is `git config user.email`; ErrNoRecord when unset.
	UserEmail(ctx context.Context) (string, error)
	// Head is HEAD's abbreviated sha; ErrNoRecord before the first commit.
	Head(ctx context.Context) (string, error)
	// IsAncestor reports whether ancestor is reachable from commit; a sha
	// this clone does not have is false, not an error.
	IsAncestor(ctx context.Context, ancestor, commit string) (bool, error)
}

Git is all lore asks of git. Paths are relative to the repository root.

type Kind

type Kind string

Kind is an entry's kind, in the order a change lives through them.

const (
	KindIntent   Kind = "intent"
	KindDecision Kind = "decision"
	KindFinding  Kind = "finding"
	KindVerdict  Kind = "verdict"
	KindLesson   Kind = "lesson"
)

type Problem

type Problem struct {
	File    string `json:"file"`
	Message string `json:"message"`
}

Problem is one thing Validate found, in the file it names.

func (Problem) String

func (p Problem) String() string

type Record

type Record struct {
	Change  string  `json:"change"`
	Intent  *Entry  `json:"intent,omitempty"`
	Verdict *Entry  `json:"verdict,omitempty"`
	Entries []Entry `json:"entries"` // decisions, findings and lessons on the line
}

Record is one change's part of a why answer.

type Rejected

type Rejected struct {
	Option string `json:"option" yaml:"option"`
	Reason string `json:"reason" yaml:"reason"`
}

Rejected is an option a decision did not take, and why.

type Store

type Store struct {
	Root string
	Git  Git
	// Now and Entropy default to the clock and ulid's monotonic source; a
	// test sets them for stable ids.
	Now     func() time.Time
	Entropy io.Reader
}

Store is one lore root: a directory per change, a file per entry.

func (*Store) Add

func (s *Store) Add(ctx context.Context, e Entry) (string, error)

Add validates e against the schema and the rest of its change, writes it, and returns its id. Human comes from git when unset, At from the clock. There is no update or delete: a file is created once and never rewritten.

func (*Store) Changes

func (s *Store) Changes() ([]string, error)

Changes lists the change directories under the root, sorted. No root is no changes.

func (*Store) Check

func (s *Store) Check(change string) (*CheckResult, error)

Check fails a change with no intent, no verdict or a latest verdict that is not a proceed, or a blocker or major finding still open.

func (*Store) Effective

func (s *Store) Effective(change string) ([]Entry, error)

Effective is the change's entries with every superseded one dropped, the intent first, then by time. A fork keeps its later head.

func (*Store) Lessons

func (s *Store) Lessons(path string) ([]Entry, error)

Lessons is every effective, non-retired lesson anchored to path, across every change, oldest first.

func (*Store) Load

func (s *Store) Load(change string) ([]Entry, error)

Load reads every entry of the change, by id, which is time order. A change with no directory is ErrNoRecord; a file that does not parse is an error naming it.

func (*Store) Validate

func (s *Store) Validate(change string) ([]Problem, error)

Validate lints every entry of the change: the schema, the filename and directory against the frontmatter, and the supersedes rules.

func (*Store) Why

func (s *Store) Why(ctx context.Context, path string, line int, rev string) (*WhyResult, error)

Why is the lore on line of path: entries anchored there in the changes the blamed commit's Change: (else Task:) trailers name, else entries anchored at that commit on the path; none is ErrNoRecord. rev blames the file as that revision had it, so a rewritten line answers with the change that wrote it.

type ValidationError

type ValidationError struct {
	Problems []string
}

ValidationError lists every problem found, not the first.

func (*ValidationError) Error

func (v *ValidationError) Error() string

type WhyResult

type WhyResult struct {
	Path    string   `json:"path"`
	Line    int      `json:"line"`
	Rev     string   `json:"rev,omitempty"` // the revision the line was read at; the working tree when empty
	Commit  string   `json:"commit"`
	Via     string   `json:"via"`
	Records []Record `json:"records"`
}

WhyResult is what lore knows about one line.

Jump to

Keyboard shortcuts

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