workflow

package
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Mar 18, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Dispatch

func Dispatch(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter) error

Dispatch is the central router for the post-commit hook workflow. All contextual detection (non-TTY, doc-skip, merge, rebase, cherry-pick, amend) is delegated to Detect() inside HandleReactive (Story 2.5).

func HandleProactive

func HandleProactive(ctx context.Context, workDir string, streams domain.IOStreams, opts ProactiveOpts) error

HandleProactive runs the manual or retroactive documentation flow for `lore new`. When opts.Commit is non-nil, retroactive mode pre-fills Type/What from the commit and sets generated_by to "retroactive" with the commit hash in front matter.

func HandleReactive

func HandleReactive(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter) error

HandleReactive runs the full interactive post-commit flow:

  1. Detects context (merge, rebase, cherry-pick, amend, non-TTY, doc-skip).
  2. Reads HEAD commit info via gitAdapter.
  3. Presents the question flow on streams.
  4. Generates and persists the document under .lore/docs/.
  5. Prints "Captured {filename}" on stderr.

On context cancellation (Ctrl+C forwarded via signal.NotifyContext in main), any partial answers collected before the interruption are saved to .lore/pending/{hash}.yaml (silent best-effort). HandleReactive runs the full interactive post-commit flow with default detection options. Use handleReactiveWithOpts for injection in tests.

func IsInteractiveTTY

func IsInteractiveTTY(streams domain.IOStreams) bool

IsInteractiveTTY reports whether the session is running in an interactive TTY. Detection order (deterministic, not timer-based):

  1. TERM=dumb → false (Emacs shell-mode, IDEs)
  2. LORE_LINE_MODE=1 → false (forced plain output)
  3. stdin or stderr not TTY → false
  4. Otherwise → true

Story 2.5 reuses this helper in detection.go without duplication.

func MapCommitType

func MapCommitType(ccType string) string

MapCommitType converts a Conventional Commit type to a Lore document type. Falls back to "note" for unknown types.

func RelativeAge

func RelativeAge(d time.Duration) string

RelativeAge formats a duration into a human-readable relative age string.

func ResolvePending

func ResolvePending(ctx context.Context, workDir string, streams domain.IOStreams, item PendingItem, gitAdapter domain.GitAdapter, opts ResolveOpts) error

ResolvePending resolves a pending item: displays commit context, asks only remaining questions (preserving partial answers), generates the document via the standard pipeline, and deletes the pending file.

func SavePending

func SavePending(workDir string, record PendingRecord) error

SavePending writes partial answers to .lore/pending/{hash}.yaml. The directory is created with os.MkdirAll if absent (per NOTE m19). Relative paths work when CWD is the git work tree (item L19).

Types

type Answers

type Answers struct {
	Type         string
	What         string
	Why          string
	Alternatives string // empty if express mode skipped
	Impact       string // empty if express mode skipped
}

Answers holds the user's responses to the interactive question flow.

func (Answers) ToGenerateInput

func (a Answers) ToGenerateInput(commit *domain.CommitInfo, generatedBy string) generator.GenerateInput

ToGenerateInput converts Answers + CommitInfo into a generator.GenerateInput. The conversion happens in workflow/ to avoid circular deps (generator → workflow). generatedBy distinguishes hook-triggered ("hook") from manual ("manual") flows so that the front-matter field is correct for both reactive.go and proactive.go.

type DetectOpts

type DetectOpts struct {
	// GetEnv reads an environment variable. Defaults to os.Getenv.
	GetEnv func(string) string

	// IsTTY reports whether the given streams represent an interactive TTY.
	// Defaults to IsInteractiveTTY (which delegates to ui.IsTerminal).
	// M2 fix: replaces the ForceInteractive bool antipattern — tests inject
	// func(_ domain.IOStreams) bool { return true } to bypass TTY detection
	// without polluting production code with a test-only flag.
	IsTTY func(domain.IOStreams) bool

	// Corpus provides doc existence checks for cherry-pick (AC-5) and amend
	// (AC-4) detection. When nil, these checks are skipped and the old
	// unconditional skip/amend behavior applies (backward compat for tests
	// that don't need doc existence verification).
	Corpus domain.CorpusReader
}

DetectOpts holds injectable dependencies for testability (NOTE m22).

type DetectionResult

type DetectionResult struct {
	Action  string // "proceed", "skip", "defer", "amend"
	Reason  string // "doc-skip", "non-tty", "rebase", "merge", "cherry-pick", "amend"
	Message string // human-readable message for stderr (empty = silent)
}

DetectionResult describes how the hook should handle the current commit.

func Detect

func Detect(ctx context.Context, ref string, git domain.GitAdapter, streams domain.IOStreams, opts DetectOpts) (DetectionResult, error)

