workflow

package
v1.2.3 Latest Latest
Warning

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

Go to latest
Published: May 3, 2026 License: AGPL-3.0 Imports: 22 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrInterrupted = fmt.Errorf("interrupted")

ErrInterrupted signals that the user pressed Ctrl+C during an interactive prompt.

Functions

func CountDiffLines added in v1.0.0

func CountDiffLines(diff string) (added, deleted int)

CountDiffLines counts added and deleted lines from unified diff output. Limited to first 10000 lines for performance on very large diffs.

func Dispatch

func Dispatch(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter, engine *decision.Engine, store domain.LoreStore) error

Dispatch is the central router for the post-commit hook workflow. engine and store may be nil (backward compat — graceful degradation).

func DispatchFull added in v1.1.0

func DispatchFull(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter, engine *decision.Engine, store domain.LoreStore, cfg DispatchConfig) error

DispatchFull is the full dispatch with all configuration options.

func DispatchWithNotifyConfig added in v1.0.0

func DispatchWithNotifyConfig(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter, engine *decision.Engine, store domain.LoreStore, notifyCfg *notify.NotifyConfig) error

DispatchWithNotifyConfig is Dispatch with explicit notification configuration (ADR-023). notifyCfg may be nil (defaults to DefaultNotifyConfig).

func ExtractFilesFromDiff added in v1.0.0

func ExtractFilesFromDiff(diff string) []string

ExtractFilesFromDiff parses file names from unified diff output (+++ b/ lines).

func FlushOnInterrupt added in v1.1.0

func FlushOnInterrupt()

FlushOnInterrupt saves the current flow state as pending. Called from the signal handler in root.go. Safe to call multiple times or with no state.

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 HandleReactiveWithEngine added in v1.0.0

func HandleReactiveWithEngine(ctx context.Context, workDir string, streams domain.IOStreams, gitAdapter domain.GitAdapter, engine *decision.Engine, store domain.LoreStore) error

HandleReactiveWithEngine runs the full interactive post-commit flow with optional Decision Engine and store.

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

This helper is reused 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 PreflightCheck added in v1.1.0

func PreflightCheck(workDir string) error

PreflightCheck validates that the documentation pipeline can succeed BEFORE asking the user any questions. Returns nil if everything is OK, or a descriptive error if the pipeline would fail post-questions.

Why: the user should never spend 90 seconds answering questions only to hit a template or filesystem error at the end. This preserves the "90 seconds or nothing" contract.

func RegisterInterruptState added in v1.1.0

func RegisterInterruptState(workDir, commitHash, commitMsg string, answers *Answers)

RegisterInterruptState sets the current flow state so FlushOnInterrupt can save a pending record. Call with nil answers to clear.

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

	// Store provides O(1) doc lookup by commit hash. When nil, falls back to corpus scan.
	Store domain.LoreStore

	// Engine is the Decision Engine for multi-signal scoring.
	// When nil, step 7 is skipped and fallback proceed applies (backward compat).
	Engine *decision.Engine

	// SignalCtx holds pre-built signal context for the Decision Engine.
	// Only used when Engine is non-nil.
	SignalCtx *decision.SignalContext

	// NotifyConfig holds notification preferences from .lorerc.
	// Used by handleDetectionResult to configure non-TTY notifications (ADR-023).
	// When nil, DefaultNotifyConfig() is used.
	NotifyConfig *notify.NotifyConfig

	// AmendPrompt controls whether to ask "Document this change?" (Question 0)
	// before the amend flow in TTY mode. Defaults to true.
	// Set to false via hooks.amend_prompt=false in .lorerc to skip the prompt.
	AmendPrompt *bool
}

DetectOpts holds injectable dependencies for testability (NOTE m22).

type DetectionAction added in v1.0.0

type DetectionAction = string

DetectionAction represents the possible outcomes of commit detection.

const (
	ActionProceed     DetectionAction = "proceed"
	ActionSkip        DetectionAction = "skip"
	ActionDefer       DetectionAction = "defer"
	ActionAmend       DetectionAction = "amend"
	ActionAutoSkip    DetectionAction = "auto-skip"
	ActionSuggestSkip DetectionAction = "suggest-skip"
	ActionAskReduced  DetectionAction = "ask-reduced"
	ActionAskFull     DetectionAction = "ask-full"
)

type DetectionResult

type DetectionResult struct {
	Action                 string // "proceed", "skip", "defer", "amend", "auto-skip", "suggest-skip", "ask-reduced", "ask-full"
	Reason                 string
	Message                string // human-readable message for stderr (empty = silent)
	Score                  int    // 0-100 from Decision Engine (0 if no scoring)
	QuestionMode           string // full, reduced, confirm, none
	PrefilledWhat          string
	PrefilledWhy           string
	PrefilledWhyConfidence float64
}

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 DispatchConfig added in v1.1.0

type DispatchConfig struct {
	NotifyConfig *notify.NotifyConfig
	AmendPrompt  *bool // nil = default (true); set to false to skip Question 0
}

DispatchConfig holds optional configuration for the post-commit dispatch.

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` 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 using an interactive arrow-key selector (TTY) or a text input with validation (non-TTY). Invalid types are rejected with a retry loop.

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.

func (*QuestionFlow) RunFlowWithMode added in v1.0.0

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

RunFlowWithMode orchestrates the question flow with Decision Engine context. In "reduced" mode: Type and What are pre-filled, only Why is asked interactively. In "confirm" mode: all 3 are pre-filled, user confirms with Enter. If detection is nil, behaves like RunFlow (full mode).

type QuestionMode added in v1.0.0

type QuestionMode = string

QuestionMode controls the depth of interactive questioning.

const (
	QModeFull    QuestionMode = "full"
	QModeReduced QuestionMode = "reduced"
	QModeConfirm QuestionMode = "confirm"
	QModeNone    QuestionMode = "none"
)

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

	// Batch fields — when Type, What, and Why are all non-empty,
	// skip interactive prompts and generate directly (ADR-023 AC-12).
	Type         string
	What         string
	Why          string
	Alternatives string
	Impact       string
}

ResolveOpts holds options for ResolvePending.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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