You've already forked agentic-coding-workflow
124 lines
4.1 KiB
Go
124 lines
4.1 KiB
Go
package docs
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/git"
|
|
"github.com/yan1h/agent-coding-workflow/internal/jobs"
|
|
)
|
|
|
|
// maxDiffChars bounds the diffstat fed to the LLM so a huge commit can't blow the
|
|
// prompt budget.
|
|
const maxDiffChars = 12000
|
|
|
|
// WorkspaceLocator resolves the on-disk directory holding a commit's files.
|
|
type WorkspaceLocator interface {
|
|
LocateWorkspace(ctx context.Context, wsID uuid.UUID) (mainPath string, err error)
|
|
LocateWorktree(ctx context.Context, worktreeID uuid.UUID) (path string, err error)
|
|
}
|
|
|
|
// Summarizer turns a diffstat into a markdown summary. Implemented in app.go over
|
|
// the llm registry (default chat endpoint); the SummaryRunner only sees this
|
|
// narrow contract so tests can inject a canned-output fake.
|
|
type Summarizer interface {
|
|
// Summarize returns (title, bodyMarkdown, modelID, error).
|
|
Summarize(ctx context.Context, branch, commitSHA, diff string) (title, bodyMD, model string, err error)
|
|
}
|
|
|
|
// SummaryRunner is the jobs.Handler that generates a commit_summaries row. It is
|
|
// event-driven (enqueued post-commit), never a periodic ticker.
|
|
type SummaryRunner struct {
|
|
repo Repository
|
|
locator WorkspaceLocator
|
|
gitr git.Runner
|
|
summ Summarizer
|
|
log *slog.Logger
|
|
}
|
|
|
|
// NewSummaryRunner constructs a SummaryRunner. log may be nil.
|
|
func NewSummaryRunner(repo Repository, locator WorkspaceLocator, gitr git.Runner, summ Summarizer, log *slog.Logger) *SummaryRunner {
|
|
if log == nil {
|
|
log = slog.Default()
|
|
}
|
|
return &SummaryRunner{repo: repo, locator: locator, gitr: gitr, summ: summ, log: log}
|
|
}
|
|
|
|
var _ jobs.Handler = (*SummaryRunner)(nil)
|
|
|
|
// Type returns the job type this handler processes.
|
|
func (h *SummaryRunner) Type() jobs.JobType { return JobTypeCommitSummary }
|
|
|
|
// Handle generates and upserts a commit summary. Idempotent: a completed summary
|
|
// for the same (workspace, commit, kind) is left untouched.
|
|
func (h *SummaryRunner) Handle(ctx context.Context, job jobs.Job) error {
|
|
var p summaryPayload
|
|
if err := json.Unmarshal(job.Payload, &p); err != nil {
|
|
return jobs.Permanent(fmt.Errorf("docs.commit_summary: bad payload: %w", err))
|
|
}
|
|
|
|
// Skip if a completed summary already exists (idempotent re-enqueue).
|
|
if existing, err := h.repo.Get(ctx, p.WorkspaceID, p.CommitSHA, KindCommit); err == nil && existing != nil && existing.Status == StatusCompleted {
|
|
return nil
|
|
}
|
|
|
|
dir := ""
|
|
if p.WorktreeID != nil {
|
|
path, lerr := h.locator.LocateWorktree(ctx, *p.WorktreeID)
|
|
if lerr != nil {
|
|
return fmt.Errorf("locate worktree: %w", lerr)
|
|
}
|
|
dir = path
|
|
} else {
|
|
path, lerr := h.locator.LocateWorkspace(ctx, p.WorkspaceID)
|
|
if lerr != nil {
|
|
return fmt.Errorf("locate workspace: %w", lerr)
|
|
}
|
|
dir = path
|
|
}
|
|
|
|
diff, err := h.gitr.DiffStat(ctx, dir, p.CommitSHA)
|
|
if err != nil {
|
|
return fmt.Errorf("git show --stat: %w", err)
|
|
}
|
|
if len(diff) > maxDiffChars {
|
|
diff = diff[:maxDiffChars] + "\n...[truncated]"
|
|
}
|
|
|
|
title, body, model, serr := h.summ.Summarize(ctx, p.Branch, p.CommitSHA, diff)
|
|
if serr != nil {
|
|
// Persist a failed row so the failure is visible, then return for retry.
|
|
msg := serr.Error()
|
|
if _, uerr := h.repo.Upsert(ctx, UpsertInput{
|
|
WorkspaceID: p.WorkspaceID, WorktreeID: p.WorktreeID, Branch: p.Branch,
|
|
CommitSHA: p.CommitSHA, Kind: KindCommit, BodyMD: "",
|
|
Status: StatusFailed, Error: &msg,
|
|
}); uerr != nil {
|
|
h.log.Error("docs.summary.upsert_failed_row", "commit", p.CommitSHA, "err", uerr.Error())
|
|
}
|
|
return fmt.Errorf("summarize: %w", serr)
|
|
}
|
|
|
|
var titlePtr, modelPtr *string
|
|
if t := strings.TrimSpace(title); t != "" {
|
|
titlePtr = &t
|
|
}
|
|
if m := strings.TrimSpace(model); m != "" {
|
|
modelPtr = &m
|
|
}
|
|
if _, err := h.repo.Upsert(ctx, UpsertInput{
|
|
WorkspaceID: p.WorkspaceID, WorktreeID: p.WorktreeID, Branch: p.Branch,
|
|
CommitSHA: p.CommitSHA, Kind: KindCommit, Title: titlePtr,
|
|
BodyMD: body, Model: modelPtr, Status: StatusCompleted,
|
|
}); err != nil {
|
|
return fmt.Errorf("upsert summary: %w", err)
|
|
}
|
|
h.log.Info("docs.summary.completed", "workspace_id", p.WorkspaceID, "commit", p.CommitSHA)
|
|
return nil
|
|
}
|