Detect determines the appropriate action for the current commit context.

Detection order (first match wins — priority is deterministic per Dev Notes):

  1. [doc-skip] marker → skip silently (explicit developer intent, exit 0)
  2. Non-TTY / TERM=dumb → defer pending (CI must never block)
  3. Rebase in progress → defer pending (avoid questionnaire per replay)
  4. Merge commit → skip with 1-line stderr message
  5. Cherry-pick → skip silently (CHERRY_PICK_HEAD present)
  6. Amend → propose modification of existing doc
  7. Otherwise → proceed with normal interactive flow

GitAdapter methods do NOT accept ctx (per architecture.md NOTE C2). Cancellation is managed at the workflow/cobra level.

type LineRenderer

type LineRenderer struct {
	// contains filtered or unexported fields
}

LineRenderer is the non-TTY renderer for CI/pipe environments. One line per event, no ANSI rewriting — CI-compatible.

func NewLineRenderer

func NewLineRenderer(streams domain.IOStreams) *LineRenderer

func (*LineRenderer) ExpressSkip

func (r *LineRenderer) ExpressSkip(skipped int)

func (*LineRenderer) Progress

func (r *LineRenderer) Progress(current, total int, label string)

func (*LineRenderer) QuestionConfirm

func (r *LineRenderer) QuestionConfirm(question string, answer string)

func (*LineRenderer) QuestionStart

func (r *LineRenderer) QuestionStart(question string, defaultVal string)

type Option

type Option func(*flowOptions)

Option is a functional option for QuestionFlow.

func WithExpressThreshold

func WithExpressThreshold(d time.Duration) Option

WithExpressThreshold sets the cumulative time threshold for express mode.

type PendingAnswers

type PendingAnswers struct {
	Type         string `yaml:"type"`
	What         string `yaml:"what"`
	Why          string `yaml:"why,omitempty"`
	Alternatives string `yaml:"alternatives,omitempty"`
	Impact       string `yaml:"impact,omitempty"`
}

PendingAnswers holds the question responses collected before interruption.

type PendingItem

type PendingItem struct {
	Filename      string    // e.g. "abc1234.yaml"
	CommitHash    string    // short hash (from filename / record.Commit)
	CommitMessage string    // from record.Message
	CommitDate    time.Time // parsed from record.Date
	Answers       PendingAnswers
	Progress      string // "3/5"
	RelativeAge   string // "2 days ago"
}

PendingItem is the view-model for a single pending entry, used by ListPending and the cmd layer.

func ListPending

func ListPending(ctx context.Context, pendingDir string, warnWriter func(string)) ([]PendingItem, error)

ListPending reads and parses all YAML files in pendingDir, returning them sorted by date descending (most recent first). Corrupt files are skipped with a warning written to warnWriter (if non-nil).

func SkipPending

func SkipPending(ctx context.Context, pendingDir string, commitHash string) (PendingItem, error)

SkipPending deletes the pending file matching the given commit hash without creating a document. The hash must match exactly or be a unique prefix among all pending items.

type PendingRecord

type PendingRecord struct {
	Commit  string         `yaml:"commit"`
	Date    string         `yaml:"date"`
	Message string         `yaml:"message"`
	Answers PendingAnswers `yaml:"answers"`
	Status  string         `yaml:"status"` // "partial" | "deferred"
	Reason  string         `yaml:"reason"` // "interrupted" | "non-tty" | "rebase"
}

PendingRecord is the YAML structure written to .lore/pending/{hash}.yaml on Ctrl+C or non-TTY deferral. The file is retained for manual inspection until `lore pending` (Epic 10) processes it.

func BuildPendingRecord

func BuildPendingRecord(answers Answers, commitHash, commitMsg, reason, status string) PendingRecord

BuildPendingRecord converts partial answers into a PendingRecord. commitHash / commitMsg may be empty if the commit could not be read. status must be "partial" (interrupted mid-flow) or "deferred" (non-TTY / rebase batch).

type ProactiveOpts

type ProactiveOpts struct {
	Type   string                      // pre-filled type (may be empty)
	What   string                      // pre-filled what (may be empty)
	Why    string                      // pre-filled why (may be empty)
	Commit *domain.CommitInfo          // retroactive mode: resolved commit info (nil → manual mode)
	IsTTY  func(domain.IOStreams) bool // N4 fix: optional TTY override for testing (nil → IsInteractiveTTY)
}

ProactiveOpts holds pre-filled arguments from the CLI for lore new.

type ProgressRenderer

type ProgressRenderer struct {
	// contains filtered or unexported fields
}

ProgressRenderer is the TTY renderer that condenses confirmed answers via ANSI. Budget: ~7 lines stderr max.

func NewProgressRenderer

func NewProgressRenderer(streams domain.IOStreams) *ProgressRenderer

func (*ProgressRenderer) ExpressSkip

func (r *ProgressRenderer) ExpressSkip(skipped int)

ExpressSkip prints the express skip feedback.

func (*ProgressRenderer) Progress

func (r *ProgressRenderer) Progress(current, total int, label string)

Progress renders the progress bar [##·] N+ label.

func (*ProgressRenderer) QuestionConfirm

func (r *ProgressRenderer) QuestionConfirm(question string, answer string)

QuestionConfirm updates the confirmed bar and redraws.

func (*ProgressRenderer) QuestionStart

func (r *ProgressRenderer) QuestionStart(question string, defaultVal string)

QuestionStart prints the progress bar + question prompt. Overwrites previous lines via cursor-up escape.

type QuestionFlow

type QuestionFlow struct {
	// contains filtered or unexported fields
}

QuestionFlow orchestrates the inverse funnel question sequence.

func NewQuestionFlow

func NewQuestionFlow(streams domain.IOStreams, renderer Renderer, opts ...Option) *QuestionFlow

NewQuestionFlow creates a QuestionFlow with the given renderer and options.

func (*QuestionFlow) AskAlternatives

func (q *QuestionFlow) AskAlternatives(ctx context.Context) (string, error)

AskAlternatives prompts for alternatives considered (optional).

func (*QuestionFlow) AskImpact

func (q *QuestionFlow) AskImpact(ctx context.Context) (string, error)

AskImpact prompts for impact (optional).

func (*QuestionFlow) AskQuestions

func (q *QuestionFlow) AskQuestions(ctx context.Context, opts QuestionOpts) (Answers, error)

AskQuestions is the unified question flow for all documentation paths. It handles pre-filled answers, express mode, and commit-based defaults.

On error (including context cancellation), AskQuestions returns the partial answers collected so far. Callers are responsible for saving these as pending via BuildPendingRecord + SavePending — AskQuestions does not persist state because it lacks the commit hash and context needed for the pending record.

Behavior per field:

  • Pre-filled + valid → confirm and skip (no prompt)
  • Pre-filled but invalid type → interactive with "note" default
  • Empty → interactive prompt with commit-derived defaults when available
  • Express mode → if first 3 Qs answered within expressThreshold, skip Alternatives+Impact

func (*QuestionFlow) AskType

func (q *QuestionFlow) AskType(ctx context.Context, defaultType string) (string, error)

AskType prompts for document type with a pre-filled default. Enter confirms, any input replaces.

func (*QuestionFlow) AskWhat

func (q *QuestionFlow) AskWhat(ctx context.Context, defaultWhat string) (string, error)

AskWhat prompts for what was done, pre-filled from commit subject.

func (*QuestionFlow) AskWhy

func (q *QuestionFlow) AskWhy(ctx context.Context) (string, error)

AskWhy prompts for the reason — the single true question of the flow.

func (*QuestionFlow) RunFlow

func (q *QuestionFlow) RunFlow(ctx context.Context, commit *domain.CommitInfo) (Answers, error)

RunFlow orchestrates all 5 questions with express mode and returns Answers. commitInfo is used to pre-fill Type and What defaults. Package-internal: called by runDocumentationFlow in reactive.go.

type QuestionOpts

type QuestionOpts struct {
	PreFilled  Answers            // partial or full pre-filled answers (resolve/proactive)
	Express    bool               // enable express mode — timer-based skip for Alternatives+Impact (reactive only)
	CommitInfo *domain.CommitInfo // for pre-fill defaults (type from MapCommitType, what from Subject)
}

QuestionOpts controls the behavior of AskQuestions for all 4 documentation paths.

type Renderer

type Renderer interface {
	// QuestionStart displays a question before the user types.
	QuestionStart(question string, defaultVal string)
	// QuestionConfirm condenses a confirmed answer into the summary bar.
	QuestionConfirm(question string, answer string)
	// Progress shows the current question position and label.
	Progress(current, total int, label string)
	// ExpressSkip announces that optional questions were skipped.
	ExpressSkip(skipped int)
}

Renderer abstracts TTY vs non-TTY output during the question flow. Implementations: ProgressRenderer (TTY) and LineRenderer (non-TTY/CI).

func NewRenderer

func NewRenderer(streams domain.IOStreams) Renderer

NewRenderer returns the appropriate Renderer for the given streams.

type ResolveOpts

type ResolveOpts struct {
	IsTTY func(domain.IOStreams) bool // optional TTY override for testing
}

ResolveOpts holds options for ResolvePending.

Jump to

Keyboard shortcuts

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